From d152bbb14112c9c08f815af2e17232403dd82381 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Wed, 13 Mar 2019 13:53:34 +0200 Subject: [PATCH 01/22] Add async to kin-sdk --- Pipfile | 3 +- Pipfile.lock | 156 ++++-------------------- kin/__init__.py | 5 +- kin/account.py | 156 ++++++++++-------------- kin/blockchain/builder.py | 67 ---------- kin/blockchain/errors.py | 9 -- kin/blockchain/horizon.py | 248 -------------------------------------- kin/client.py | 151 +++++++++-------------- kin/version.py | 2 +- requirements-dev.txt | 1 + requirements.txt | 2 +- setup.py | 2 +- 12 files changed, 156 insertions(+), 646 deletions(-) delete mode 100644 kin/blockchain/builder.py delete mode 100644 kin/blockchain/horizon.py diff --git a/Pipfile b/Pipfile index 0a8a2bd..fd81d65 100644 --- a/Pipfile +++ b/Pipfile @@ -8,7 +8,7 @@ python_version = "3.7" [packages] schematics = "==2.0.1" -kin-base = "==1.0.7" +kin-base = {ref = "async-kin", git = "https://github.com/kinecosystem/py-kin-base"} [dev-packages] attrs = "==17.4.0" @@ -19,5 +19,6 @@ pluggy = "==0.6.0" py = "==1.5.2" pytest = "==3.4.0" pytest-cov = "==2.5.1" +pytest-asyncio = "*" #[pipenv] #keep_outdated = true diff --git a/Pipfile.lock b/Pipfile.lock index 4a67d4e..31b88b2 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "5ce413a399d5244cafeba64f23e5660e2e62de6e9d49bab5d21e5d4110f68c8b" + "sha256": "14fbb77daa8dd5fd4ce6010ebeba639708d0e1e1ff78ea9743342006cd243b03" }, "pipfile-spec": 6, "requires": { @@ -16,101 +16,9 @@ ] }, "default": { - "certifi": { - "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" - ], - "version": "==2018.11.29" - }, - "chardet": { - "hashes": [ - "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", - "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" - ], - "version": "==3.0.4" - }, - "crc16": { - "hashes": [ - "sha256:1b9f697a93491ae42ed653c1e78ea25a33532afab87b513e6890975450271a01", - "sha256:c1f86aa0390f4baf07d2631b16b979580eae1d9a973a826ce45353a22ee8d396" - ], - "markers": "sys_platform != 'win32' and sys_platform != 'cygwin'", - "version": "==0.1.1" - }, - "ed25519": { - "hashes": [ - "sha256:2991b94e1883d1313c956a1e3ced27b8a2fdae23ac40c0d9d0b103d5a70d1d2a" - ], - "markers": "sys_platform != 'win32' and sys_platform != 'cygwin'", - "version": "==1.4" - }, - "idna": { - "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" - ], - "version": "==2.7" - }, "kin-base": { - "hashes": [ - "sha256:0f9b13a256bfc79372111389e1f80ec6f10a5bd85aaf1a6acd1c156173a81123", - "sha256:ed3994c12dcbf175eef101d49090dc829e2057714d22a640ae12aea9e118ee66" - ], - "index": "pypi", - "version": "==1.0.7" - }, - "mnemonic": { - "hashes": [ - "sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d" - ], - "version": "==0.18" - }, - "numpy": { - "hashes": [ - "sha256:1b1cf8f7300cf7b11ddb4250b3898c711a6187df05341b5b7153db23ffe5d498", - "sha256:27a0d018f608a3fe34ac5e2b876f4c23c47e38295c47dd0775cc294cd2614bc1", - "sha256:3fde172e28c899580d32dc21cb6d4a1225d62362f61050b654545c662eac215a", - "sha256:497d7c86df4f85eb03b7f58a7dd0f8b948b1f582e77629341f624ba301b4d204", - "sha256:4e28e66cf80c09a628ae680efeb0aa9a066eb4bb7db2a5669024c5b034891576", - "sha256:58be95faf0ca2d886b5b337e7cba2923e3ad1224b806a91223ea39f1e0c77d03", - "sha256:5b4dfb6551eaeaf532054e2c6ef4b19c449c2e3a709ebdde6392acb1372ecabc", - "sha256:63f833a7c622e9082df3cbaf03b4fd92d7e0c11e2f9d87cb57dbf0e84441964b", - "sha256:71bf3b7ca15b1967bba3a1ef6a8e87286382a8b5e46ac76b42a02fe787c5237d", - "sha256:733dc5d47e71236263837825b69c975bc08728ae638452b34aeb1d6fa347b780", - "sha256:82f00a1e2695a0e5b89879aa25ea614530b8ebdca6d49d4834843d498e8a5e92", - "sha256:866bf72b9c3bfabe4476d866c70ee1714ad3e2f7b7048bb934892335e7b6b1f7", - "sha256:8aeac8b08f4b8c52129518efcd93706bb6d506ccd17830b67d18d0227cf32d9e", - "sha256:8d2cfb0aef7ec8759736cce26946efa084cdf49797712333539ef7d135e0295e", - "sha256:981224224bbf44d95278eb37996162e8beb6f144d2719b144e86dfe2fce6c510", - "sha256:981daff58fa3985a26daa4faa2b726c4e7a1d45178100125c0e1fdaf2ac64978", - "sha256:9ad36dbfdbb0cba90a08e7343fadf86f43cf6d87450e8d2b5d71d7c7202907e4", - "sha256:a251570bb3cb04f1627f23c234ad09af0e54fc8194e026cf46178f2e5748d647", - "sha256:b5ff7dae352fd9e1edddad1348698e9fea14064460a7e39121ef9526745802e6", - "sha256:c898f9cca806102fcacb6309899743aa39efb2ad2a302f4c319f54db9f05cd84", - "sha256:cf4b970042ce148ad8dce4369c02a4078b382dadf20067ce2629c239d76460d1", - "sha256:d1569013e8cc8f37e9769d19effdd85e404c976cd0ca28a94e3ddc026c216ae8", - "sha256:dca261e85fe0d34b2c242ecb31c9ab693509af2cf955d9caf01ee3ef3669abd0", - "sha256:ec8bf53ef7c92c99340972519adbe122e82c81d5b87cbd955c74ba8a8cd2a4ad", - "sha256:f2e55726a9ee2e8129d6ce6abb466304868051bcc7a09d652b3b07cd86e801a2", - "sha256:f4dee74f2626c783a3804df9191e9008946a104d5a284e52427a53ff576423cb", - "sha256:f592fd7fe1f20b5041928cce1330937eca62f9058cb41e69c2c2d83cffc0d1e3", - "sha256:ffab5b80bba8c86251291b8ce2e6c99a61446459d4c6637f5d5cc8c9ce37c972" - ], - "version": "==1.15.2" - }, - "pbkdf2": { - "hashes": [ - "sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979" - ], - "version": "==1.3" - }, - "requests": { - "hashes": [ - "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", - "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" - ], - "version": "==2.20.0" + "git": "https://github.com/kinecosystem/py-kin-base", + "ref": "f26ad2d2d612f66a3298b42aef43652b157624b4" }, "schematics": { "hashes": [ @@ -119,32 +27,6 @@ ], "index": "pypi", "version": "==2.0.1" - }, - "six": { - "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" - ], - "version": "==1.11.0" - }, - "stellar-base-sseclient": { - "hashes": [ - "sha256:2a500f3015dede4e9fac0f9d6d9d85f4fdd7fe1c9c10b2b111a6ae190cc5dc00" - ], - "version": "==0.0.21" - }, - "toml": { - "hashes": [ - "sha256:8e86bd6ce8cc11b9620cb637466453d94f5d57ad86f17e98a98d1f73e3baab2d" - ], - "version": "==0.9.4" - }, - "urllib3": { - "hashes": [ - "sha256:61bf29cada3fc2fbefad4fdf059ea4bd1b4a86d2b6d15e1c7c0b582b9752fe39", - "sha256:de9529817c93f27c8ccbfead6985011db27bd0ddfcdb2d86f3f663385c6a9c22" - ], - "version": "==1.24.1" } }, "develop": { @@ -158,10 +40,10 @@ }, "certifi": { "hashes": [ - "sha256:47f9c83ef4c0c621eaef743f133f09fa8a74a9b75f037e8624f83bd1b6626cb7", - "sha256:993f830721089fef441cdfeb4b2c8c9df86f0c63239f06bd025a76a7daddb033" + "sha256:59b7658e26ca9c7339e00f8f4636cdfe59d34fa37b9b04f6f9e9926b3cece1a5", + "sha256:b26104d6835d1f5e49452a26eb2ff87fe7090b89dfcaee5ea2212697e1e1d7ae" ], - "version": "==2018.11.29" + "version": "==2019.3.9" }, "chardet": { "hashes": [ @@ -227,10 +109,10 @@ }, "idna": { "hashes": [ - "sha256:156a6814fb5ac1fc6850fb002e0852d56c0c8d2531923a51032d1b70760e186e", - "sha256:684a38a6f903c1d71d6d5fac066b58d7768af4de2b832e426ec79c30daa94a16" + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" ], - "version": "==2.7" + "version": "==2.8" }, "pluggy": { "hashes": [ @@ -257,6 +139,14 @@ "index": "pypi", "version": "==3.4.0" }, + "pytest-asyncio": { + "hashes": [ + "sha256:9fac5100fd716cbecf6ef89233e8590a4ad61d729d1732e0a96b84182df1daaf", + "sha256:d734718e25cfc32d2bf78d346e99d33724deeba774cc4afdf491530c6184b63b" + ], + "index": "pypi", + "version": "==0.10.0" + }, "pytest-cov": { "hashes": [ "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", @@ -267,17 +157,17 @@ }, "requests": { "hashes": [ - "sha256:99dcfdaaeb17caf6e526f32b6a7b780461512ab3f1d992187801694cba42770c", - "sha256:a84b8c9ab6239b578f22d1c21d51b696dcfe004032bb80ea832398d6909d7279" + "sha256:502a824f31acdacb3a35b6690b5fbf0bc41d63a24a45c4004352b0242707598e", + "sha256:7bf2a778576d825600030a110f3c0e3e8edc51dfaafe1c146e39a2027784957b" ], - "version": "==2.20.0" + "version": "==2.21.0" }, "six": { "hashes": [ - "sha256:70e8a77beed4562e7f14fe23a786b54f6296e34344c23bc42f07b15018ff98e9", - "sha256:832dc0e10feb1aa2c68dcc57dbb658f1c7e65b9b61af69048abc87a2db00a0eb" + "sha256:3350809f0555b11f552448330d0b52d5f24c91a322ea4a15ef22629740f3761c", + "sha256:d16a0141ec1a18405cd4ce8b4613101da75da0e9a7aec5bdd4fa804d0e0eba73" ], - "version": "==1.11.0" + "version": "==1.12.0" }, "urllib3": { "hashes": [ diff --git a/kin/__init__.py b/kin/__init__.py index 505091f..80a9702 100644 --- a/kin/__init__.py +++ b/kin/__init__.py @@ -5,4 +5,7 @@ from .transactions import OperationTypes, decode_transaction from .blockchain.keypair import Keypair from .blockchain.environment import Environment -from .blockchain.builder import Builder + +# Override kin_base user agent with the kin-sdk user agent +from kin_base import horizon +horizon.USER_AGENT = config.SDK_USER_AGENT diff --git a/kin/account.py b/kin/account.py index 351b249..7db13bb 100644 --- a/kin/account.py +++ b/kin/account.py @@ -3,19 +3,20 @@ import re import json -from kin_base.transaction_envelope import TransactionEnvelope +from kin_base import Builder from kin_base.network import NETWORKS +from kin_base.transaction_envelope import TransactionEnvelope from .blockchain.keypair import Keypair -from .blockchain.horizon import Horizon -from .blockchain.builder import Builder from .blockchain.channel_manager import ChannelManager, ChannelStatuses from . import errors as KinErrors -from .transactions import build_memo +from .transactions import build_memo, RawTransaction, SimplifiedTransaction from .blockchain.errors import TransactionResultCode, HorizonErrorType, HorizonError -from .config import SDK_USER_AGENT, APP_ID_REGEX, KIN_DECIMAL_PRECISION +from .config import APP_ID_REGEX, KIN_DECIMAL_PRECISION from .blockchain.utils import is_valid_address, is_valid_secret_key +from .blockchain.horizon_models import AccountData +from typing import List, Optional, Union import logging logger = logging.getLogger(__name__) @@ -37,112 +38,100 @@ def __init__(self, seed, client, channel_seeds, app_id): # Set keypair self.keypair = Keypair(seed) - # check that sdk wallet account exists - if not self._client.does_account_exists(self.keypair.public_address): - raise KinErrors.AccountNotFoundError(self.keypair.public_address) if channel_seeds is not None: # Use given channels self.channel_seeds = channel_seeds + for channel_seed in self.channel_seeds: + if not is_valid_secret_key(channel_seed): + raise KinErrors.StellarSecretInvalidError else: # Use the base account as the only channel self.channel_seeds = [seed] - for channel_seed in self.channel_seeds: - if not is_valid_secret_key(channel_seed): - raise KinErrors.StellarSecretInvalidError - - # set connection pool size for channels + monitoring connection + extra - pool_size = max(1, len(self.channel_seeds)) + 2 - - # Set an horizon instance with the new pool_size - self.horizon = Horizon(self._client.environment.horizon_uri, - pool_size=pool_size, user_agent=SDK_USER_AGENT) self.channel_manager = ChannelManager(self.channel_seeds) def get_public_address(self): """Return this KinAccount's public address""" return self.keypair.public_address - def get_balance(self): + async def get_balance(self) -> float: """ Get the KIN balance of this KinAccount :return: the kin balance - :rtype: float :raises: KinErrors.AccountNotFoundError if the account does not exist. """ - return self._client.get_account_balance(self.keypair.public_address) + return await self._client.get_account_balance(self.keypair.public_address) - def get_data(self): + async def get_data(self) -> AccountData: """ Gets this KinAccount's data :return: account data - :rtype: kin.blockchain.horizon_models.AccountData :raises: KinErrors.AccountNotFoundError if the account does not exist. """ - return self._client.get_account_data(self.keypair.public_address) + return await self._client.get_account_data(self.keypair.public_address) - def get_status(self, verbose=False): + async def get_status(self, verbose: Optional[bool] = False) -> dict: """ Get the config and status of this KinAccount object - :param bool verbose: Should the channels status be verbose + :param verbose: Should the channels status be verbose :return: The config and status of this KinAccount object :rtype dict """ account_status = { 'app_id': self.app_id, 'public_address': self.get_public_address(), - 'balance': self.get_balance(), - 'channels': self.channel_manager.get_status(verbose) + 'balance': await self.get_balance(), + 'channels': self.channel_manager.get_status(verbose) #TODO: await? } total_status = { - 'client': self._client.get_config(), + 'client': await self._client.get_config(), 'account': account_status } return total_status - def get_transaction_history(self, amount=10, descending=True, cursor=None, simple=True): + async def get_transaction_history(self, amount: Optional[int] = 10, descending: Optional[bool] = True, + cursor: Optional[int, None] = None, + simple: Optional[bool] = True) -> List[Union[SimplifiedTransaction, RawTransaction]]: """ Get the transaction history for this kin account - :param int amount: The maximum number of transactions to get - :param bool descending: The order of the transactions, True will start from the latest one - :param int cursor: The horizon paging token - :param bool simple: Should the returned txs be simplified, if True, complicated txs will be ignored + :param amount: The maximum number of transactions to get + :param descending: The order of the transactions, True will start from the latest one + :param cursor: The horizon paging token + :param simple: Should the returned txs be simplified, if True, complicated txs will be ignored :return: A list of transactions - :rtype: list """ - return self._client.get_account_tx_history(self.get_public_address(), - amount=amount, - descending=descending, - cursor=cursor, - simple=simple) + return await self._client.get_account_tx_history(self.get_public_address(), + amount=amount, + descending=descending, + cursor=cursor, + simple=simple) - def get_transaction_builder(self, fee): + def get_transaction_builder(self, fee: int) -> Builder: """ Get a transaction builder using this account - :param int fee: The fee that will be used for the transaction - :return: kin.Builder + :param fee: The fee that will be used for the transaction """ - return Builder(self._client.environment.name, self.horizon, fee, self.keypair.secret_seed) + return Builder(horizon=self._client.horizon, + network_name=self._client.environment.name, + fee=fee, + secret=self.keypair.secret_seed) - def create_account(self, address, starting_balance, fee, memo_text=None): + async def create_account(self, address: str, starting_balance: Union[float, str], fee: int, + memo_text: Optional[str] = None) -> str: """Create an account identified by the provided address. - :param str address: the address of the account to create. - - :param float|str starting_balance: the starting KIN balance of the account. - - :param str memo_text: (optional) a text to put into transaction memo, up to MEMO_CAP chars. - - :param int fee: fee to be deducted for the tx + :param address: the address of the account to create. + :param starting_balance: the starting KIN balance of the account. + :param memo_text: (optional) a text to put into transaction memo, up to MEMO_CAP chars. + :param fee: fee to be deducted for the tx :return: the hash of the transaction - :rtype: str :raises: KinErrors.StellarAddressInvalidError: if the provided address has a wrong format. :raises: KinErrors.AccountExistsError if the account already exists. @@ -152,27 +141,24 @@ def create_account(self, address, starting_balance, fee, memo_text=None): """ builder = self.build_create_account(address, starting_balance, fee, memo_text) - with self.channel_manager.get_channel() as channel: - builder.set_channel(channel) + with self.channel_manager.get_channel() as channel: # TODO: async with? + await builder.set_channel(channel) builder.sign(channel) # Also sign with the root account if a different channel was used if builder.address != self.keypair.public_address: builder.sign(self.keypair.secret_seed) - return self.submit_transaction(builder) + return await self.submit_transaction(builder) - def send_kin(self, address, amount, fee, memo_text=None): + async def send_kin(self, address: str, amount: Union[float, str], fee: int, + memo_text: Optional[str] = None) -> str: """Send KIN to the account identified by the provided address. - :param str address: the account to send KIN to. - - :param float|str amount: the amount of KIN to send. - - :param str memo_text: (optional) a text to put into transaction memo. - - :param int fee: fee to be deducted + :param address: the account to send KIN to. + :param amount: the amount of KIN to send. + :param memo_text: (optional) a text to put into transaction memo. + :param fee: fee to be deducted :return: the hash of the transaction - :rtype: str :raises: KinErrors.StellarAddressInvalidError: if the provided address has a wrong format. :raises: ValueError: if the amount is not positive. @@ -184,26 +170,22 @@ def send_kin(self, address, amount, fee, memo_text=None): """ builder = self.build_send_kin(address, amount, fee, memo_text) with self.channel_manager.get_channel() as channel: - builder.set_channel(channel) + await builder.set_channel(channel) builder.sign(channel) # Also sign with the root account if a different channel was used if builder.address != self.keypair.public_address: builder.sign(self.keypair.secret_seed) - return self.submit_transaction(builder) + return await self.submit_transaction(builder) - def build_create_account(self, address, starting_balance, fee, memo_text=None): + def build_create_account(self, address: str, starting_balance: Union[float, str], fee: int, + memo_text: Optional[str] = None) -> Builder: """Build a tx that will create an account identified by the provided address. - - :param str address: the address of the account to create. - - :param float|str starting_balance: the starting XLM balance of the account. - - :param str memo_text: (optional) a text to put into transaction memo, up to MEMO_CAP chars. - - :param int fee: fee to be deducted for the tx + :param address: the address of the account to create. + :param starting_balance: the starting XLM balance of the account. + :param memo_text: (optional) a text to put into transaction memo, up to MEMO_CAP chars. + :param fee: fee to be deducted for the tx :return: a transaction builder object - :rtype: kin.Builder :raises: KinErrors.StellarAddressInvalidError: if the supplied address has a wrong format. """ @@ -219,19 +201,15 @@ def build_create_account(self, address, starting_balance, fee, memo_text=None): builder.append_create_account_op(address, str(starting_balance), source=self.keypair.public_address) return builder - def build_send_kin(self, address, amount, fee, memo_text=None): + def build_send_kin(self, address: str, amount: Union[float, str], fee: int, + memo_text: Optional[str] = None) -> Builder: """Build a tx to send KIN to the account identified by the provided address. - - :param str address: the account to send asset to. - - :param float|str amount: the KIN amount to send. - - :param str memo_text: (optional) a text to put into transaction memo. - - :param int fee: fee to be deducted for the tx + :param address: the account to send asset to. + :param amount: the KIN amount to send. + :param memo_text: (optional) a text to put into transaction memo. + :param fee: fee to be deducted for the tx :return: a transaction builder - :rtype: kin.Builder :raises: KinErrors.StellarAddressInvalidError: if the provided address has a wrong format. :raises: ValueError: if the amount is not positive. @@ -248,7 +226,7 @@ def build_send_kin(self, address, amount, fee, memo_text=None): builder.append_payment_op(address, str(amount), source=self.keypair.public_address) return builder - def submit_transaction(self, tx_builder): + async def submit_transaction(self, tx_builder) -> str: """ Submit a transaction to the blockchain. :param kin.Builder tx_builder: The transaction builder @@ -256,7 +234,7 @@ def submit_transaction(self, tx_builder): :rtype: str """ try: - return tx_builder.submit()['hash'] + return (await tx_builder.submit())['hash'] # If the channel is out of KIN, top it up and try again except HorizonError as e: logger.warning('send transaction error with channel {}: {}'.format(tx_builder.address, str(e))) diff --git a/kin/blockchain/builder.py b/kin/blockchain/builder.py deleted file mode 100644 index 1d223ab..0000000 --- a/kin/blockchain/builder.py +++ /dev/null @@ -1,67 +0,0 @@ -"""Contains the builder class to build transactions""" - -from kin_base.builder import Builder as BaseBuilder -from kin_base.keypair import Keypair -from kin_base.memo import NoneMemo - - -class Builder(BaseBuilder): - """ - This class overrides :class:`kin_base.builder` to provide additional functionality. - # TODO: maybe merge this with kin-base builder - """ - - # TODO: make seed optional (need to change kin_base) - def __init__(self, network_name, horizon, fee, secret): - """ - Create a new transaction builder - :param str network_name: The name of the network - :param kin.Horizon horizon: The horizon instance to use - :param int fee: Fee for the transaction - :param str secret: The seed to be used - """ - - # call base class constructor to init base class variables - # sequence is one since it get overridden later - super(Builder, self).__init__(secret=secret, sequence=1, fee=fee) - - # custom overrides - - self.network = network_name - self.horizon = horizon - - def clear(self): - """"Clears the builder so it can be reused.""" - self.ops = [] - self.time_bounds = None - self.memo = NoneMemo() - self.tx = None - self.te = None - - def update_sequence(self): - """ - Update the builder with the *current* sequence of the account - # TODO: kin-base builder increments this value by 1 when building a tx. - # Remove this functionality from py-stellar-base and change this method set the current sequence+1 - """ - - # TODO: kin-base checks for 'not sequence' to find if there is no sequence, therefore - # Sequence of 0 fails, write it as a str for now and fix in kin-base later - self.sequence = str(self.get_sequence()) - - def next(self): - """ - Alternative implementation that does not create a new builder but clears the current one and increments - the account sequence number. - """ - self.clear() - self.sequence = str(int(self.sequence) + 1) - - def set_channel(self, channel_seed): - """ - Set a channel to be used for this transaction - :param str channel_seed: Seed to use as the channel - """ - self.keypair = Keypair.from_seed(channel_seed) - self.address = self.keypair.address().decode() - self.update_sequence() diff --git a/kin/blockchain/errors.py b/kin/blockchain/errors.py index ce1e985..a564a81 100644 --- a/kin/blockchain/errors.py +++ b/kin/blockchain/errors.py @@ -39,15 +39,6 @@ class ChannelsFullError(Exception): """ - -class HorizonError(HTTPProblemDetails, Exception): - def __init__(self, err_dict): - super(HTTPProblemDetails, self).__init__(err_dict, strict=False) - super(Exception, self).__init__(self.title) - if len(self.type) > len(HORIZON_NS_PREFIX): - self.type = self.type[len(HORIZON_NS_PREFIX):] - - # noinspection PyClassHasNoInit class HorizonErrorType: BAD_REQUEST = 'bad_request' # cannot understand the request due to invalid parameters diff --git a/kin/blockchain/horizon.py b/kin/blockchain/horizon.py deleted file mode 100644 index 392acb2..0000000 --- a/kin/blockchain/horizon.py +++ /dev/null @@ -1,248 +0,0 @@ -"""Contains the Horizon class to interact with horizon""" - -import requests -from requests.adapters import HTTPAdapter, DEFAULT_POOLSIZE -from requests.exceptions import RequestException -import sys -from time import sleep -from urllib3.util import Retry - -from kin_base.horizon import HORIZON_LIVE, HORIZON_TEST - -from .errors import HorizonError - -import logging - -logger = logging.getLogger(__name__) - -try: - from sseclient import SSEClient -except ImportError: - SSEClient = None - -if sys.version[0] == '2': - # noinspection PyUnresolvedReferences - from urllib import urlencode -else: - # noinspection PyUnresolvedReferences - from urllib.parse import urlencode - -DEFAULT_REQUEST_TIMEOUT = 11 # two ledgers + 1 sec, let's retry faster and not wait 60 secs. -DEFAULT_NUM_RETRIES = 5 -DEFAULT_BACKOFF_FACTOR = 0.5 -USER_AGENT = 'py-stellar-base' - - -class Horizon(object): - """ - This class redefines :class:`kin_base.horizon.Horizon` to provide additional functionality: - - persistent connection to Horizon and connection pool - - configurable request retry functionality - - Horizon error checking and deserialization - """ - - def __init__(self, horizon_uri=None, pool_size=DEFAULT_POOLSIZE, num_retries=DEFAULT_NUM_RETRIES, - request_timeout=DEFAULT_REQUEST_TIMEOUT, backoff_factor=DEFAULT_BACKOFF_FACTOR, user_agent=USER_AGENT): - if horizon_uri is None: - self.horizon_uri = HORIZON_TEST - else: - self.horizon_uri = horizon_uri - - self.pool_size = pool_size - self.num_retries = num_retries - self.request_timeout = request_timeout - self.backoff_factor = backoff_factor - - # adding 504 to the list of statuses to retry - self.status_forcelist = list(Retry.RETRY_AFTER_STATUS_CODES) - self.status_forcelist.append(504) - - # configure standard session - - # configure retry handler - retry = Retry(total=self.num_retries, backoff_factor=self.backoff_factor, redirect=0, - status_forcelist=self.status_forcelist) - # init transport adapter - adapter = HTTPAdapter(pool_connections=self.pool_size, pool_maxsize=self.pool_size, max_retries=retry) - - # init session - session = requests.Session() - - # set default headers - session.headers.update({'User-Agent': user_agent}) - - session.mount('http://', adapter) - session.mount('https://', adapter) - self._session = session - - # configure SSE session (differs from our standard session) - - sse_retry = Retry(total=1000000, redirect=0, status_forcelist=self.status_forcelist) - sse_adapter = HTTPAdapter(pool_connections=self.pool_size, pool_maxsize=self.pool_size, max_retries=sse_retry) - sse_session = requests.Session() - sse_session.headers.update({'User-Agent': user_agent}) - sse_session.mount('http://', sse_adapter) - sse_session.mount('https://', sse_adapter) - self._sse_session = sse_session - - def submit(self, te): - """Submit the transaction using a pooled connection, and retry on failure.""" - params = {'tx': te} - url = self.horizon_uri + '/transactions/' - - # POST is not included in Retry's method_whitelist for a good reason. - # our custom retry mechanism follows - reply = None - retry_count = self.num_retries - while True: - try: - reply = self._session.post(url, data=params, timeout=self.request_timeout) - return check_horizon_reply(reply.json()) - except (RequestException, ValueError) as e: - if reply: - msg = 'horizon submit exception: {}, reply: [{}] {}'.format(str(e), reply.status_code, reply.text) - else: - msg = 'horizon submit exception: {}'.format(str(e)) - logging.warning(msg) - - if reply and reply.status_code not in self.status_forcelist: - raise Exception('invalid horizon reply: [{}] {}'.format(reply.status_code, reply.text)) - # retry - if retry_count <= 0: - raise - retry_count -= 1 - logging.warning('submit retry attempt {}'.format(retry_count)) - sleep(self.backoff_factor) - - def query(self, rel_url, params=None, sse=False): - abs_url = self.horizon_uri + rel_url - reply = self._query(abs_url, params, sse) - return check_horizon_reply(reply) if not sse else reply - - def account(self, address): - url = '/accounts/' + address - return self.query(url) - - def account_effects(self, address, params=None, sse=False): - url = '/accounts/' + address + '/effects/' - return self.query(url, params, sse) - - def account_offers(self, address, params=None): - url = '/accounts/' + address + '/offers/' - return self.query(url, params) - - def account_operations(self, address, params=None, sse=False): - url = '/accounts/' + address + '/operations/' - return self.query(url, params, sse) - - def account_transactions(self, address, params=None, sse=False): - url = '/accounts/' + address + '/transactions/' - return self.query(url, params, sse) - - def account_payments(self, address, params=None, sse=False): - url = '/accounts/' + address + '/payments/' - return self.query(url, params, sse) - - def transactions(self, params=None, sse=False): - url = '/transactions/' - return self.query(url, params, sse) - - def transaction(self, tx_hash): - url = '/transactions/' + tx_hash - return self.query(url) - - def transaction_operations(self, tx_hash, params=None): - url = '/transactions/' + tx_hash + '/operations/' - return self.query(url, params) - - def transaction_effects(self, tx_hash, params=None): - url = '/transactions/' + tx_hash + '/effects/' - return self.query(url, params) - - def transaction_payments(self, tx_hash, params=None): - url = '/transactions/' + tx_hash + '/payments/' - return self.query(url, params) - - def order_book(self, params=None): - url = '/order_book/' - return self.query(url, params) - - def trades(self, params=None): - url = '/trades/' - return self.query(url, params) - - def ledgers(self, params=None, sse=False): - url = '/ledgers/' - return self.query(url, params, sse) - - def ledger(self, ledger_id): - url = '/ledgers/' + str(ledger_id) - return self.query(url) - - def ledger_transactions(self, ledger_id, params=None): - url = '/ledgers/' + str(ledger_id) + '/transactions/' - return self.query(url, params) - - def ledger_effects(self, ledger_id, params=None): - url = '/ledgers/' + str(ledger_id) + '/effects/' - return self.query(url, params) - - def ledger_operations(self, ledger_id, params=None): - url = '/ledgers/' + str(ledger_id) + '/operations/' - return self.query(url, params) - - def ledger_payments(self, ledger_id, params=None): - url = '/ledgers/' + str(ledger_id) + '/payments/' - return self.query(url, params) - - def effects(self, params=None, sse=False): - url = '/effects/' - return self.query(url, params, sse) - - def operations(self, params=None, sse=False): - url = '/operations/' - return self.query(url, params, sse) - - def operation(self, op_id, params=None): - url = '/operations/' + str(op_id) - return self.query(url, params) - - def operation_effects(self, op_id, params=None): - url = '/operations/' + str(op_id) + '/effects/' - return self.query(url, params) - - def payments(self, params=None, sse=False): - url = '/payments/' - return self.query(url, params, sse) - - def assets(self, params=None): - url = '/assets/' - return self.query(url, params) - - def _query(self, url, params=None, sse=False): - if not sse: - reply = self._session.get(url, params=params, timeout=self.request_timeout) - try: - return reply.json() - except ValueError: - raise Exception('invalid horizon reply: [{}] {}'.format(reply.status_code, reply.text)) - - # SSE connection - if SSEClient is None: - raise ValueError('SSE not supported, missing sseclient module') - - return SSEClient(url, session=self._sse_session, params=params) - - @staticmethod - def testnet(): - return Horizon(horizon_uri=HORIZON_TEST) - - @staticmethod - def livenet(): - return Horizon(horizon_uri=HORIZON_LIVE) - - -def check_horizon_reply(reply): - if 'status' not in reply: - return reply - raise HorizonError(reply) diff --git a/kin/client.py b/kin/client.py index cd312dc..d7db05a 100644 --- a/kin/client.py +++ b/kin/client.py @@ -1,31 +1,33 @@ """Contains the KinClient class to interact with the blockchain""" -import requests +from kin_base import Horizon -from .config import SDK_USER_AGENT, ANON_APP_ID, MAX_RECORDS_PER_REQUEST +from .config import ANON_APP_ID, MAX_RECORDS_PER_REQUEST from . import errors as KinErrors -from .blockchain.horizon import Horizon from .monitors import SingleMonitor, MultiMonitor -from .transactions import OperationTypes, SimplifiedTransaction, RawTransaction, build_memo +from .transactions import SimplifiedTransaction, RawTransaction from .account import KinAccount from .blockchain.horizon_models import AccountData from .blockchain.utils import is_valid_address, is_valid_transaction_hash from .version import __version__ +from .blockchain.environment import Environment + +from typing import List, Optional, Union import logging logger = logging.getLogger(__name__) -class KinClient(object): +class KinClient: """ The :class:`kin.KinClient` class is the primary interface to the KIN Python SDK based on Kin Blockchain. It maintains a connection context with a Horizon node and hides all the specifics of dealing with Kin REST API. """ - def __init__(self, environment): + def __init__(self, environment: Environment): """Create a new instance of the KinClient to query the Kin blockchain. - :param kin.Environment environment: an environment for the client to point to. + :param environment: an environment for the client to point to. :return: An instance of the KinClient. :rtype: KinErrors.KinClient @@ -34,25 +36,24 @@ def __init__(self, environment): self.environment = environment self.network = environment.name - self.horizon = Horizon(horizon_uri=environment.horizon_uri, user_agent=SDK_USER_AGENT) - logger.info('Kin SDK inited on network {}, horizon endpoint {}'.format(self.network, self.horizon.horizon_uri)) + self.horizon = Horizon(environment.horizon_uri) + logger.info('Kin Client initialized on network {}, horizon endpoint {}'. + format(self.network, self.horizon.horizon_uri)) - def kin_account(self, seed, channel_secret_keys=None, app_id=ANON_APP_ID): + def kin_account(self, seed: str, channel_secret_keys: Optional[List[str]] = None, + app_id: Optional[str] = ANON_APP_ID) -> KinAccount: """ Create a new instance of a KinAccount to perform authenticated operations on the blockchain. - :param str seed: The secret seed of the account that will be used - :param list[str] channel_secret_keys: A list of seeds to be used as channels - :param str app_id: the unique id of your app + :param seed: The secret seed of the account that will be used + :param channel_secret_keys: A list of seeds to be used as channels + :param app_id: the unique id of your app :return: An instance of KinAccount - :rtype: kin.KinAccount - - :raises: KinErrors.AccountNotFoundError if SDK wallet or channel account is not yet created. """ # Create a new kin account, using self as the KinClient to be used - return KinAccount(seed, self,channel_secret_keys, app_id) + return KinAccount(seed, self, channel_secret_keys, app_id) - def get_config(self): + async def get_config(self) -> dict: """Get system configuration data and online status. :return: a dictionary containing the data :rtype: dict @@ -66,39 +67,36 @@ def get_config(self): 'error': None, }, 'transport': { - 'pool_size': self.horizon.pool_size, + 'pool_size': self.horizon._session.connector.limit, 'num_retries': self.horizon.num_retries, - 'request_timeout': self.horizon.request_timeout, - 'retry_statuses': self.horizon.status_forcelist, + 'request_timeout': self.horizon._session._timeout.total, 'backoff_factor': self.horizon.backoff_factor, } } # now check Horizon connection try: - self.horizon.query('') + await self.horizon.metrics() status['horizon']['online'] = True except Exception as e: status['horizon']['error'] = str(e) return status - def get_minimum_fee(self): + async def get_minimum_fee(self) -> int: """ Get the current minimum fee acceptable for a tx :return: The minimum fee - :type: int """ params = {'order': 'desc', 'limit': 1} - return self.horizon.ledgers(params=params)['_embedded']['records'][0]['base_fee_in_stroops'] + return (await self.horizon.ledgers(order='desc', limit=1))['_embedded']['records'][0]['base_fee_in_stroops'] - def get_account_balance(self, address): + async def get_account_balance(self, address: str) -> float: """ Get the KIN balance of a given account - :param str address: the public address of the account to query + :param address: the public address of the account to query :return: the balance of the account - :rtype: float :raises: StellarAddressInvalidError: if the provided address has the wrong format. :raises: KinErrors.AccountNotFoundError if the account does not exist. @@ -106,18 +104,17 @@ def get_account_balance(self, address): if not is_valid_address(address): raise KinErrors.StellarAddressInvalidError('invalid address: {}'.format(address)) - account_data = self.get_account_data(address) + account_data = await self.get_account_data(address) for balance in account_data.balances: # accounts will always have native asset if balance.asset_type == 'native': return balance.balance - def does_account_exists(self, address): + async def does_account_exists(self, address: str) -> bool: """ Find out if a given account exists on the blockchain - :param str address: The kin account to query about + :param address: The kin account to query about :return: does the account exists on the blockchain - :rtype boolean :raises: KinErrors.StellarAddressInvalidError if the address is not valid. """ @@ -126,18 +123,17 @@ def does_account_exists(self, address): raise KinErrors.StellarAddressInvalidError('invalid address: {}'.format(address)) try: - self.get_account_balance(address) + await self.get_account_balance(address) return True except KinErrors.AccountNotFoundError: return False - def get_account_data(self, address): + async def get_account_data(self, address: str) -> AccountData: """Get account data. - :param str address: the public address of the account to query. + :param address: the public address of the account to query. :return: account data - :rtype: kin.blockchain.horizon_models.AccountData :raises: StellarAddressInvalidError: if the provided address has a wrong format. :raises: :class:`KinErrors.AccountNotFoundError`: if the account does not exist. @@ -147,21 +143,20 @@ def get_account_data(self, address): raise KinErrors.StellarAddressInvalidError('invalid address: {}'.format(address)) try: - acc = self.horizon.account(address) + acc = await self.horizon.account(address) return AccountData(acc, strict=False) except Exception as e: err = KinErrors.translate_error(e) raise KinErrors.AccountNotFoundError(address) if \ isinstance(err, KinErrors.ResourceNotFoundError) else err - def get_transaction_data(self, tx_hash, simple=True): + async def get_transaction_data(self, tx_hash: str, simple: Optional[bool] = True) -> Union[SimplifiedTransaction, RawTransaction]: """Gets transaction data. - :param str tx_hash: transaction hash. - :param boolean simple: (optional) returns a simplified transaction object + :param tx_hash: transaction hash. + :param simple: (optional) Should the method return a simplified or raw transaction :return: transaction data - :rtype: kin.transactions.RawTransaction | kin.transactions.SimplifiedTransaction :raises: ValueError: if the provided hash is invalid. :raises: :class:`KinErrors.ResourceNotFoundError`: if the transaction does not exist. @@ -172,7 +167,7 @@ def get_transaction_data(self, tx_hash, simple=True): raise ValueError('invalid transaction hash: {}'.format(tx_hash)) try: - raw_tx = RawTransaction(self.horizon.transaction(tx_hash)) + raw_tx = RawTransaction(await self.horizon.transaction(tx_hash)) except Exception as e: raise KinErrors.translate_error(e) @@ -180,16 +175,17 @@ def get_transaction_data(self, tx_hash, simple=True): return SimplifiedTransaction(raw_tx) return raw_tx - def get_account_tx_history(self, address, amount=10, descending=True, cursor=None, simple=True): + async def get_account_tx_history(self, address: str, amount: Optional[int] = 10, descending: Optional[bool] = True, + cursor: Optional[int, None] = None, + simple: Optional[bool] = True) -> List[Union[SimplifiedTransaction, RawTransaction]]: """ Get the transaction history for a given account. - :param str address: The public address of the account to query - :param int amount: The maximum number of transactions to get - :param bool descending: The order of the transactions, True will start from the latest one - :param int cursor: The horizon paging token - :param bool simple: Should the returned txs be simplified, if True, complicated txs will be ignored + :param address: The public address of the account to query + :param amount: The maximum number of transactions to get + :param descending: The order of the transactions, True will start from the latest one + :param cursor: The horizon paging token + :param simple: Should the returned txs be simplified, if True, complicated txs will be ignored :return: A list of transactions - :rtype: list[kin.transactions.RawTransaction | kin.transactions.SimplifiedTransaction] """ if not is_valid_address(address): @@ -201,16 +197,10 @@ def get_account_tx_history(self, address, amount=10, descending=True, cursor=Non tx_list = [] requested_amount = amount if amount < MAX_RECORDS_PER_REQUEST else MAX_RECORDS_PER_REQUEST - params = { - 'limit': requested_amount, - 'order': 'desc' if descending else 'asc' - } - - # cursor is optional - if cursor is not None: - params['cursor'] = cursor - horizon_response = self.horizon.account_transactions(address, params) + horizon_response = await self.horizon.account_transactions(address, + cursor=cursor, limit=requested_amount, + order='desc' if descending else 'asc') for transaction in horizon_response['_embedded']['records']: raw_tx = RawTransaction(transaction) @@ -229,44 +219,14 @@ def get_account_tx_history(self, address, amount=10, descending=True, cursor=Non if remaining_txs <= 0 or len(horizon_response['_embedded']['records']) < amount: return tx_list # If there are anymore transactions, recursively get the next transaction page - return tx_list.extend(self.get_account_tx_history(address, remaining_txs, descending, last_cursor, simple)) - - def verify_kin_payment(self, tx_hash, source, destination, amount, memo=None, check_memo=False, app_id=ANON_APP_ID): - """ - Verify that a give tx matches the desired parameters - :param str tx_hash: The hash of the transaction to query - :param str source: The expected source account - :param str destination: The expected destination account - :param float amount: The expected amount - :param str memo: (optional) The expected memo - :param boolean check_memo: (optional) Should the memo match - :param str app_id: the id of the app that sent the tx - :return: True/False - :rtype: boolean - """ - - try: - tx = self.get_transaction_data(tx_hash) - operation = tx.operation - if operation.type != OperationTypes.PAYMENT: - return False - if source != tx.source or destination != operation.destination or amount != operation.amount: - return False - if check_memo and build_memo(app_id, memo) != tx.memo: - return False - - return True - - except KinErrors.CantSimplifyError: - return False + return tx_list.extend(await self.get_account_tx_history(address, remaining_txs, descending, last_cursor, simple)) - def friendbot(self, address): + async def friendbot(self, address: str): """ Use the friendbot service to create and fund an account - :param str address: The address to create and fund + :param address: The address to create and fund :return: the hash of the friendbot transaction - :rtype str :raises ValueError: if no friendbot service was provided :raises ValueError: if the address is invalid @@ -282,12 +242,13 @@ def friendbot(self, address): if self.does_account_exists(address): raise KinErrors.AccountExistsError(address) - response = requests.get(self.environment.friendbot_url, params={'addr': address}) - if response.ok: - return response.json()['hash'] + response = await self.horizon._session.get(self.environment.friendbot_url, params={'addr': address}) + if response.status == 200: + return (await response.json(encoding='utf-8'))['hash'] else: - raise KinErrors.FriendbotError(response.status_code, response.text) + raise KinErrors.FriendbotError(response.status, await (response.text(encoding='utf-8'))) + # TODO: asyncify def monitor_account_payments(self, address, callback_fn): """Monitor KIN payment transactions related to the account identified by provided address. NOTE: the function starts a background thread. diff --git a/kin/version.py b/kin/version.py index 3a5935a..3d67cd6 100644 --- a/kin/version.py +++ b/kin/version.py @@ -1 +1 @@ -__version__ = "2.3.1" +__version__ = "2.4.0" diff --git a/requirements-dev.txt b/requirements-dev.txt index 09ed9a7..77be61b 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -6,3 +6,4 @@ pluggy==0.6.0 py==1.5.2 pytest-cov==2.5.1 pytest==3.4.0 +pytest-asyncio==0.10.0 diff --git a/requirements.txt b/requirements.txt index 1b2881e..34f670b 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -kin-base==1.0.7 +kin-base==1.1.0 schematics==2.0.1 diff --git a/setup.py b/setup.py index 305bf9c..961e4e2 100644 --- a/setup.py +++ b/setup.py @@ -31,5 +31,5 @@ ], install_requires=requires, tests_require=tests_requires, - python_requires='>=3.4', + python_requires='>=3.5.3', ) From 1f283af18c8aee9dbd75297756d717cca77aa0a4 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Thu, 14 Mar 2019 15:39:17 +0200 Subject: [PATCH 02/22] Allign errors with new kin-base HorizonError --- kin/errors.py | 42 ++++++++++------------- test/test_errors.py | 84 ++++++++++++++++++++++----------------------- 2 files changed, 60 insertions(+), 66 deletions(-) diff --git a/kin/errors.py b/kin/errors.py index 51e4884..83179a3 100644 --- a/kin/errors.py +++ b/kin/errors.py @@ -1,10 +1,12 @@ """Contains errors related to the Kin SDK""" -from requests.exceptions import RequestException +from aiohttp.client_exceptions import ClientError -from .blockchain.errors import * +from kin_base.exceptions import HorizonError from kin_base.exceptions import NotValidParamError, StellarAddressInvalidError, StellarSecretInvalidError +from .blockchain.errors import * + # All exceptions should subclass from SdkError in this module. class SdkError(Exception): @@ -127,13 +129,6 @@ def __init__(self, error_code=None, extra=None): super(CantSimplifyError, self).__init__('Tx simplification error', error_code, extra) -class StoppedMonitorError(SdkError): - """A stopped monitor cannot be modified""" - - def __init__(self, error_code=None, extra=None): - super(StoppedMonitorError, self).__init__('Stopped monitor cannot be modified', error_code, extra) - - class WrongNetworkError(SdkError): """The account is not using the network specified in the tx""" @@ -144,7 +139,7 @@ def __init__(self, error_code=None, extra=None): def translate_error(err): """A high-level error translator.""" - if isinstance(err, RequestException): + if isinstance(err, ClientError): return NetworkError({'internal_error': str(err)}) if isinstance(err, ChannelsBusyError): return ThrottleError @@ -188,7 +183,7 @@ def translate_horizon_error(horizon_error): def translate_transaction_error(tx_error): """Transaction error translator.""" - tx_result_code = tx_error.extras.result_codes.transaction + tx_result_code = tx_error.extras['result_codes']['transaction'] if tx_result_code in [TransactionResultCode.TOO_EARLY, TransactionResultCode.TOO_LATE, TransactionResultCode.MISSING_OPERATION, @@ -202,7 +197,7 @@ def translate_transaction_error(tx_error): if tx_result_code == TransactionResultCode.INSUFFICIENT_BALANCE: return LowBalanceError(tx_result_code) if tx_result_code == TransactionResultCode.FAILED: - return translate_operation_error(tx_error.extras.result_codes.operations) + return translate_operation_error(tx_error.extras['result_codes']['operations']) return InternalError(tx_result_code, {'internal_error': 'unknown transaction error'}) @@ -213,22 +208,21 @@ def translate_operation_error(op_result_codes): if code != OperationResultCode.SUCCESS: op_result_code = code break - if op_result_code == OperationResultCode.BAD_AUTH \ - or op_result_code == CreateAccountResultCode.MALFORMED \ - or op_result_code == PaymentResultCode.NO_ISSUER \ - or op_result_code == PaymentResultCode.LINE_FULL \ - or op_result_code == ChangeTrustResultCode.INVALID_LIMIT: + if op_result_code in [OperationResultCode.BAD_AUTH, + CreateAccountResultCode.MALFORMED, + PaymentResultCode.NO_ISSUER, + PaymentResultCode.LINE_FULL, + ChangeTrustResultCode.INVALID_LIMIT]: return RequestError(op_result_code) - if op_result_code == OperationResultCode.NO_ACCOUNT or op_result_code == PaymentResultCode.NO_DESTINATION: + if op_result_code in [OperationResultCode.NO_ACCOUNT, PaymentResultCode.NO_DESTINATION]: return AccountNotFoundError(error_code=op_result_code) if op_result_code == CreateAccountResultCode.ACCOUNT_EXISTS: return AccountExistsError(error_code=op_result_code) - if op_result_code == CreateAccountResultCode.LOW_RESERVE \ - or op_result_code == PaymentResultCode.UNDERFUNDED: + if op_result_code in [CreateAccountResultCode.LOW_RESERVE, PaymentResultCode.UNDERFUNDED]: return LowBalanceError(op_result_code) - if op_result_code == PaymentResultCode.SRC_NO_TRUST \ - or op_result_code == PaymentResultCode.NO_TRUST \ - or op_result_code == PaymentResultCode.SRC_NOT_AUTHORIZED \ - or op_result_code == PaymentResultCode.NOT_AUTHORIZED: + if op_result_code in [PaymentResultCode.SRC_NO_TRUST, + PaymentResultCode.NO_TRUST, + PaymentResultCode.SRC_NOT_AUTHORIZED, + PaymentResultCode.NOT_AUTHORIZED]: return AccountNotActivatedError(error_code=op_result_code) return InternalError(op_result_code, {'internal_error': 'unknown operation error'}) diff --git a/test/test_errors.py b/test/test_errors.py index 3bc69a6..cd0db11 100644 --- a/test/test_errors.py +++ b/test/test_errors.py @@ -27,55 +27,55 @@ def test_translate_horizon_error(): fixtures = [ # RequestError - [HorizonErrorType.BAD_REQUEST, KinErrors.RequestError, 'bad request', {}], - [HorizonErrorType.FORBIDDEN, KinErrors.RequestError, 'bad request', {}], - [HorizonErrorType.NOT_ACCEPTABLE, KinErrors.RequestError, 'bad request', {}], - [HorizonErrorType.UNSUPPORTED_MEDIA_TYPE, KinErrors.RequestError, 'bad request', {}], - [HorizonErrorType.NOT_IMPLEMENTED, KinErrors.RequestError, 'bad request', {}], - [HorizonErrorType.BEFORE_HISTORY, KinErrors.RequestError, 'bad request', {}], - [HorizonErrorType.STALE_HISTORY, KinErrors.RequestError, 'bad request', {}], - [HorizonErrorType.TRANSACTION_MALFORMED, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.BAD_REQUEST, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.FORBIDDEN, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.NOT_ACCEPTABLE, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.UNSUPPORTED_MEDIA_TYPE, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.NOT_IMPLEMENTED, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.BEFORE_HISTORY, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.STALE_HISTORY, KinErrors.RequestError, 'bad request', {}], + [KinErrors.HorizonErrorType.TRANSACTION_MALFORMED, KinErrors.RequestError, 'bad request', {}], # ResourceNotFoundError - [HorizonErrorType.NOT_FOUND, KinErrors.ResourceNotFoundError, 'resource not found', {}], + [KinErrors.HorizonErrorType.NOT_FOUND, KinErrors.ResourceNotFoundError, 'resource not found', {}], # ServerError - [HorizonErrorType.RATE_LIMIT_EXCEEDED, KinErrors.ServerError, 'server error', {}], - [HorizonErrorType.SERVER_OVER_CAPACITY, KinErrors.ServerError, 'server error', {}], + [KinErrors.HorizonErrorType.RATE_LIMIT_EXCEEDED, KinErrors.ServerError, 'server error', {}], + [KinErrors.HorizonErrorType.SERVER_OVER_CAPACITY, KinErrors.ServerError, 'server error', {}], # InternalError - [HorizonErrorType.INTERNAL_SERVER_ERROR, KinErrors.InternalError, 'internal error', {}], + [KinErrors.HorizonErrorType.INTERNAL_SERVER_ERROR, KinErrors.InternalError, 'internal error', {}], ['unknown', KinErrors.InternalError, 'internal error', {'internal_error': 'unknown horizon error'}], ] for fixture in fixtures: - err_dict['type'] = HORIZON_NS_PREFIX + fixture[0] - e = KinErrors.translate_horizon_error(HorizonError(err_dict)) + err_dict['type'] = KinErrors.HORIZON_NS_PREFIX + fixture[0] + e = KinErrors.translate_horizon_error(KinErrors.HorizonError(err_dict)) assert isinstance(e, fixture[1]) assert e.error_code == fixture[0] assert e.message == fixture[2] def test_translate_transaction_error(): - err_dict = dict(type=HORIZON_NS_PREFIX + HorizonErrorType.TRANSACTION_FAILED, title='title', status=400, + err_dict = dict(type=KinErrors.HORIZON_NS_PREFIX + KinErrors.HorizonErrorType.TRANSACTION_FAILED, title='title', status=400, detail='detail', instance='instance', extras={'result_codes': {'operations': [], 'transaction': 'tx_failed'}}) fixtures = [ # RequestError - [TransactionResultCode.TOO_EARLY, KinErrors.RequestError, 'bad request', {}], - [TransactionResultCode.TOO_LATE, KinErrors.RequestError, 'bad request', {}], - [TransactionResultCode.MISSING_OPERATION, KinErrors.RequestError, 'bad request', {}], - [TransactionResultCode.BAD_AUTH, KinErrors.RequestError, 'bad request', {}], - [TransactionResultCode.BAD_AUTH_EXTRA, KinErrors.RequestError, 'bad request', {}], - [TransactionResultCode.BAD_SEQUENCE, KinErrors.RequestError, 'bad request', {}], - [TransactionResultCode.INSUFFICIENT_FEE, KinErrors.RequestError, 'bad request', {}], + [KinErrors.TransactionResultCode.TOO_EARLY, KinErrors.RequestError, 'bad request', {}], + [KinErrors.TransactionResultCode.TOO_LATE, KinErrors.RequestError, 'bad request', {}], + [KinErrors.TransactionResultCode.MISSING_OPERATION, KinErrors.RequestError, 'bad request', {}], + [KinErrors.TransactionResultCode.BAD_AUTH, KinErrors.RequestError, 'bad request', {}], + [KinErrors.TransactionResultCode.BAD_AUTH_EXTRA, KinErrors.RequestError, 'bad request', {}], + [KinErrors.TransactionResultCode.BAD_SEQUENCE, KinErrors.RequestError, 'bad request', {}], + [KinErrors.TransactionResultCode.INSUFFICIENT_FEE, KinErrors.RequestError, 'bad request', {}], # AccountNotFoundError - [TransactionResultCode.NO_ACCOUNT, KinErrors.AccountNotFoundError, 'account not found', {}], + [KinErrors.TransactionResultCode.NO_ACCOUNT, KinErrors.AccountNotFoundError, 'account not found', {}], # LowBalanceError - [TransactionResultCode.INSUFFICIENT_BALANCE, KinErrors.LowBalanceError, 'low balance', {}], + [KinErrors.TransactionResultCode.INSUFFICIENT_BALANCE, KinErrors.LowBalanceError, 'low balance', {}], # InternalError ['unknown', KinErrors.InternalError, 'internal error', {'internal_error': 'unknown transaction error'}] @@ -83,7 +83,7 @@ def test_translate_transaction_error(): for fixture in fixtures: err_dict['extras']['result_codes']['transaction'] = fixture[0] - e = KinErrors.translate_horizon_error(HorizonError(err_dict)) + e = KinErrors.translate_horizon_error(KinErrors.HorizonError(err_dict)) assert isinstance(e, fixture[1]) assert e.error_code == fixture[0] assert e.message == fixture[2] @@ -92,45 +92,45 @@ def test_translate_transaction_error(): def test_translate_operation_error(): # RequestError - err_dict = dict(type=HORIZON_NS_PREFIX + HorizonErrorType.TRANSACTION_FAILED, title='title', status=400, + err_dict = dict(type=KinErrors.HORIZON_NS_PREFIX + KinErrors.HorizonErrorType.TRANSACTION_FAILED, title='title', status=400, detail='detail', instance='instance', extras={'result_codes': {'operations': [], 'transaction': 'tx_failed'}}) fixtures = [ # RequestError - [[OperationResultCode.BAD_AUTH], KinErrors.RequestError, 'bad request', {}], - [[CreateAccountResultCode.MALFORMED], KinErrors.RequestError, 'bad request', {}], - [[PaymentResultCode.NO_ISSUER], KinErrors.RequestError, 'bad request', {}], - [[PaymentResultCode.LINE_FULL], KinErrors.RequestError, 'bad request', {}], - [[ChangeTrustResultCode.INVALID_LIMIT], KinErrors.RequestError, 'bad request', {}], + [[KinErrors.OperationResultCode.BAD_AUTH], KinErrors.RequestError, 'bad request', {}], + [[KinErrors.CreateAccountResultCode.MALFORMED], KinErrors.RequestError, 'bad request', {}], + [[KinErrors.PaymentResultCode.NO_ISSUER], KinErrors.RequestError, 'bad request', {}], + [[KinErrors.PaymentResultCode.LINE_FULL], KinErrors.RequestError, 'bad request', {}], + [[KinErrors.ChangeTrustResultCode.INVALID_LIMIT], KinErrors.RequestError, 'bad request', {}], # AccountNotFoundError - [[OperationResultCode.NO_ACCOUNT], KinErrors.AccountNotFoundError, 'account not found', {}], - [[PaymentResultCode.NO_DESTINATION], KinErrors.AccountNotFoundError, 'account not found', {}], + [[KinErrors.OperationResultCode.NO_ACCOUNT], KinErrors.AccountNotFoundError, 'account not found', {}], + [[KinErrors.PaymentResultCode.NO_DESTINATION], KinErrors.AccountNotFoundError, 'account not found', {}], # AccountExistsError - [[CreateAccountResultCode.ACCOUNT_EXISTS], KinErrors.AccountExistsError, 'account already exists', {}], + [[KinErrors.CreateAccountResultCode.ACCOUNT_EXISTS], KinErrors.AccountExistsError, 'account already exists', {}], # LowBalanceError - [[CreateAccountResultCode.LOW_RESERVE], KinErrors.LowBalanceError, 'low balance', {}], - [[PaymentResultCode.UNDERFUNDED], KinErrors.LowBalanceError, 'low balance', {}], + [[KinErrors.CreateAccountResultCode.LOW_RESERVE], KinErrors.LowBalanceError, 'low balance', {}], + [[KinErrors.PaymentResultCode.UNDERFUNDED], KinErrors.LowBalanceError, 'low balance', {}], # AccountNotActivatedError - [[PaymentResultCode.SRC_NO_TRUST], KinErrors.AccountNotActivatedError, 'account not activated', {}], - [[PaymentResultCode.NO_TRUST], KinErrors.AccountNotActivatedError, 'account not activated', {}], - [[PaymentResultCode.SRC_NOT_AUTHORIZED], KinErrors.AccountNotActivatedError, 'account not activated', {}], - [[PaymentResultCode.NOT_AUTHORIZED], KinErrors.AccountNotActivatedError, 'account not activated', {}], + [[KinErrors.PaymentResultCode.SRC_NO_TRUST], KinErrors.AccountNotActivatedError, 'account not activated', {}], + [[KinErrors.PaymentResultCode.NO_TRUST], KinErrors.AccountNotActivatedError, 'account not activated', {}], + [[KinErrors.PaymentResultCode.SRC_NOT_AUTHORIZED], KinErrors.AccountNotActivatedError, 'account not activated', {}], + [[KinErrors.PaymentResultCode.NOT_AUTHORIZED], KinErrors.AccountNotActivatedError, 'account not activated', {}], # InternalError [['unknown'], KinErrors.InternalError, 'internal error', {'internal_error': 'unknown operation error'}], # MultiOp - [[OperationResultCode.SUCCESS, PaymentResultCode.UNDERFUNDED], KinErrors.LowBalanceError, 'low balance', {}] + [[KinErrors.OperationResultCode.SUCCESS, KinErrors.PaymentResultCode.UNDERFUNDED], KinErrors.LowBalanceError, 'low balance', {}] ] for fixture in fixtures: err_dict['extras']['result_codes']['operations'] = fixture[0] - e = KinErrors.translate_horizon_error(HorizonError(err_dict)) + e = KinErrors.translate_horizon_error(KinErrors.HorizonError(err_dict)) assert isinstance(e, fixture[1]) assert e.error_code == fixture[0][-1] assert e.message == fixture[2] From 82b8ee3221e216330ba404100e009939c750bbfd Mon Sep 17 00:00:00 2001 From: ronserruya Date: Thu, 14 Mar 2019 15:39:31 +0200 Subject: [PATCH 03/22] Add async and type hints --- kin/account.py | 38 ++-- kin/blockchain/channel_manager.py | 89 ++++------ kin/blockchain/environment.py | 17 +- kin/blockchain/errors.py | 11 -- kin/blockchain/keypair.py | 28 ++- kin/client.py | 50 ++---- kin/config.py | 2 +- kin/monitors.py | 277 +++++++----------------------- kin/transactions.py | 1 - kin/utils.py | 42 +++-- 10 files changed, 175 insertions(+), 380 deletions(-) diff --git a/kin/account.py b/kin/account.py index 7db13bb..f884124 100644 --- a/kin/account.py +++ b/kin/account.py @@ -11,7 +11,7 @@ from .blockchain.channel_manager import ChannelManager, ChannelStatuses from . import errors as KinErrors from .transactions import build_memo, RawTransaction, SimplifiedTransaction -from .blockchain.errors import TransactionResultCode, HorizonErrorType, HorizonError +from .blockchain.errors import TransactionResultCode, HorizonErrorType from .config import APP_ID_REGEX, KIN_DECIMAL_PRECISION from .blockchain.utils import is_valid_address, is_valid_secret_key from .blockchain.horizon_models import AccountData @@ -95,7 +95,7 @@ async def get_status(self, verbose: Optional[bool] = False) -> dict: return total_status async def get_transaction_history(self, amount: Optional[int] = 10, descending: Optional[bool] = True, - cursor: Optional[int, None] = None, + cursor: Optional[int] = None, simple: Optional[bool] = True) -> List[Union[SimplifiedTransaction, RawTransaction]]: """ Get the transaction history for this kin account @@ -141,7 +141,7 @@ async def create_account(self, address: str, starting_balance: Union[float, str] """ builder = self.build_create_account(address, starting_balance, fee, memo_text) - with self.channel_manager.get_channel() as channel: # TODO: async with? + async with self.channel_manager.get_channel() as channel: await builder.set_channel(channel) builder.sign(channel) # Also sign with the root account if a different channel was used @@ -169,7 +169,7 @@ async def send_kin(self, address: str, amount: Union[float, str], fee: int, :raises: KinErrors.NotValidParamError: if the fee is not valid """ builder = self.build_send_kin(address, amount, fee, memo_text) - with self.channel_manager.get_channel() as channel: + async with self.channel_manager.get_channel() as channel: await builder.set_channel(channel) builder.sign(channel) # Also sign with the root account if a different channel was used @@ -226,7 +226,7 @@ def build_send_kin(self, address: str, amount: Union[float, str], fee: int, builder.append_payment_op(address, str(amount), source=self.keypair.public_address) return builder - async def submit_transaction(self, tx_builder) -> str: + async def submit_transaction(self, tx_builder: Builder) -> str: """ Submit a transaction to the blockchain. :param kin.Builder tx_builder: The transaction builder @@ -236,21 +236,22 @@ async def submit_transaction(self, tx_builder) -> str: try: return (await tx_builder.submit())['hash'] # If the channel is out of KIN, top it up and try again - except HorizonError as e: + except KinErrors.HorizonError as e: logger.warning('send transaction error with channel {}: {}'.format(tx_builder.address, str(e))) if e.type == HorizonErrorType.TRANSACTION_FAILED \ - and e.extras.result_codes.transaction == TransactionResultCode.INSUFFICIENT_BALANCE: + and e.extras['result_codes']['transaction'] == TransactionResultCode.INSUFFICIENT_BALANCE: self.channel_manager.channel_pool.queue[tx_builder.address] = ChannelStatuses.UNDERFUNDED - self._top_up(tx_builder.address) + await self._top_up(tx_builder.address) self.channel_manager.channel_pool.queue[tx_builder.address] = ChannelStatuses.TAKEN # Insufficient balance is a "fast-fail", the sequence number doesn't increment # so there is no need to build the transaction again - self.submit_transaction(tx_builder) + await self.submit_transaction(tx_builder) else: raise KinErrors.translate_error(e) + # TODO: asyncify def monitor_payments(self, callback_fn): """Monitor KIN payment transactions related to this account NOTE: the function starts a background thread. @@ -263,12 +264,11 @@ def monitor_payments(self, callback_fn): """ return self._client.monitor_account_payments(self.keypair.public_address, callback_fn) - def whitelist_transaction(self, payload): + def whitelist_transaction(self, payload: Union[str, dict]) -> str: """ Sign on a transaction to whitelist it - :param str payload: the json received from the client + :param payload: the json received from the client :return: a signed transaction encoded as base64 - :rtype str """ # load the object from the json @@ -302,20 +302,16 @@ def whitelist_transaction(self, payload): # Internal methods - def _top_up(self, address): + async def _top_up(self, address: str) -> None: """ Top up a channel with the base account. - :param str address: The address to top up + :param address: The address to top up """ - # In theory, if the sdk runs in threads, and 2 or more channels - # are out of funds and needed to be topped up at the exact same time - # there is a chance for a bad_sequence error, - # however it is virtually impossible that this situation will occur. # TODO: let user config the amount of kin to top up - min_fee = self._client.get_minimum_fee() + min_fee = await self._client.get_minimum_fee() builder = self.get_transaction_builder(min_fee) builder.append_payment_op(address, str((min_fee / KIN_DECIMAL_PRECISION) * 1000)) # Enough for 1K txs - builder.update_sequence() + await builder.update_sequence() builder.sign() - builder.submit() + await builder.submit() diff --git a/kin/blockchain/channel_manager.py b/kin/blockchain/channel_manager.py index 35e2a2b..57bde22 100644 --- a/kin/blockchain/channel_manager.py +++ b/kin/blockchain/channel_manager.py @@ -1,80 +1,59 @@ """Contains classes and methods related to channels""" -import sys import random -from contextlib import contextmanager +from contextlib import asynccontextmanager +from asyncio.queues import Queue as queue from enum import Enum -from .errors import ChannelsBusyError, ChannelsFullError - -if sys.version[0] == '2': - import Queue as queue -else: - import queue - -CHANNEL_GET_TIMEOUT = 11 # how much time to wait until a channel is available, in seconds -CHANNEL_PUT_TIMEOUT = 0.5 # how much time to wait for a channel to return to the queue +from typing import List, Optional class ChannelManager: """Provide useful methods to interact with the underlying ChannelPool""" - def __init__(self, channel_seeds): + def __init__(self, channel_seeds: List[str]): """ Crete a channel manager instance - :param list[str] channel_seeds: The seeds of the channels to use + :param channel_seeds: The seeds of the channels to use """ self.channel_pool = ChannelPool(channel_seeds) - @contextmanager - def get_channel(self, timeout=CHANNEL_GET_TIMEOUT): + @asynccontextmanager + async def get_channel(self) -> str: """ Get an available channel - :param float timeout: (Optional) How long to wait before raising an exception :return a free channel seed - :rtype str - :raises KinErrors.ChannelBusyError """ - try: - channel = self.channel_pool.get(timeout=timeout) - except queue.Empty: - raise ChannelsBusyError() + channel = await self.channel_pool.get() try: yield channel finally: - if self.channel_pool.queue[channel] != ChannelStatuses.UNDERFUNDED: - self.put_channel(channel) + if self.channel_pool._queue[channel] != ChannelStatuses.UNDERFUNDED: + await self.put_channel(channel) - def put_channel(self, channel, timeout=CHANNEL_PUT_TIMEOUT): + async def put_channel(self, channel) -> None: """ Set a channel status back to FREE :param str channel: the channel to set back to FREE - :param float timeout: (Optional) How long to wait before raising an exception - - :raises KinErrors.ChannelsFullError """ - try: - self.channel_pool.put(channel, timeout=timeout) - except queue.Full: - raise ChannelsFullError() + await self.channel_pool.put(channel) - def get_status(self, verbose=False): + def get_status(self, verbose: Optional[bool] = False) -> dict: """ Return the current status of the channel manager - :param bool verbose: Include all channel seeds and their statuses in the response + :param verbose: Include all channel seeds and their statuses in the response :return: The status of the channel manager - :rtype dict """ free_channels = len(self.channel_pool.get_free_channels()) status = { - 'total_channels': len(self.channel_pool.queue), + 'total_channels': len(self.channel_pool._queue), 'free_channels': free_channels, - 'non_free_channels': len(self.channel_pool.queue) - free_channels + 'non_free_channels': len(self.channel_pool._queue) - free_channels } if verbose: - status['channels'] = self.channel_pool.queue + status['channels'] = self.channel_pool._queue return status @@ -87,10 +66,9 @@ class ChannelStatuses(str, Enum): UNDERFUNDED = 'underfunded' -# TODO: remove object when we kill python2 -class ChannelPool(queue.Queue, object): +class ChannelPool(queue): """ - A thread-safe queue that sets a member's status instead of pulling it in/out of the queue. + An async queue that sets a member's status instead of pulling it in/out of the queue. This queue gets members randomly when 'get' is used, as opposed to always get the last member. """ def __init__(self, channels_seeds): @@ -101,44 +79,45 @@ def __init__(self, channels_seeds): # Init base queue super(ChannelPool, self).__init__(len(channels_seeds)) # Change queue from a 'deque' object to a dict full of free channels - self.queue = {channel: ChannelStatuses.FREE for channel in channels_seeds} + self._queue = {channel: ChannelStatuses.FREE for channel in channels_seeds} - def _get(self): + def _get(self) -> str: """ Randomly get an available free channel from the dict :return: a channel seed - :rtype str """ # Get a list of all free channels free_channels = self.get_free_channels() # Select a random free channel selected_channel = random.choice(free_channels) # Change channel state to taken - self.queue[selected_channel] = ChannelStatuses.TAKEN + self._queue[selected_channel] = ChannelStatuses.TAKEN return selected_channel - def _put(self, channel): + def _put(self, channel: str) -> None: """ Change a channel status back to FREE :param str channel: the channel seed """ # Change channel state to free - self.queue[channel] = ChannelStatuses.FREE + self._queue[channel] = ChannelStatuses.FREE - def _qsize(self): + def qsize(self) -> int: """ - Used to determine if the queue is empty + Counts free channels in the queue :return: amount of free channels in the queue - :rtype int """ - # Base queue checks if the queue is not empty by checking the length of the queue (_qsize() != 0) - # We need to check it by checking how many channels are free return len(self.get_free_channels()) - def get_free_channels(self): + def empty(self) -> bool: + """ + Used to check if the queue is empty + """ + return len(self.get_free_channels()) == 0 + + def get_free_channels(self) -> List[str]: """ Get a list of channels with "FREE" status - :rtype list[str] """ - return [channel for channel, status in self.queue.items() if status == ChannelStatuses.FREE] + return [channel for channel, status in self._queue.items() if status == ChannelStatuses.FREE] diff --git a/kin/blockchain/environment.py b/kin/blockchain/environment.py index a1ca0c2..d513bf7 100644 --- a/kin/blockchain/environment.py +++ b/kin/blockchain/environment.py @@ -3,23 +3,20 @@ from hashlib import sha256 from kin_base.network import NETWORKS -from kin_base.asset import Asset -from kin_base.exceptions import StellarAddressInvalidError -from .utils import is_valid_address +from typing import Optional class Environment: """Environments holds the parameters that will be used to connect to horizon""" - def __init__(self, name, horizon_endpoint_uri, network_passphrase, friendbot_url=None): + def __init__(self, name: str, horizon_endpoint_uri: str, network_passphrase: str, + friendbot_url: Optional[str] = None): """ - - :param str name: Name of the environment. - :param str horizon_uri: a Horizon endpoint. - :param str network_passphrase: The passphrase/network_id of the environment. - :param str friendbot_url: a url to a friendbot service + :param name: Name of the environment. + :param horizon_uri: a Horizon endpoint. + :param network_passphrase: The passphrase/network_id of the environment. + :param friendbot_url: a url to a friendbot service :return: An instance of the Environment class. - :rtype: kin.Environment """ # Add the network to the kin_base network list. NETWORKS[name.upper()] = network_passphrase diff --git a/kin/blockchain/errors.py b/kin/blockchain/errors.py index a564a81..c2018e6 100644 --- a/kin/blockchain/errors.py +++ b/kin/blockchain/errors.py @@ -1,16 +1,5 @@ """Contains errors types related to horizon""" -from .horizon_models import HTTPProblemDetails - - -class ChannelsBusyError(Exception): - pass - - -class ChannelsFullError(Exception): - pass - - HORIZON_NS_PREFIX = 'https://stellar.org/horizon-errors/' """ Horizon error example: diff --git a/kin/blockchain/keypair.py b/kin/blockchain/keypair.py index fce9cf5..b2307ba 100644 --- a/kin/blockchain/keypair.py +++ b/kin/blockchain/keypair.py @@ -8,14 +8,16 @@ from .utils import is_valid_secret_key +from typing import Optional + class Keypair: - """A simpler version of kin_base.Keypair that holds the public address and secret seed.""" + """A simpler version of kin_base.Keypair that caches the public address and secret seed.""" - def __init__(self, seed=None): + def __init__(self, seed: Optional[str] = None): """ # Create an instance of Keypair. - :param str seed: (Optional) The secret seed of an account + :param seed: (Optional) The secret seed of an account """ self.secret_seed = seed or self.generate_seed() if not is_valid_secret_key(self.secret_seed): @@ -27,43 +29,39 @@ def __init__(self, seed=None): self._hint = base_keypair.signature_hint() self._signing_key = base_keypair.signing_key - def sign(self, data): + def sign(self, data: bytes) -> DecoratedSignature: """ Sign any data using the keypair's private key - :param bytes data: any data to sign + :param data: any data to sign :return: a decorated signature - :rtype kin_base.stellarxdr.StellarXDR_type.DecoratedSignature """ signature = self._signing_key.sign(data) return DecoratedSignature(self._hint, signature) @staticmethod - def address_from_seed(seed): + def address_from_seed(seed: str) -> str: """ Get a public address from a secret seed. - :param str seed: The secret seed of an account. + :param seed: The secret seed of an account. :return: A public address. - :rtype str """ return BaseKeypair.from_seed(seed).address().decode() @staticmethod - def generate_seed(): + def generate_seed() -> str: """ Generate a random secret seed. :return: A secret seed. - :rtype str """ return BaseKeypair.random().seed().decode() @staticmethod - def generate_hd_seed(base_seed, salt): + def generate_hd_seed(base_seed: str, salt: str) -> str: """ Generate a highly deterministic seed from a base seed + salt - :param str base_seed: The base seed to generate a seed from - :param str salt: A unique string that will be used to generate the seed + :param base_seed: The base seed to generate a seed from + :param salt: A unique string that will be used to generate the seed :return: a new seed. - :rtype str """ # Create a new raw seed from this hash raw_seed = sha256((base_seed + salt).encode()).digest() diff --git a/kin/client.py b/kin/client.py index d7db05a..a4f08db 100644 --- a/kin/client.py +++ b/kin/client.py @@ -4,7 +4,7 @@ from .config import ANON_APP_ID, MAX_RECORDS_PER_REQUEST from . import errors as KinErrors -from .monitors import SingleMonitor, MultiMonitor +from .monitors import single_monitor, multi_monitor from .transactions import SimplifiedTransaction, RawTransaction from .account import KinAccount from .blockchain.horizon_models import AccountData @@ -12,7 +12,7 @@ from .version import __version__ from .blockchain.environment import Environment -from typing import List, Optional, Union +from typing import List, Optional, Union, AsyncGenerator import logging @@ -40,6 +40,12 @@ def __init__(self, environment: Environment): logger.info('Kin Client initialized on network {}, horizon endpoint {}'. format(self.network, self.horizon.horizon_uri)) + async def __aenter__(self): + return self + + async def __aexit__(self, exc_type, exc_val, exc_tb): + await self.horizon.__aexit__(exc_type, exc_val, exc_tb) + def kin_account(self, seed: str, channel_secret_keys: Optional[List[str]] = None, app_id: Optional[str] = ANON_APP_ID) -> KinAccount: """ @@ -176,7 +182,7 @@ async def get_transaction_data(self, tx_hash: str, simple: Optional[bool] = True return raw_tx async def get_account_tx_history(self, address: str, amount: Optional[int] = 10, descending: Optional[bool] = True, - cursor: Optional[int, None] = None, + cursor: Optional[int] = None, simple: Optional[bool] = True) -> List[Union[SimplifiedTransaction, RawTransaction]]: """ Get the transaction history for a given account. @@ -246,43 +252,25 @@ async def friendbot(self, address: str): if response.status == 200: return (await response.json(encoding='utf-8'))['hash'] else: - raise KinErrors.FriendbotError(response.status, await (response.text(encoding='utf-8'))) + raise KinErrors.FriendbotError(response.status, await response.text(encoding='utf-8')) - # TODO: asyncify - def monitor_account_payments(self, address, callback_fn): + def monitor_account_payments(self, address: str, timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: """Monitor KIN payment transactions related to the account identified by provided address. - NOTE: the function starts a background thread. :param str address: the address of the account to query. + :param timeout: How long to wait for each event - :param callback_fn: the function to call on each received payment as `callback_fn(address, tx_data, monitor)`. - :type: callable[str,kin.transactions.SimplifiedTransaction, kin.SingleMonitor] - - :return: a monitor instance - :rtype: kin.monitors.SingleMonitor - - :raises: ValueError: when no address is given. :raises: ValueError: if the address is in the wrong format - :raises: KinErrors.AccountNotActivatedError if the account given is not activated + :raises: asyncio.TimeoutError: If too much time has passed between events (only if "timeout" is set) """ + return single_monitor(self, address, timeout=timeout) - return SingleMonitor(self, address, callback_fn) - - def monitor_accounts_payments(self, addresses, callback_fn): + def monitor_accounts_payments(self, addresses: set, timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: """Monitor KIN payment transactions related to multiple accounts - NOTE: the function starts a background thread. - - :param str addresses: the addresses of the accounts to query. - :param callback_fn: the function to call on each received payment as `callback_fn(address, tx_data, monitor)`. - :type: callable[str,kin.transactions.SimplifiedTransaction ,kin.monitors.MultiMonitor] + :param addresses: the addresses of the accounts to query. + :param timeout: How long to wait for each event - :return: a monitor instance - :rtype: kin.monitors.MultiMonitor - - :raises: ValueError: when no address is given. - :raises: ValueError: if the addresses are in the wrong format - :raises: KinErrors.AccountNotActivatedError if the accounts given are not activated + :raises: asyncio.TimeoutError: If too much time has passed between events (only if "timeout" is set) """ - - return MultiMonitor(self, addresses, callback_fn) + return multi_monitor(self, addresses, timeout=timeout) diff --git a/kin/config.py b/kin/config.py index 910e16a..6ab68a3 100644 --- a/kin/config.py +++ b/kin/config.py @@ -25,4 +25,4 @@ MAX_RECORDS_PER_REQUEST = 200 -SDK_USER_AGENT = 'kin-core-python/{}'.format(__version__) +SDK_USER_AGENT = 'kin-sdk-python/{}'.format(__version__) diff --git a/kin/monitors.py b/kin/monitors.py index 1a676f2..f22c0bf 100644 --- a/kin/monitors.py +++ b/kin/monitors.py @@ -1,227 +1,72 @@ """Contains classes for monitoring the blockchain""" -from threading import Thread, Event +import json from .blockchain.utils import is_valid_address -from .transactions import OperationTypes, SimplifiedTransaction ,RawTransaction -from .errors import AccountNotFoundError, CantSimplifyError, StoppedMonitorError, StellarAddressInvalidError +from .transactions import OperationTypes, SimplifiedTransaction, RawTransaction +from .errors import CantSimplifyError, StellarAddressInvalidError + +from typing import Optional, AsyncGenerator import logging logger = logging.getLogger(__name__) -class SingleMonitor: - """Single Monitor to monitor Kin payment on a single account""" - - def __init__(self, kin_client, address, callback_fn): - """ - Monitors a single account for kin payments - :param kin.KinClient kin_client: a kin client directed to the correct network - :param str address: address to watch - :param callback_fn: the function to call on each received payment as `callback_fn(address, tx_data, monitor)`. - :type: callable[str,kin.transactions.SimplifiedTransaction ,kin.monitors.MultiMonitor] - """ - self.kin_client = kin_client - self.callback_fn = callback_fn - - if not address: - raise ValueError('no address to monitor') - - if not is_valid_address(address): - raise StellarAddressInvalidError('invalid address: {}'.format(address)) - - if not self.kin_client.does_account_exists(address): - raise AccountNotFoundError(address) - - self.address = address - - # Currently, due to nonstandard SSE implementation in Horizon, - # using cursor=now will block until the first tx happens. - # Instead, we determine the cursor ourselves. - # Fix will be for horizon to send any message just to start a connection. - # This will cause a tx - params = {} - reply = self.kin_client.horizon.account_transactions(address, params={'order': 'desc', 'limit': 2}) - if len(reply['_embedded']['records']) == 2: - cursor = reply['_embedded']['records'][1]['paging_token'] - params = {'cursor': cursor} - - # make synchronous SSE request (will raise errors in the current thread) - self.sse_client = self.kin_client.horizon.account_transactions(address, sse=True, params=params) - - self.stop_event = Event() - # start monitoring thread - self.thread = Thread(target=self.event_processor, args=(self.stop_event,)) - self.thread.daemon = True - self.thread.start() - - def event_processor(self, stop_event): - """ - Method to filter through SSE events and find kin payments for an account - :param threading.Event stop_event: an event that can be used to stop this method - """ - import json +async def single_monitor(kin_client: 'KinClient', address: str, + timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: + """ + Monitors a single account for kin payments + :param kin_client: a kin client directed to the correct network + :param address: address to watch + :param timeout: How long to wait for a new event + + :raises: asyncio.TimeoutError: If too much time has passed between events (only if "timeout" is set) + """ + if not is_valid_address(address): + raise StellarAddressInvalidError('invalid address: {}'.format(address)) + + sse_client = await kin_client.horizon.account_transactions(address, sse=True, sse_timeout=timeout) + + async for tx in sse_client: try: - for event in self.sse_client: - if stop_event.is_set(): - return - if event.event != 'message': - continue - try: - tx = json.loads(event.data) - - try: - tx_data = SimplifiedTransaction(RawTransaction(tx)) - except CantSimplifyError: - continue - - if tx_data.operation.type != OperationTypes.PAYMENT: - continue - - self.callback_fn(self.address, tx_data, self) - - except Exception as ex: - logger.exception(ex) - continue - except TypeError: - # If we got a type error, that means retry was none, so we should end the thread - return - - def stop(self): - """ - Stop monitoring the account. - - The thread will terminate in up to X seconds, where X is the timeout set by the blockchain. - """ - - # Set the stop event, this will terminate the thread once we get the next event from the blockchain. - self.stop_event.set() - - # Change the retry value, - # this will cause an exception when trying to reconnect after timeout by the blockchain, - # which will terminate the thread. - self.sse_client.retry = None - - -class MultiMonitor: - """Multi Monitor to monitor Kin payment on a multiple accounts""" - - def __init__(self, kin_client, addresses, callback_fn): - """ - Monitors multiple accounts for kin payments - :param kin.KinClient kin_client: a kin client directed to the correct network - :param str addresses: addresses to watch - :param callback_fn: the function to call on each received payment as `callback_fn(address, tx_data, monitor)`. - :type: callable[str,kin.transactions.SimplifiedTransaction ,kin.monitors.MultiMonitor] - """ - self.kin_client = kin_client - self.callback_fn = callback_fn - - if not addresses: - raise ValueError('no address to monitor') - - for address in addresses: - if not is_valid_address(address): - raise StellarAddressInvalidError('invalid address: {}'.format(address)) - if not self.kin_client.does_account_exists(address): - raise AccountNotFoundError(address) - - self.addresses = addresses - - # Currently, due to nonstandard SSE implementation in Horizon, - # using cursor=now will block until the first tx happens. - # Instead, we determine the cursor ourselves. - # Fix will be for horizon to send any message just to start a connection - params = {} - reply = self.kin_client.horizon.transactions(params={'order': 'desc', 'limit': 1}) - if len(reply['_embedded']['records']) == 1: - cursor = reply['_embedded']['records'][0]['paging_token'] - params = {'cursor': cursor} - - # make synchronous SSE request (will raise errors in the current thread) - self.sse_client = self.kin_client.horizon.transactions(sse=True, params=params) - - self.stop_event = Event() - # start monitoring thread - self.thread = Thread(target=self.event_processor, args=(self.stop_event,)) - self.thread.daemon = True - self.thread.start() - - def event_processor(self, stop_event): - """ - Method to filter through SSE events and find kin payments for an account - :param threading.Event stop_event: an event that can be used to stop this method - """ - import json + tx_data = SimplifiedTransaction(RawTransaction(tx)) + except CantSimplifyError: + logger.debug("SSE transaction couldn't be simplified: ", tx) + continue + + if tx_data.operation.type != OperationTypes.PAYMENT: + logger.debug("Non-payment SSE transaction skipped: ", tx_data) + continue + + yield tx_data + + +async def multi_monitor(kin_client: 'KinClient', addresses: set, + timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: + """ + Monitors a single account for kin payments + :param kin_client: a kin client directed to the correct network + :param addresses: set of addresses to watch + :param timeout: How long to wait for a new event + + :raises: asyncio.TimeoutError: If too much time has passed between events (only if "timeout" is set) + """ + + sse_client = await kin_client.horizon.transactions(sse=True, sse_timeout=timeout) + + async for tx in sse_client: try: - for event in self.sse_client: - if stop_event.is_set(): - return - if event.event != 'message': - continue - try: - tx = json.loads(event.data) - - try: - tx_data = SimplifiedTransaction(RawTransaction(tx)) - except CantSimplifyError: - continue - - if tx_data.operation.type != OperationTypes.PAYMENT: - continue - - if tx_data.source in self.addresses: - self.callback_fn(tx_data.source, tx_data, self) - if tx_data.operation.destination in self.addresses: - self.callback_fn(tx_data.operation.destination, tx_data, self) - - except Exception as ex: - logger.exception(ex) - continue - except TypeError: - # If we got a type error, that means retry was none, so we should end the thread - return - - def stop(self): - """ - Stop monitoring the account. - - The thread will terminate in up to X seconds, where X is the timeout set by the blockchain. - """ - - # Set the stop event, this will terminate the thread once we get the next event from the blockchain. - self.stop_event.set() - - # Change the retry value, - # this will cause an exception when trying to reconnect after timeout by the blockchain, - # which will terminate the thread. - self.sse_client.retry = None - - def add_address(self, address): - """ - Add address to the watched addresses list - :param address: address to add - """ - if address in self.addresses: - return - - if self.stop_event.is_set(): - raise StoppedMonitorError() - - if not is_valid_address(address): - raise StellarAddressInvalidError('invalid address: {}'.format(address)) - - if not self.kin_client.does_account_exists(address): - raise AccountNotFoundError(address) - - self.addresses.append(address) - - def remove_address(self, address): - """ - Remove an address for the list of addresses to watch - :param address: the address to remove - """ - if self.stop_event.is_set(): - raise StoppedMonitorError() - - self.addresses.remove(address) + tx_data = SimplifiedTransaction(RawTransaction(tx)) + except CantSimplifyError: + logger.debug("SSE transaction couldn't be simplified: ", tx) + continue + + if tx_data.operation.type != OperationTypes.PAYMENT: + logger.debug("Non-payment SSE transaction skipped: ", tx_data) + continue + + # Will yield twice if both of these are correct. (someone sent to himself) - which it fine + if tx_data.source in addresses: + yield tx_data.source, tx_data + if tx_data.operation.destination in addresses: + yield tx_data.operation.destination, tx_data diff --git a/kin/transactions.py b/kin/transactions.py index 37bc392..7969bb9 100644 --- a/kin/transactions.py +++ b/kin/transactions.py @@ -10,7 +10,6 @@ from kin_base.transaction_envelope import TransactionEnvelope as BaseEnvelop from kin_base.memo import TextMemo, NoneMemo from kin_base.operation import Payment, CreateAccount -from .blockchain.channel_manager import CHANNEL_PUT_TIMEOUT from .errors import CantSimplifyError from .config import MEMO_TEMPLATE diff --git a/kin/utils.py b/kin/utils.py index 6c3248b..a2762de 100644 --- a/kin/utils.py +++ b/kin/utils.py @@ -2,66 +2,70 @@ from hashlib import sha256 +from kin_base import Builder + from .client import KinClient -from .blockchain.builder import Builder from .blockchain.keypair import Keypair from .errors import AccountNotFoundError +from .blockchain.environment import Environment + +from typing import List -def create_channels(master_seed, environment, amount, starting_balance, salt): +async def create_channels(master_seed: str, environment: Environment, amount: int, + starting_balance: float, salt: str) -> List[str]: """ Create HD seeds based on a master seed and salt - :param str master_seed: The master seed that creates the seeds - :param Kin.Environment environment: The blockchain environment to create the seeds on - :param int amount: Number of seeds to create (Up to 100) - :param float starting_balance: Starting balance to create channels with - :param str salt: A string to be used to create the HD seeds + :param master_seed: The master seed that creates the seeds + :param environment: The blockchain environment to create the seeds on + :param amount: Number of seeds to create (Up to 100) + :param starting_balance: Starting balance to create channels with + :param salt: A string to be used to create the HD seeds :return: The list of seeds generated - :rtype list[str] """ client = KinClient(environment) base_key = Keypair(master_seed) - if not client.does_account_exists(base_key.public_address): + if not await client.does_account_exists(base_key.public_address): raise AccountNotFoundError(base_key.public_address) - fee = client.get_minimum_fee() + fee = await client.get_minimum_fee() channels = get_hd_channels(master_seed, salt, amount) # Create a builder for the transaction - builder = Builder(environment.name, client.horizon, fee, master_seed) + builder = Builder(client.horizon, environment.name, fee, master_seed) # Find out if this salt+seed combination was ever used to create channels. # If so, the user might only be interested in adding channels, # so we need to find what seed to start from # First check if the last channel exists, if it does, we don't need to create any channel. - if client.does_account_exists(Keypair.address_from_seed(channels[-1])): + if await client.does_account_exists(Keypair.address_from_seed(channels[-1])): return channels for index, seed in enumerate(channels): - if client.does_account_exists(Keypair.address_from_seed(seed)): + if await client.does_account_exists(Keypair.address_from_seed(seed)): continue # Start creating from the current seed forward for channel_seed in channels[index:]: builder.append_create_account_op(Keypair.address_from_seed(channel_seed), str(starting_balance)) - builder.update_sequence() + await builder.update_sequence() builder.sign() - builder.submit() + await builder.submit() break return channels -def get_hd_channels(master_seed, salt, amount): +def get_hd_channels(master_seed: str, salt: str, amount: int) -> List[str]: """ Get a list of channels generated based on a seed and salt - :param str master_seed: the base seed that created the channels - :param str salt: A string to be used to generate the seeds - :param int amount: Number of seeds to generate (Up to 100) + :param master_seed: the base seed that created the channels + :param salt: A string to be used to generate the seeds + :param amount: Number of seeds to generate (Up to 100) :return: The list of seeds generated :rtype list[str] """ From 0b27b1a291c9e66589d95d2470397b8b7e438ad2 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Sun, 17 Mar 2019 14:47:03 +0200 Subject: [PATCH 04/22] Add type hints to transactions --- kin/client.py | 1 + kin/monitors.py | 12 +++-------- kin/transactions.py | 51 ++++++++++++++++++++++----------------------- 3 files changed, 29 insertions(+), 35 deletions(-) diff --git a/kin/client.py b/kin/client.py index a4f08db..9fd9751 100644 --- a/kin/client.py +++ b/kin/client.py @@ -222,6 +222,7 @@ async def get_account_tx_history(self, address: str, amount: Optional[int] = 10, remaining_txs = amount - len(tx_list) # if we got all the txs that we wanted, or there are no more txs + # TODO: paging does not work DP-370 if remaining_txs <= 0 or len(horizon_response['_embedded']['records']) < amount: return tx_list # If there are anymore transactions, recursively get the next transaction page diff --git a/kin/monitors.py b/kin/monitors.py index f22c0bf..059e234 100644 --- a/kin/monitors.py +++ b/kin/monitors.py @@ -1,6 +1,4 @@ """Contains classes for monitoring the blockchain""" -import json - from .blockchain.utils import is_valid_address from .transactions import OperationTypes, SimplifiedTransaction, RawTransaction from .errors import CantSimplifyError, StellarAddressInvalidError @@ -41,18 +39,14 @@ async def single_monitor(kin_client: 'KinClient', address: str, yield tx_data -async def multi_monitor(kin_client: 'KinClient', addresses: set, - timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: +async def multi_monitor(kin_client: 'KinClient', addresses: set) -> AsyncGenerator[SimplifiedTransaction, None]: """ Monitors a single account for kin payments :param kin_client: a kin client directed to the correct network :param addresses: set of addresses to watch - :param timeout: How long to wait for a new event - - :raises: asyncio.TimeoutError: If too much time has passed between events (only if "timeout" is set) """ - sse_client = await kin_client.horizon.transactions(sse=True, sse_timeout=timeout) + sse_client = await kin_client.horizon.transactions(sse=True) async for tx in sse_client: try: @@ -69,4 +63,4 @@ async def multi_monitor(kin_client: 'KinClient', addresses: set, if tx_data.source in addresses: yield tx_data.source, tx_data if tx_data.operation.destination in addresses: - yield tx_data.operation.destination, tx_data + yield tx_data.operation.destination, tx_data \ No newline at end of file diff --git a/kin/transactions.py b/kin/transactions.py index 7969bb9..20d0f56 100644 --- a/kin/transactions.py +++ b/kin/transactions.py @@ -1,5 +1,4 @@ """Contains classes and methods related to transactions and operations""" -import sys from hashlib import sha256 from binascii import hexlify import base64 @@ -14,6 +13,7 @@ from .errors import CantSimplifyError from .config import MEMO_TEMPLATE +from typing import Union, Optional # This is needed in order to calculate transaction hash. # It is the xdr representation of kin_base.XDR.const.ENVELOP_TYPE_TX (2) @@ -21,10 +21,22 @@ NATIVE_ASSET_TYPE = 'native' +class RawTransaction: + """Class to hold raw info about a transaction""" + def __init__(self, horizon_tx_response: dict): + """ + :param horizon_tx_response: the json response from an horizon query + """ + # Network_id is left as '' since we override the hash anyway + self.tx = decode_transaction(horizon_tx_response['envelope_xdr'], network_id='', simple=False) + self.timestamp = horizon_tx_response['created_at'] + self.hash = horizon_tx_response['hash'] + + class SimplifiedTransaction: """Class to hold simplified info about a transaction""" - def __init__(self, raw_tx): + def __init__(self, raw_tx: RawTransaction): self.id = raw_tx.hash self.timestamp = raw_tx.timestamp @@ -45,7 +57,7 @@ def __init__(self, raw_tx): class SimplifiedOperation: """Class to hold simplified info about a operation""" - def __init__(self, op_data): + def __init__(self, op_data: Union[CreateAccount, Payment]): if isinstance(op_data, Payment): # Raise error if its not a KIN payment if op_data.asset.type != NATIVE_ASSET_TYPE: @@ -64,18 +76,6 @@ def __init__(self, op_data): raise CantSimplifyError('Cant simplify operation with {} operation'.format(op_data.type)) -class RawTransaction: - """Class to hold raw info about a transaction""" - def __init__(self, horizon_tx_response): - """ - :param dict horizon_tx_response: the json response from an horizon query - """ - # Network_id is left as '' since we override the hash anyway - self.tx = decode_transaction(horizon_tx_response['envelope_xdr'], network_id='', simple=False) - self.timestamp = horizon_tx_response['created_at'] - self.hash = horizon_tx_response['hash'] - - class OperationTypes(Enum): """Possible operation types for a simple operation""" @@ -83,13 +83,12 @@ class OperationTypes(Enum): CREATE_ACCOUNT = 2 -def build_memo(app_id, memo): +def build_memo(app_id: str, memo: Union[str, None]) -> str: """ Build a memo for a tx that fits the pre-defined template - :param str app_id: The app_id to include in the memo - :param str memo: The memo to include + :param app_id: The app_id to include in the memo + :param memo: The memo to include :return: the finished memo - :rtype: str """ finished_memo = MEMO_TEMPLATE.format(app_id) if memo is not None: @@ -98,14 +97,14 @@ def build_memo(app_id, memo): return finished_memo -def decode_transaction(b64_tx, network_id, simple=True): +def decode_transaction(b64_tx: str, network_id: str, simple: Optional[bool] = True) -> Union[SimplifiedTransaction, RawTransaction]: """ Decode a base64 transaction envelop - :param str b64_tx: a transaction envelop encoded in base64 - :param boolean simple: should the tx be simplified - :param str network_id: the network_id for the transaction + :param b64_tx: a transaction envelop encoded in base64 + :param simple: should the tx be simplified + :param network_id: the network_id for the transaction :return: The transaction - :rtype kin.transactions.SimplifiedTransaction | kin_base.Transaction + :raises: KinErrors.CantSimplifyError: if the tx cannot be simplified """ unpacker = Xdr.StellarXDRUnpacker(base64.b64decode(b64_tx)) @@ -123,7 +122,7 @@ def decode_transaction(b64_tx, network_id, simple=True): return envelop.tx -def calculate_tx_hash(tx, network_passphrase_hash): +def calculate_tx_hash(tx: BaseTransaction, network_passphrase_hash: bytes) -> str: """ Calculate a tx hash. @@ -133,7 +132,7 @@ def calculate_tx_hash(tx, network_passphrase_hash): 3. The xdr representation of the transaction :param tx: The builder's transaction object :param network_passphrase_hash: The network passphrase hash - :return: + :return: The hex encoded transaction hash """ # Pack the transaction to xdr packer = Xdr.StellarXDRPacker() From ef83c6c00d49e30a9b4fa3bf3f2cec9c1f3817d0 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Mon, 18 Mar 2019 13:33:07 +0200 Subject: [PATCH 05/22] Add tests to async functionality --- Pipfile | 16 +- Pipfile.lock | 133 +++++++------- kin/account.py | 20 +-- kin/client.py | 6 +- kin/errors.py | 4 +- requirements-dev.txt | 4 - requirements.txt | 2 +- test/conftest.py | 20 ++- test/test_account.py | 54 +++--- test/test_builder.py | 96 ----------- test/test_channel_manager.py | 32 ++-- test/test_client.py | 97 ++++++----- test/test_errors.py | 6 - test/test_horizon.py | 325 ----------------------------------- test/test_monitor.py | 81 +-------- 15 files changed, 200 insertions(+), 696 deletions(-) delete mode 100644 test/test_builder.py delete mode 100644 test/test_horizon.py diff --git a/Pipfile b/Pipfile index fd81d65..a6d6a61 100644 --- a/Pipfile +++ b/Pipfile @@ -7,18 +7,12 @@ name = "pypi" python_version = "3.7" [packages] -schematics = "==2.0.1" +schematics = "==2.1.*" kin-base = {ref = "async-kin", git = "https://github.com/kinecosystem/py-kin-base"} [dev-packages] -attrs = "==17.4.0" -codecov = "==2.0.15" -coverage = "==4.5.1" -funcsigs = "==1.0.2" -pluggy = "==0.6.0" -py = "==1.5.2" -pytest = "==3.4.0" -pytest-cov = "==2.5.1" +codecov = "*" +coverage = "*" +pytest = "*" +pytest-cov = "*" pytest-asyncio = "*" -#[pipenv] -#keep_outdated = true diff --git a/Pipfile.lock b/Pipfile.lock index 31b88b2..111efff 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "14fbb77daa8dd5fd4ce6010ebeba639708d0e1e1ff78ea9743342006cd243b03" + "sha256": "9682530ca93d3be6dd87473c2ca99f9e3a29ddb027316f051ef39db5a7271e21" }, "pipfile-spec": 6, "requires": { @@ -18,25 +18,31 @@ "default": { "kin-base": { "git": "https://github.com/kinecosystem/py-kin-base", - "ref": "f26ad2d2d612f66a3298b42aef43652b157624b4" + "ref": "6bf51c030bea5bae982fd36167d9c0375de5782f" }, "schematics": { "hashes": [ - "sha256:d9798a9ba0e1e1f2bde4a15780baa95ed66f748fa52d22bb89893d66ad0fac55", - "sha256:eaecac4ae5a86faa111f16befa26510bc66dc093c52df200f3aad54459e39640" + "sha256:8fcc6182606fd0b24410a1dbb066d9bbddbe8da9c9509f47b743495706239283", + "sha256:a40b20635c0e43d18d3aff76220f6cd95ea4decb3f37765e49529b17d81b0439" ], "index": "pypi", - "version": "==2.0.1" + "version": "==2.1.0" } }, "develop": { + "atomicwrites": { + "hashes": [ + "sha256:03472c30eb2c5d1ba9227e4c2ca66ab8287fbfbbda3888aa93dc2e28fc6811b4", + "sha256:75a9445bac02d8d058d5e1fe689654ba5a6556a1dfd8ce6ec55a0ed79866cfa6" + ], + "version": "==1.3.0" + }, "attrs": { "hashes": [ - "sha256:1c7960ccfd6a005cd9f7ba884e6316b5e430a3f1a6c37c5f87d8b43f83b54ec9", - "sha256:a17a9573a6f475c99b551c0e0a812707ddda1ec9653bed04c13841404ed6f450" + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" ], - "index": "pypi", - "version": "==17.4.0" + "version": "==19.1.0" }, "certifi": { "hashes": [ @@ -62,50 +68,40 @@ }, "coverage": { "hashes": [ - "sha256:03481e81d558d30d230bc12999e3edffe392d244349a90f4ef9b88425fac74ba", - "sha256:0b136648de27201056c1869a6c0d4e23f464750fd9a9ba9750b8336a244429ed", - "sha256:0bf8cbbd71adfff0ef1f3a1531e6402d13b7b01ac50a79c97ca15f030dba6306", - "sha256:10a46017fef60e16694a30627319f38a2b9b52e90182dddb6e37dcdab0f4bf95", - "sha256:198626739a79b09fa0a2f06e083ffd12eb55449b5f8bfdbeed1df4910b2ca640", - "sha256:23d341cdd4a0371820eb2b0bd6b88f5003a7438bbedb33688cd33b8eae59affd", - "sha256:28b2191e7283f4f3568962e373b47ef7f0392993bb6660d079c62bd50fe9d162", - "sha256:2a5b73210bad5279ddb558d9a2bfedc7f4bf6ad7f3c988641d83c40293deaec1", - "sha256:2eb564bbf7816a9d68dd3369a510be3327f1c618d2357fa6b1216994c2e3d508", - "sha256:337ded681dd2ef9ca04ef5d93cfc87e52e09db2594c296b4a0a3662cb1b41249", - "sha256:3a2184c6d797a125dca8367878d3b9a178b6fdd05fdc2d35d758c3006a1cd694", - "sha256:3c79a6f7b95751cdebcd9037e4d06f8d5a9b60e4ed0cd231342aa8ad7124882a", - "sha256:3d72c20bd105022d29b14a7d628462ebdc61de2f303322c0212a054352f3b287", - "sha256:3eb42bf89a6be7deb64116dd1cc4b08171734d721e7a7e57ad64cc4ef29ed2f1", - "sha256:4635a184d0bbe537aa185a34193898eee409332a8ccb27eea36f262566585000", - "sha256:56e448f051a201c5ebbaa86a5efd0ca90d327204d8b059ab25ad0f35fbfd79f1", - "sha256:5a13ea7911ff5e1796b6d5e4fbbf6952381a611209b736d48e675c2756f3f74e", - "sha256:69bf008a06b76619d3c3f3b1983f5145c75a305a0fea513aca094cae5c40a8f5", - "sha256:6bc583dc18d5979dc0f6cec26a8603129de0304d5ae1f17e57a12834e7235062", - "sha256:701cd6093d63e6b8ad7009d8a92425428bc4d6e7ab8d75efbb665c806c1d79ba", - "sha256:7608a3dd5d73cb06c531b8925e0ef8d3de31fed2544a7de6c63960a1e73ea4bc", - "sha256:76ecd006d1d8f739430ec50cc872889af1f9c1b6b8f48e29941814b09b0fd3cc", - "sha256:7aa36d2b844a3e4a4b356708d79fd2c260281a7390d678a10b91ca595ddc9e99", - "sha256:7d3f553904b0c5c016d1dad058a7554c7ac4c91a789fca496e7d8347ad040653", - "sha256:7e1fe19bd6dce69d9fd159d8e4a80a8f52101380d5d3a4d374b6d3eae0e5de9c", - "sha256:8c3cb8c35ec4d9506979b4cf90ee9918bc2e49f84189d9bf5c36c0c1119c6558", - "sha256:9d6dd10d49e01571bf6e147d3b505141ffc093a06756c60b053a859cb2128b1f", - "sha256:be6cfcd8053d13f5f5eeb284aa8a814220c3da1b0078fa859011c7fffd86dab9", - "sha256:c1bb572fab8208c400adaf06a8133ac0712179a334c09224fb11393e920abcdd", - "sha256:de4418dadaa1c01d497e539210cb6baa015965526ff5afc078c57ca69160108d", - "sha256:e05cb4d9aad6233d67e0541caa7e511fa4047ed7750ec2510d466e806e0255d6", - "sha256:f05a636b4564104120111800021a92e43397bc12a5c72fed7036be8556e0029e", - "sha256:f3f501f345f24383c0000395b26b726e46758b71393267aeae0bd36f8b3ade80" + "sha256:3684fabf6b87a369017756b551cef29e505cb155ddb892a7a29277b978da88b9", + "sha256:39e088da9b284f1bd17c750ac672103779f7954ce6125fd4382134ac8d152d74", + "sha256:3c205bc11cc4fcc57b761c2da73b9b72a59f8d5ca89979afb0c1c6f9e53c7390", + "sha256:465ce53a8c0f3a7950dfb836438442f833cf6663d407f37d8c52fe7b6e56d7e8", + "sha256:48020e343fc40f72a442c8a1334284620f81295256a6b6ca6d8aa1350c763bbe", + "sha256:5296fc86ab612ec12394565c500b412a43b328b3907c0d14358950d06fd83baf", + "sha256:5f61bed2f7d9b6a9ab935150a6b23d7f84b8055524e7be7715b6513f3328138e", + "sha256:68a43a9f9f83693ce0414d17e019daee7ab3f7113a70c79a3dd4c2f704e4d741", + "sha256:6b8033d47fe22506856fe450470ccb1d8ba1ffb8463494a15cfc96392a288c09", + "sha256:7ad7536066b28863e5835e8cfeaa794b7fe352d99a8cded9f43d1161be8e9fbd", + "sha256:7bacb89ccf4bedb30b277e96e4cc68cd1369ca6841bde7b005191b54d3dd1034", + "sha256:839dc7c36501254e14331bcb98b27002aa415e4af7ea039d9009409b9d2d5420", + "sha256:8f9a95b66969cdea53ec992ecea5406c5bd99c9221f539bca1e8406b200ae98c", + "sha256:932c03d2d565f75961ba1d3cec41ddde00e162c5b46d03f7423edcb807734eab", + "sha256:988529edadc49039d205e0aa6ce049c5ccda4acb2d6c3c5c550c17e8c02c05ba", + "sha256:998d7e73548fe395eeb294495a04d38942edb66d1fa61eb70418871bc621227e", + "sha256:9de60893fb447d1e797f6bf08fdf0dbcda0c1e34c1b06c92bd3a363c0ea8c609", + "sha256:9e80d45d0c7fcee54e22771db7f1b0b126fb4a6c0a2e5afa72f66827207ff2f2", + "sha256:a545a3dfe5082dc8e8c3eb7f8a2cf4f2870902ff1860bd99b6198cfd1f9d1f49", + "sha256:a5d8f29e5ec661143621a8f4de51adfb300d7a476224156a39a392254f70687b", + "sha256:aca06bfba4759bbdb09bf52ebb15ae20268ee1f6747417837926fae990ebc41d", + "sha256:bb23b7a6fd666e551a3094ab896a57809e010059540ad20acbeec03a154224ce", + "sha256:bfd1d0ae7e292105f29d7deaa9d8f2916ed8553ab9d5f39ec65bcf5deadff3f9", + "sha256:c62ca0a38958f541a73cf86acdab020c2091631c137bd359c4f5bddde7b75fd4", + "sha256:c709d8bda72cf4cd348ccec2a4881f2c5848fd72903c185f363d361b2737f773", + "sha256:c968a6aa7e0b56ecbd28531ddf439c2ec103610d3e2bf3b75b813304f8cb7723", + "sha256:df785d8cb80539d0b55fd47183264b7002077859028dfe3070cf6359bf8b2d9c", + "sha256:f406628ca51e0ae90ae76ea8398677a921b36f0bd71aab2099dfed08abd0322f", + "sha256:f46087bbd95ebae244a0eda01a618aff11ec7a069b15a3ef8f6b520db523dcf1", + "sha256:f8019c5279eb32360ca03e9fac40a12667715546eed5c5eb59eb381f2f501260", + "sha256:fc5f4d209733750afd2714e9109816a29500718b32dd9a5db01c0cb3a019b96a" ], "index": "pypi", - "version": "==4.5.1" - }, - "funcsigs": { - "hashes": [ - "sha256:330cc27ccbf7f1e992e69fef78261dc7c6569012cf397db8d3de0234e6c937ca", - "sha256:a7bb0f2cf3a3fd1ab2732cb49eba4252c2af4240442415b4abce3b87022a8f50" - ], - "index": "pypi", - "version": "==1.0.2" + "version": "==4.5.3" }, "idna": { "hashes": [ @@ -114,30 +110,35 @@ ], "version": "==2.8" }, + "more-itertools": { + "hashes": [ + "sha256:0125e8f60e9e031347105eb1682cef932f5e97d7b9a1a28d9bf00c22a5daef40", + "sha256:590044e3942351a1bdb1de960b739ff4ce277960f2425ad4509446dbace8d9d1" + ], + "markers": "python_version > '2.7'", + "version": "==6.0.0" + }, "pluggy": { "hashes": [ - "sha256:7f8ae7f5bdf75671a718d2daf0a64b7885f74510bcd98b1a0bb420eb9a9d0cff", - "sha256:d345c8fe681115900d6da8d048ba67c25df42973bda370783cd58826442dcd7c", - "sha256:e160a7fcf25762bb60efc7e171d4497ff1d8d2d75a3d0df7a21b76821ecbf5c5" + "sha256:19ecf9ce9db2fce065a7a0586e07cfb4ac8614fe96edf628a264b1c70116cf8f", + "sha256:84d306a647cc805219916e62aab89caa97a33a1dd8c342e87a37f91073cd4746" ], - "index": "pypi", - "version": "==0.6.0" + "version": "==0.9.0" }, "py": { "hashes": [ - "sha256:8cca5c229d225f8c1e3085be4fcf306090b00850fefad892f9d96c7b6e2f310f", - "sha256:ca18943e28235417756316bfada6cd96b23ce60dd532642690dcfdaba988a76d" + "sha256:64f65755aee5b381cea27766a3a147c3f15b9b6b9ac88676de66ba2ae36793fa", + "sha256:dc639b046a6e2cff5bbe40194ad65936d6ba360b52b3c3fe1d08a82dd50b5e53" ], - "index": "pypi", - "version": "==1.5.2" + "version": "==1.8.0" }, "pytest": { "hashes": [ - "sha256:6074ea3b9c999bd6d0df5fa9d12dd95ccd23550df2a582f5f5b848331d2e82ca", - "sha256:95fa025cd6deb5d937e04e368a00552332b58cae23f63b76c8c540ff1733ab6d" + "sha256:592eaa2c33fae68c7d75aacf042efc9f77b27c08a6224a4f59beab8d9a420523", + "sha256:ad3ad5c450284819ecde191a654c09b0ec72257a2c711b9633d677c71c9850c4" ], "index": "pypi", - "version": "==3.4.0" + "version": "==4.3.1" }, "pytest-asyncio": { "hashes": [ @@ -149,11 +150,11 @@ }, "pytest-cov": { "hashes": [ - "sha256:03aa752cf11db41d281ea1d807d954c4eda35cfa1b21d6971966cc041bbf6e2d", - "sha256:890fe5565400902b0c78b5357004aab1c814115894f4f21370e2433256a3eeec" + "sha256:0ab664b25c6aa9716cbf203b17ddb301932383046082c081b9848a0edf5add33", + "sha256:230ef817450ab0699c6cc3c9c8f7a829c34674456f2ed8df1fe1d39780f7c87f" ], "index": "pypi", - "version": "==2.5.1" + "version": "==2.6.1" }, "requests": { "hashes": [ diff --git a/kin/account.py b/kin/account.py index f884124..20d10d9 100644 --- a/kin/account.py +++ b/kin/account.py @@ -16,7 +16,7 @@ from .blockchain.utils import is_valid_address, is_valid_secret_key from .blockchain.horizon_models import AccountData -from typing import List, Optional, Union +from typing import List, Optional, Union, AsyncGenerator import logging logger = logging.getLogger(__name__) @@ -241,9 +241,9 @@ async def submit_transaction(self, tx_builder: Builder) -> str: if e.type == HorizonErrorType.TRANSACTION_FAILED \ and e.extras['result_codes']['transaction'] == TransactionResultCode.INSUFFICIENT_BALANCE: - self.channel_manager.channel_pool.queue[tx_builder.address] = ChannelStatuses.UNDERFUNDED + self.channel_manager.channel_pool._queue[tx_builder.address] = ChannelStatuses.UNDERFUNDED await self._top_up(tx_builder.address) - self.channel_manager.channel_pool.queue[tx_builder.address] = ChannelStatuses.TAKEN + self.channel_manager.channel_pool._queue[tx_builder.address] = ChannelStatuses.TAKEN # Insufficient balance is a "fast-fail", the sequence number doesn't increment # so there is no need to build the transaction again @@ -251,18 +251,14 @@ async def submit_transaction(self, tx_builder: Builder) -> str: else: raise KinErrors.translate_error(e) - # TODO: asyncify - def monitor_payments(self, callback_fn): + def monitor_payments(self, timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: """Monitor KIN payment transactions related to this account - NOTE: the function starts a background thread. + :param timeout: How long to wait for each event - :param callback_fn: the function to call on each received payment as `callback_fn(address, tx_data, monitor)`. - :type: callable[str,kin.transactions.SimplifiedTransaction,kin.monitors.SingleMonitor] - - :return: a monitor instance - :rtype: kin.monitors.SingleMonitor + :raises: ValueError: if the address is in the wrong format + :raises: asyncio.TimeoutError: If too much time has passed between events (only if "timeout" is set) """ - return self._client.monitor_account_payments(self.keypair.public_address, callback_fn) + return self._client.monitor_account_payments(self.keypair.public_address, timeout) def whitelist_transaction(self, payload: Union[str, dict]) -> str: """ diff --git a/kin/client.py b/kin/client.py index 9fd9751..59583f3 100644 --- a/kin/client.py +++ b/kin/client.py @@ -68,7 +68,7 @@ async def get_config(self) -> dict: 'sdk_version': __version__, 'environment': self.environment.name, 'horizon': { - 'uri': self.horizon.horizon_uri, + 'uri': str(self.horizon.horizon_uri), 'online': False, 'error': None, }, @@ -85,7 +85,7 @@ async def get_config(self) -> dict: await self.horizon.metrics() status['horizon']['online'] = True except Exception as e: - status['horizon']['error'] = str(e) + status['horizon']['error'] = repr(e) return status @@ -246,7 +246,7 @@ async def friendbot(self, address: str): if not is_valid_address(address): raise KinErrors.StellarAddressInvalidError('invalid address: {}'.format(address)) - if self.does_account_exists(address): + if await self.does_account_exists(address): raise KinErrors.AccountExistsError(address) response = await self.horizon._session.get(self.environment.friendbot_url, params={'addr': address}) diff --git a/kin/errors.py b/kin/errors.py index 83179a3..bfbda06 100644 --- a/kin/errors.py +++ b/kin/errors.py @@ -141,8 +141,6 @@ def translate_error(err): """A high-level error translator.""" if isinstance(err, ClientError): return NetworkError({'internal_error': str(err)}) - if isinstance(err, ChannelsBusyError): - return ThrottleError if isinstance(err, HorizonError): return translate_horizon_error(err) return InternalError(None, {'internal_error': str(err)}) @@ -152,7 +150,7 @@ def translate_horizon_error(horizon_error): """Horizon error translator.""" # query errors if horizon_error.type == HorizonErrorType.BAD_REQUEST: - return RequestError(horizon_error.type, {'invalid_field': horizon_error.extras.invalid_field}) + return RequestError(horizon_error.type, {'invalid_field': horizon_error.extras.get('invalid_field')}) if horizon_error.type == HorizonErrorType.NOT_FOUND: return ResourceNotFoundError(horizon_error.type) if horizon_error.type in [HorizonErrorType.FORBIDDEN, diff --git a/requirements-dev.txt b/requirements-dev.txt index 77be61b..7562b4f 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,9 +1,5 @@ -attrs==17.4.0 codecov==2.0.15 coverage==4.5.1 -funcsigs==1.0.2 -pluggy==0.6.0 -py==1.5.2 pytest-cov==2.5.1 pytest==3.4.0 pytest-asyncio==0.10.0 diff --git a/requirements.txt b/requirements.txt index 34f670b..e29c1d0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ kin-base==1.1.0 -schematics==2.0.1 +schematics==2.1.* diff --git a/test/conftest.py b/test/conftest.py index eb8653d..65112ad 100644 --- a/test/conftest.py +++ b/test/conftest.py @@ -1,5 +1,6 @@ import pytest +import asyncio from kin import Environment, KinClient import logging @@ -27,21 +28,30 @@ def __init__(self, **entries): self.__dict__.update(entries) environment=docker_environment) -@pytest.fixture(scope='session') -def test_client(setup): +@pytest.yield_fixture(scope='session') +async def test_client(setup): # Create a base KinClient print('Created a base KinClient') - return KinClient(setup.environment) + client = KinClient(setup.environment) + yield client + await client.__aexit__(None, None, None) @pytest.fixture(scope='session') -def test_account(setup, test_client): +async def test_account(setup, test_client): # Create and fund the sdk account from the root account sdk_address = 'GAIDUTTQ5UIZDW7VZ2S3ZAFLY6LCRT5ZVHF5X3HDJVDQ4OJWYGJVJDZB' sdk_seed = 'SBKI7MEF62NHHH3AOXBHII46K2FD3LVH63FYHUDLTBUYT3II6RAFLZ7B' root_account = test_client.kin_account(setup.issuer_seed) - root_account.create_account(sdk_address, 10000 + 1000000, fee=100) + await root_account.create_account(sdk_address, 10000 + 1000000, fee=100) print('Created the base kin account') return test_client.kin_account(sdk_seed) + + +@pytest.yield_fixture(scope='session') +def event_loop(): + loop = asyncio.get_event_loop_policy().new_event_loop() + yield loop + loop.close() diff --git a/test/test_account.py b/test/test_account.py index 1de7ef6..90f1ae0 100644 --- a/test/test_account.py +++ b/test/test_account.py @@ -8,9 +8,6 @@ def test_create_basic(test_client, test_account): - with pytest.raises(KinErrors.AccountNotFoundError): - account = test_client.kin_account('SD6IDZHCMX3Z4QPDIC33PECKLLY572DAA5S3DZDALEVVACJKSZPVPJC6') - with pytest.raises(KinErrors.StellarSecretInvalidError): account = test_client.kin_account('bad format') @@ -18,7 +15,7 @@ def test_create_basic(test_client, test_account): assert account assert account.keypair.secret_seed == SDK_SEED assert account.keypair.public_address == SDK_PUBLIC - assert account.horizon + assert account._client is test_client assert account.channel_manager @@ -26,22 +23,24 @@ def test_get_address(test_client, test_account): assert test_account.get_public_address() == SDK_PUBLIC -def test_create_account(setup, test_client, test_account): +@pytest.mark.asyncio +async def test_create_account(setup, test_client, test_account): with pytest.raises(KinErrors.AccountExistsError): - test_account.create_account(setup.issuer_address, 0, fee=100) + await test_account.create_account(setup.issuer_address, 0, fee=100) - test_account.create_account('GDN7KB72OO7G6VBD3CXNRFXVELLW6F36PS42N7ASZHODV7Q5GYPETQ74', 0, fee=100) - assert test_client.does_account_exists('GDN7KB72OO7G6VBD3CXNRFXVELLW6F36PS42N7ASZHODV7Q5GYPETQ74') + await test_account.create_account('GDN7KB72OO7G6VBD3CXNRFXVELLW6F36PS42N7ASZHODV7Q5GYPETQ74', 0, fee=100) + assert await test_client.does_account_exists('GDN7KB72OO7G6VBD3CXNRFXVELLW6F36PS42N7ASZHODV7Q5GYPETQ74') -def test_send_kin(test_client, test_account): +@pytest.mark.asyncio +async def test_send_kin(test_client, test_account): recipient = 'GBZWWLRJRWL4DLYOJMCHXJUOJJY5NLNJHQDRQHVQH43KFCPC3LEOWPYM' - test_client.friendbot(recipient) + await test_client.friendbot(recipient) - test_account.send_kin(recipient, 10, fee=100) - balance = test_client.get_account_balance(recipient) + await test_account.send_kin(recipient, 10, fee=100) + balance = await test_client.get_account_balance(recipient) with pytest.raises(KinErrors.NotValidParamError): - test_account.send_kin(recipient, 1.1234567898765, fee=100) + await test_account.send_kin(recipient, 1.1234567898765, fee=100) assert balance == 10 @@ -56,9 +55,9 @@ def test_build_create_account(test_account): builder = test_account.build_create_account(recipient, starting_balance=10, fee=100) - assert builder + def test_build_send_kin(test_account): recipient = 'GBZWWLRJRWL4DLYOJMCHXJUOJJY5NLNJHQDRQHVQH43KFCPC3LEOWPYM' with pytest.raises(KinErrors.StellarAddressInvalidError): @@ -73,44 +72,47 @@ def test_build_send_kin(test_account): assert builder -def test_auto_top_up(test_client, test_account): +@pytest.mark.asyncio +async def test_auto_top_up(test_client, test_account): channel = 'SBYU2EBGTTGIFR4O4K4SQXTD4ISMVX4R5TX2TTB4SWVIA5WVRS2MHN4K' public = 'GBKZAXTDJRYBK347KDTOFWEBDR7OW3U67XV2BOF2NLBNEGRQ2WN6HFK6' - test_account.create_account(public, 0, fee=100) + await test_account.create_account(public, 0, fee=100) account = test_client.kin_account(test_account.keypair.secret_seed, channel_secret_keys=[channel]) - account.send_kin(public, 10, fee=100) + await account.send_kin(public, 10, fee=100) - channel_balance = test_client.get_account_balance(public) + channel_balance = await test_client.get_account_balance(public) # channel should have ran out of funds, so the base account should have topped it up assert channel_balance > 0 -def test_memo(test_client, test_account): +@pytest.mark.asyncio +async def test_memo(test_client, test_account): recipient1 = 'GCT3YLKNVEILHUOZYK3QPOVZWWVLF5AE5D24Y6I4VH7WGZYBFU2HSXYX' recipient2 = 'GDR375ZLWHZUFH2SWXFEH7WVPK5G3EQBLXPZKYEFJ5EAW4WE4WIQ5BP3' - tx1 = test_account.create_account(recipient1, 0, memo_text='Hello', fee=100) + tx1 = await test_account.create_account(recipient1, 0, memo_text='Hello', fee=100) account2 = test_client.kin_account(test_account.keypair.secret_seed, app_id='test') - tx2 = account2.create_account(recipient2, 0, memo_text='Hello', fee=100) + tx2 = await account2.create_account(recipient2, 0, memo_text='Hello', fee=100) sleep(5) - tx1_data = test_client.get_transaction_data(tx1) - tx2_data = test_client.get_transaction_data(tx2) + tx1_data = await test_client.get_transaction_data(tx1) + tx2_data = await test_client.get_transaction_data(tx2) assert tx1_data.memo == MEMO_TEMPLATE.format(ANON_APP_ID) + 'Hello' assert tx2_data.memo == MEMO_TEMPLATE.format('test') + 'Hello' with pytest.raises(KinErrors.NotValidParamError): - account2.create_account(recipient2, 0, memo_text='a'*25, fee=100) + await account2.create_account(recipient2, 0, memo_text='a'*25, fee=100) + def test_get_transaction_builder(test_account): builder = test_account.get_transaction_builder(fee=100) assert builder assert builder.address == test_account.get_public_address() assert builder.fee == 100 - assert builder.horizon is test_account.horizon - assert builder.network == test_account._client.environment.name + assert builder.horizon is test_account._client.horizon + assert builder.network_name == test_account._client.environment.name def test_whitelist_transaction(test_account): diff --git a/test/test_builder.py b/test/test_builder.py deleted file mode 100644 index 35519ae..0000000 --- a/test/test_builder.py +++ /dev/null @@ -1,96 +0,0 @@ -import pytest -import time - -from kin.blockchain.builder import Builder -from kin.blockchain.horizon import Horizon -from kin import KinErrors - - -def test_create_fail(): - with pytest.raises(KinErrors.StellarSecretInvalidError): - Builder(secret='bad', network_name=None, horizon=None, fee=100) - - -def test_create(): - seed = 'SASKOJJOG7MLXAWJGE6QNCWH5ZIBH5LWQCXPRGDHUKUOB4RBRWXXFZ2T' - address = 'GCAZ7QXD6UJ5NOVWYTNKLNP36DPJZMRO67LQ4X5CH2IHY3OG5QGECGYQ' - - # with secret - builder = Builder(secret=seed, network_name=None, horizon=None, fee=100) - assert builder - assert builder.keypair.seed().decode() == seed - assert builder.address == address - - -def test_create_custom(test_client): - seed = 'SASKOJJOG7MLXAWJGE6QNCWH5ZIBH5LWQCXPRGDHUKUOB4RBRWXXFZ2T' - address = 'GCAZ7QXD6UJ5NOVWYTNKLNP36DPJZMRO67LQ4X5CH2IHY3OG5QGECGYQ' - - horizon = Horizon() - builder = Builder(secret=seed, horizon=horizon, network_name='custom', fee=100) - assert builder - assert builder.horizon - assert builder.network == 'custom' - assert builder - assert builder.horizon == horizon - - # with horizon fixture - builder = Builder(secret=seed, - horizon=test_client.horizon, - network_name=test_client.environment.name, fee=100) - assert builder - - -@pytest.fixture(scope='session') -def test_builder(test_client, test_account): - builder = test_account.get_transaction_builder(100) - return builder - - -def test_sign(test_builder): - address = 'GCAZ7QXD6UJ5NOVWYTNKLNP36DPJZMRO67LQ4X5CH2IHY3OG5QGECGYQ' - - test_builder.append_create_account_op(address, '100') - assert len(test_builder.ops) == 1 - test_builder.sign() - assert test_builder.te - assert test_builder.tx - - -def test_clear(test_builder): - test_builder.clear() - assert len(test_builder.ops) == 0 - assert not test_builder.te - assert not test_builder.tx - - -def test_get_sequence(test_builder): - assert test_builder.get_sequence() - - -def test_update_sequence(test_builder): - test_builder.update_sequence() - # TODO: remove str() after kin-base is fixed - assert test_builder.sequence == str(test_builder.get_sequence()) - - -def test_set_channel(test_client, test_builder): - channel_addr = 'GBC6PXY4ZSO356NUPF2A2SDVEBQB2RG7XN6337NBW4F24APGHEVR3IIU' - channel = 'SA4BHY26Q3C3BSYKKGDM7UVMZ4YF6YBLX6AOWYEDBXPLOR7WQ5EJXN6X' - test_client.friendbot(channel_addr) - time.sleep(5) - test_builder.set_channel(channel) - assert test_builder.address == channel_addr - assert test_builder.sequence == str(test_builder.get_sequence()) - - -def test_next(test_builder): - address = 'GCAZ7QXD6UJ5NOVWYTNKLNP36DPJZMRO67LQ4X5CH2IHY3OG5QGECGYQ' - - sequence = test_builder.get_sequence() - test_builder.append_create_account_op(address, '100') - test_builder.sign() - test_builder.next() - assert not test_builder.tx - assert not test_builder.te - assert test_builder.sequence == str(int(sequence) + 1) diff --git a/test/test_channel_manager.py b/test/test_channel_manager.py index f57ea0f..ee3d125 100644 --- a/test/test_channel_manager.py +++ b/test/test_channel_manager.py @@ -1,5 +1,4 @@ import pytest -from kin import KinErrors from kin.blockchain.channel_manager import ChannelManager, ChannelStatuses, ChannelPool @@ -18,17 +17,18 @@ def test_create_channel_manager(): def test_create_channel_pool(): pool = ChannelPool(channels) - assert len(channels) == len(pool.queue) + assert len(channels) == len(pool._queue) for channel in channels: - assert pool.queue[channel] == ChannelStatuses.FREE + assert pool._queue[channel] == ChannelStatuses.FREE -def test_pool_get_and_put(): +@pytest.mark.asyncio +async def test_pool_get_and_put(): pool = ChannelPool(channels) - channel = pool.get() - assert pool.queue[channel] == ChannelStatuses.TAKEN - pool.put(channel) - assert pool.queue[channel] == ChannelStatuses.FREE + channel = await pool.get() + assert pool._queue[channel] == ChannelStatuses.TAKEN + await pool.put(channel) + assert pool._queue[channel] == ChannelStatuses.FREE def test_q_size(): @@ -36,16 +36,22 @@ def test_q_size(): assert pool.qsize() == 5 +def test_empty(): + pool = ChannelPool([]) + assert pool.empty() + + def test_get_available_channels(): pool = ChannelPool(channels) free_channels = pool.get_free_channels() assert len(free_channels) == 5 for channel in free_channels: - assert pool.queue[channel] == ChannelStatuses.FREE + assert pool._queue[channel] == ChannelStatuses.FREE -def test_get_channel(): +@pytest.mark.asyncio +async def test_get_channel(): manager = ChannelManager(channels) - with manager.get_channel() as channel: - assert manager.channel_pool.queue[channel] == ChannelStatuses.TAKEN - assert manager.channel_pool.queue[channel] == ChannelStatuses.FREE \ No newline at end of file + async with manager.get_channel() as channel: + assert manager.channel_pool._queue[channel] == ChannelStatuses.TAKEN + assert manager.channel_pool._queue[channel] == ChannelStatuses.FREE diff --git a/test/test_client.py b/test/test_client.py index 25374a8..1518f6b 100644 --- a/test/test_client.py +++ b/test/test_client.py @@ -13,28 +13,30 @@ def test_create(): assert client.network == TEST_ENVIRONMENT.name -def test_get_minimum_fee(test_client): - assert test_client.get_minimum_fee() == 100 +@pytest.mark.asyncio +async def test_get_minimum_fee(test_client): + assert await test_client.get_minimum_fee() == 100 -def test_get_config(setup, test_client): +@pytest.mark.asyncio +async def test_get_config(setup, test_client): from kin import Environment # bad Horizon endpoint env = Environment('bad', 'bad', 'bad', 'GDZA33STWFOVWLHAFXEOYS46DA2VMIQH3MCCVVGAUENMZMMZJFAHT4KO') - status = KinClient(env).get_config() + status = await KinClient(env).get_config() assert status['horizon'] assert status['horizon']['online'] is False - assert status['horizon']['error'].startswith("Invalid URL 'bad': No schema supplied") + assert 'InvalidURL' in status['horizon']['error'] # no Horizon on endpoint env = Environment('bad', 'http://localhost:666', 'bad', 'GDZA33STWFOVWLHAFXEOYS46DA2VMIQH3MCCVVGAUENMZMMZJFAHT4KO') - status = KinClient(env).get_config() + status = await KinClient(env).get_config() assert status['horizon'] assert status['horizon']['online'] is False - assert status['horizon']['error'].find('Connection refused') > 0 + assert 'ClientConnectorError' in status['horizon']['error'] # success - status = test_client.get_config() + status = await test_client.get_config() assert status['environment'] == setup.environment.name assert status['horizon'] assert status['horizon']['uri'] == setup.environment.horizon_uri @@ -44,34 +46,36 @@ def test_get_config(setup, test_client): assert status['transport']['pool_size'] assert status['transport']['num_retries'] assert status['transport']['request_timeout'] - assert status['transport']['retry_statuses'] assert status['transport']['backoff_factor'] -def test_get_balance(test_client, test_account): - balance = test_client.get_account_balance(test_account.get_public_address()) +@pytest.mark.asyncio +async def test_get_balance(test_client, test_account): + balance = await test_client.get_account_balance(test_account.get_public_address()) assert balance > 0 -def test_does_account_exists(test_client, test_account): +@pytest.mark.asyncio +async def test_does_account_exists(test_client, test_account): with pytest.raises(KinErrors.StellarAddressInvalidError): - test_client.does_account_exists('bad') + await test_client.does_account_exists('bad') address = 'GB7F23F7235ADJ7T2L4LJZT46LA3256QAXIU56ANKPX5LSAAS3XVA465' - assert not test_client.does_account_exists(address) - assert test_client.does_account_exists(test_account.get_public_address()) + assert not await test_client.does_account_exists(address) + assert await test_client.does_account_exists(test_account.get_public_address()) -def test_get_account_data(test_client, test_account): +@pytest.mark.asyncio +async def test_get_account_data(test_client, test_account): with pytest.raises(KinErrors.StellarAddressInvalidError): - test_client.get_account_data('bad') + await test_client.get_account_data('bad') address = 'GBSZO2C63WM2DHAH4XGCXDW5VGAM56FBIOGO2KFRSJYP5I4GGCPAVKHW' with pytest.raises(KinErrors.AccountNotFoundError): - test_client.get_account_data(address) + await test_client.get_account_data(address) - acc_data = test_client.get_account_data(test_account.get_public_address()) + acc_data = await test_client.get_account_data(test_account.get_public_address()) assert acc_data assert acc_data.id == test_account.get_public_address() assert acc_data.sequence @@ -95,20 +99,21 @@ def test_get_account_data(test_client, test_account): assert str(acc_data) -def test_get_transaction_data(setup, test_client): +@pytest.mark.asyncio +async def test_get_transaction_data(setup, test_client): from kin import OperationTypes from kin.transactions import RawTransaction with pytest.raises(ValueError): - test_client.get_transaction_data('bad') + await test_client.get_transaction_data('bad') with pytest.raises(KinErrors.ResourceNotFoundError): - test_client.get_transaction_data('deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef') + await test_client.get_transaction_data('deadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef') address = 'GAHTWFVYV4RF2AMEZP3X2VOK4HB3YOSARU7VNVTP7J2OLDSVOP564YEN' - tx_hash = test_client.friendbot(address) + tx_hash = await test_client.friendbot(address) sleep(5) - tx_data = test_client.get_transaction_data(tx_hash) + tx_data = await test_client.get_transaction_data(tx_hash) assert tx_data assert tx_data.id == tx_hash assert tx_data.timestamp @@ -119,42 +124,31 @@ def test_get_transaction_data(setup, test_client): assert tx_data.operation.destination == address assert tx_data.operation.starting_balance == 0 - tx_data = test_client.get_transaction_data(tx_hash, simple=False) + tx_data = await test_client.get_transaction_data(tx_hash, simple=False) assert isinstance(tx_data, RawTransaction) -def test_friendbot(test_client): +@pytest.mark.asyncio +async def test_friendbot(test_client): address = 'GDIPKVWPVCL5E5MX4UWMLCGXMDWEMEYAZGCI3TPJPVDG5ZFA6VJAA7RA' - test_client.friendbot(address) - assert test_client.does_account_exists(address) + await test_client.friendbot(address) + assert await test_client.does_account_exists(address) with pytest.raises(KinErrors.StellarAddressInvalidError): - test_client.friendbot('bad') + await test_client.friendbot('bad') -def test_verify_kin_payment(test_client, test_account): - address = 'GCZXR4ILXETTNQMUNF54ILRMPEG3UTUUMYKPUXU5633VCOABZZ63H7FJ' - tx_hash = test_client.friendbot(address) - sleep(5) - - assert not test_client.verify_kin_payment(tx_hash, 'source', 'destination', 123) - - tx_hash = test_account.send_kin('GCZXR4ILXETTNQMUNF54ILRMPEG3UTUUMYKPUXU5633VCOABZZ63H7FJ', 123, 100, 'Hello') - sleep(5) - assert test_client.verify_kin_payment(tx_hash, test_account.get_public_address(), address, 123) - assert test_client.verify_kin_payment(tx_hash, test_account.get_public_address(), address, 123, 'Hello', True) - - -def test_tx_history(test_client,test_account): +@pytest.mark.asyncio +async def test_tx_history(test_client,test_account): address = 'GA4GDLBEWVT5IZZ6JKR4BF3B6JJX5S6ISFC2QCC7B6ZVZWJDMR77HYP6' - test_client.friendbot(address) + await test_client.friendbot(address) txs = [] for _ in range(6): - txs.append(test_account.send_kin(address, 1, fee=100)) + txs.append(await test_account.send_kin(address, 1, fee=100)) # let horizon ingest the txs sleep(10) - tx_history = test_client.get_account_tx_history(test_account.get_public_address(), amount=6) + tx_history = await test_client.get_account_tx_history(test_account.get_public_address(), amount=6) history_ids = [tx.id for tx in tx_history] # tx history goes from latest to oldest @@ -162,10 +156,19 @@ def test_tx_history(test_client,test_account): assert txs == history_ids + # TODO: INCORRECT TESTING, broken # test paging config.MAX_RECORDS_PER_REQUEST = 2 - tx_history = test_client.get_account_tx_history(test_account.get_public_address(), amount=6) + tx_history = await test_client.get_account_tx_history(test_account.get_public_address(), amount=6) history_ids = [tx.id for tx in tx_history] assert txs == history_ids + + +@pytest.mark.asyncio +async def test_client_context(setup): + async with KinClient(TEST_ENVIRONMENT) as client: + context_channel = client + assert not context_channel.horizon._session.closed + assert context_channel.horizon._session.closed diff --git a/test/test_errors.py b/test/test_errors.py index cd0db11..402b21d 100644 --- a/test/test_errors.py +++ b/test/test_errors.py @@ -1,5 +1,3 @@ -from requests.exceptions import RequestException - from kin import KinErrors from kin.blockchain.errors import * @@ -13,10 +11,6 @@ def test_sdk_error(): def test_translate_error(): - e = KinErrors.translate_error(RequestException('error')) - assert isinstance(e, KinErrors.NetworkError) - assert e.extra['internal_error'] == 'error' - e = KinErrors.translate_error(Exception('error')) assert isinstance(e, KinErrors.InternalError) assert e.extra['internal_error'] == 'error' diff --git a/test/test_horizon.py b/test/test_horizon.py deleted file mode 100644 index b11e1d8..0000000 --- a/test/test_horizon.py +++ /dev/null @@ -1,325 +0,0 @@ -import pytest -from requests.adapters import DEFAULT_POOLSIZE - -from kin_base.horizon import HORIZON_TEST, HORIZON_LIVE -from kin.blockchain.errors import * -from kin.blockchain.horizon import ( - Horizon, - check_horizon_reply, - DEFAULT_REQUEST_TIMEOUT, - DEFAULT_NUM_RETRIES, - DEFAULT_BACKOFF_FACTOR, - USER_AGENT, -) - - -def test_check_horizon_reply(): - reply = { - 'type': HORIZON_NS_PREFIX + HorizonErrorType.TRANSACTION_FAILED, - 'status': 400, - 'title': 'title', - 'extras': { - 'result_codes': { - 'operations': [PaymentResultCode.NO_TRUST], - 'transaction': TransactionResultCode.FAILED - } - } - } - with pytest.raises(HorizonError) as exc_info: - check_horizon_reply(reply) - assert exc_info.value.type == HorizonErrorType.TRANSACTION_FAILED - - reply = "{'a':'b'}" - check_horizon_reply(reply) - - -def test_defaults(): - horizon = Horizon.testnet() - assert horizon - assert horizon.horizon_uri == HORIZON_TEST - - horizon = Horizon.livenet() - assert horizon - assert horizon.horizon_uri == HORIZON_LIVE - - -def test_create_default(): - horizon = Horizon() - assert horizon - assert horizon.horizon_uri == HORIZON_TEST - assert horizon.request_timeout == DEFAULT_REQUEST_TIMEOUT - assert horizon._session - assert horizon._session.headers['User-Agent'] == USER_AGENT - assert horizon._session.adapters['http://'] - assert horizon._session.adapters['https://'] - adapter = horizon._session.adapters['http://'] - assert adapter.max_retries - assert adapter.max_retries.total == DEFAULT_NUM_RETRIES - assert adapter.max_retries.backoff_factor == DEFAULT_BACKOFF_FACTOR - assert adapter.max_retries.redirect == 0 - assert adapter._pool_connections == DEFAULT_POOLSIZE - assert adapter._pool_maxsize == DEFAULT_POOLSIZE - - -def test_create_custom(): - horizon_uri = 'horizon_uri' - pool_size = 5 - num_retries = 10 - request_timeout = 30 - backoff_factor = 5 - horizon = Horizon(horizon_uri=horizon_uri, pool_size=pool_size, num_retries=num_retries, - request_timeout=request_timeout, backoff_factor=backoff_factor) - assert horizon - assert horizon.horizon_uri == horizon_uri - assert horizon.request_timeout == request_timeout - assert horizon._session.headers['User-Agent'] == USER_AGENT - adapter = horizon._session.adapters['http://'] - assert adapter.max_retries.total == num_retries - assert adapter.max_retries.backoff_factor == backoff_factor - assert adapter.max_retries.redirect == 0 - assert adapter._pool_connections == pool_size - assert adapter._pool_maxsize == pool_size - - -def test_account(test_client): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.account('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - # root blockchain address - address = 'GA3FLH3EVYHZUHTPQZU63JPX7ECJQL2XZFCMALPCLFYMSYC4JKVLAJWM' - reply = test_client.horizon.account(address) - assert reply - assert reply['id'] - - -def test_account_effects(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.account_effects('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - # root blockchain address - address = test_account.get_public_address() - reply = test_client.horizon.account_effects(address) - assert reply - assert reply['_embedded']['records'] - - -def test_account_offers(test_client): - # does not raise on nonexistent account! - - # root blockchain address - address = 'GA3FLH3EVYHZUHTPQZU63JPX7ECJQL2XZFCMALPCLFYMSYC4JKVLAJWM' - reply = test_client.horizon.account_offers(address) - assert reply - assert reply['_embedded'] - - -def test_account_operations(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.account_operations('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - address = test_account.get_public_address() - reply = test_client.horizon.account_operations(address) - assert reply - assert reply['_embedded']['records'] - - -def test_account_transactions(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.account_transactions('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - address = test_account.get_public_address() - reply = test_client.horizon.account_transactions(address) - assert reply - assert reply['_embedded']['records'] - - -def test_account_payments(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.account_payments('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - address = test_account.get_public_address() - reply = test_client.horizon.account_payments(address) - assert reply - assert reply['_embedded']['records'] - - -def test_transactions(test_client): - reply = test_client.horizon.transactions() - assert reply - assert reply['_embedded']['records'] - - -def get_first_tx_hash(test_client, test_account): - if not hasattr(test_account, 'first_tx_hash'): - address = test_account.get_public_address() - reply = test_client.horizon.account_transactions(address) - assert reply - tx = reply['_embedded']['records'][0] - assert tx['hash'] - test_account.first_tx_hash = tx['hash'] - return test_account.first_tx_hash - - -def test_transaction(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_account.horizon.transaction('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - tx_id = get_first_tx_hash(test_client, test_account) - reply = test_account.horizon.transaction(tx_id) - assert reply - assert reply['id'] == tx_id - - assert reply['operation_count'] == 1 - - -def test_transaction_effects(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.transaction_effects('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - tx_id = get_first_tx_hash(test_client, test_account) - reply = test_client.horizon.transaction_effects(tx_id) - assert reply - assert reply['_embedded']['records'] - - -def test_transaction_operations(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.transaction_operations('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - tx_id = get_first_tx_hash(test_client, test_account) - reply = test_client.horizon.transaction_operations(tx_id) - assert reply - assert reply['_embedded']['records'] - - -def test_transaction_payments(test_client, test_account): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.transaction_payments('bad') - assert exc_info.value.type == HorizonErrorType.NOT_FOUND - - tx_id = get_first_tx_hash(test_client, test_account) - reply = test_client.horizon.transaction_payments(tx_id) - assert reply - assert reply['_embedded']['records'] - - -def test_order_book(setup, test_client): - params = { - 'selling_asset_type': 'credit_alphanum4', - 'selling_asset_code': 'KIN', - 'selling_asset_issuer': setup.issuer_address, - 'buying_asset_type': 'native', - 'buying_asset_code': 'XLM', - } - reply = test_client.horizon.order_book(params=params) - assert reply - assert reply['base']['asset_code'] == 'KIN' - - -def test_ledgers(test_client): - reply = test_client.horizon.ledgers() - assert reply - assert reply['_embedded']['records'] - - -def test_ledger(test_client): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.ledger('bad') - assert exc_info.value.type == HorizonErrorType.BAD_REQUEST # not 'Resource Missing'! - - reply = test_client.horizon.ledger(5) - assert reply - assert reply['sequence'] == 5 - - -def test_ledger_effects(test_client): - with pytest.raises(HorizonError, match='Bad Request') as exc_info: - test_client.horizon.ledger_effects('bad') - assert exc_info.value.type == HorizonErrorType.BAD_REQUEST # not 'Resource Missing'! - - reply = test_client.horizon.ledger_effects(5) - assert reply - assert reply['_embedded'] - - -def test_ledger_operations(test_client): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.ledger_operations('bad') - assert exc_info.value.type == HorizonErrorType.BAD_REQUEST # not 'Resource Missing'! - - reply = test_client.horizon.ledger_operations(5) - assert reply - assert reply['_embedded'] - - -def test_ledger_payments(test_client): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.ledger_payments('bad') - assert exc_info.value.type == HorizonErrorType.BAD_REQUEST # not 'Resource Missing'! - - reply = test_client.horizon.ledger_payments(5) - assert reply - assert reply['_embedded'] - - -def test_effects(test_client): - reply = test_client.horizon.effects() - assert reply - assert reply['_embedded']['records'] - - -def test_operations(test_client): - reply = test_client.horizon.operations() - assert reply - assert reply['_embedded']['records'] - - -def test_operation(test_client): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.operation('bad') - assert exc_info.value.type == HorizonErrorType.BAD_REQUEST # not 'Resource Missing'! - - reply = test_client.horizon.operations() - op_id = reply['_embedded']['records'][0]['id'] - - reply = test_client.horizon.operation(op_id) - assert reply - assert reply['id'] == op_id - - -def test_operation_effects(test_client): - with pytest.raises(HorizonError) as exc_info: - test_client.horizon.operation_effects('bad') - assert exc_info.value.type == HorizonErrorType.BAD_REQUEST # not 'Resource Missing'! - - reply = test_client.horizon.operations() - op_id = reply['_embedded']['records'][0]['id'] - - reply = test_client.horizon.operation_effects(op_id) - assert reply - assert reply['_embedded']['records'] - - -def test_payments(test_client): - reply = test_client.horizon.payments() - assert reply - assert reply['_embedded']['records'] - - -def test_horizon_error_hashable(): - err_dict = dict(title='title', - status=400, - detail='detail', - instance='instance', - extras={}, - type=HORIZON_NS_PREFIX + HorizonErrorType.BAD_REQUEST) - e = HorizonError(err_dict) - {e: 1} # shouldn't fail on unhashable type diff --git a/test/test_monitor.py b/test/test_monitor.py index cdf2127..a696248 100644 --- a/test/test_monitor.py +++ b/test/test_monitor.py @@ -1,85 +1,10 @@ import pytest -from time import sleep @pytest.mark.skip(reason='Blockchain sometimes returns 404, known broken') def test_single_monitor(test_client, test_account): - address = 'GCFNA3MUPL6ZELRZQD5IGBZRWMYIQV6VVG2LCAERLY2A7W5VVRXSBH75' - seed = 'SAEAU66JLC5QNKSNABHH56XXKLHVSQAK7RH34VD2LEALNDKRBLSZ66QD' - test_client.friendbot(address) + pass - txs_found = [] - def account_tx_callback(addr, tx_data, monitor): - assert addr == address - txs_found.append(tx_data.id) - - # start monitoring - monitor = test_client.monitor_account_payments(address, account_tx_callback) - assert monitor.thread.is_alive() - - # pay from sdk to the account - hash1 = test_account.send_kin(address, 1, fee=100) - hash2 = test_account.send_kin(address, 2, fee=100) - sleep(20) - - # Horizon should timeout after 10 seconds of no traffic, make sure we reconnected - hash3 = test_account.send_kin(address, 3, fee=100) - - sleep(5) - assert hash1 in txs_found - assert hash2 in txs_found - assert hash3 in txs_found - - monitor.stop() - # Make sure we stopped monitoring - hash4 = test_account.send_kin(address, 4, fee=100) - sleep(10) - assert not monitor.thread.is_alive() - assert hash4 not in txs_found - - -@pytest.mark.skip(reason='Known broken feature on the blockchain') +@pytest.mark.skip(reason='Blockchain sometimes returns 404, known broken') def test_multi_monitor(test_client, test_account): - address1 = 'GBMU6NALXWCGEVAU2KKG4KIR3WVRSRRKDQB54VED4MKZZPV653ZVUCNB' - seed1 = 'SCIVPFA3NFG5Q7W7U3EOP2P33GOETCDMYYIM72BNBD4HKY6WF5J3IE5G' - test_client.friendbot(address1) - - address2 = 'GB5LRQXPZKCXGTHR2MGD4VNCMV53GJ5WK4NAQOBKRLMGOP3UQJEJMVH2' - seed2 = 'SAVWARZ7WGUPZJEBIUSQ2ZS4I2PPZILMHWXE7W5OSM2T5BSMCZIBP3G2' - test_client.friendbot(address2) - - txs_found1 = [] - txs_found2 = [] - - def account_tx_callback(addr, tx_data, monitor): - if addr == address1: - txs_found1.append(tx_data.id) - elif addr == address2: - txs_found2.append(tx_data.id) - - # start monitoring - monitor = test_client.monitor_accounts_payments([test_account.get_public_address(), address1], - account_tx_callback) - assert monitor.thread.is_alive() - - # pay from sdk to the account - hash1 = test_account.send_kin(address1, 1, fee=100) - sleep(5) - assert hash1 in txs_found1 - - hash2 = test_account.send_kin(address2, 2, fee=100) - sleep(5) - # The second address is not being watched - assert hash2 not in txs_found2 - - monitor.add_address(address2) - hash3 = test_account.send_kin(address2, 3, fee=100) - sleep(5) - assert hash3 in txs_found2 - - # stop monitoring - monitor.stop() - hash4 = test_account.send_kin(address2, 4, fee=100) - sleep(10) - assert not monitor.thread.is_alive() - assert hash4 not in txs_found2 + pass From daa45bd5691a2d3ae6569d4c402be43a487039cc Mon Sep 17 00:00:00 2001 From: ronserruya Date: Tue, 19 Mar 2019 12:25:40 +0200 Subject: [PATCH 06/22] Update requirments --- Pipfile | 2 +- Pipfile.lock | 149 ++++++++++++++++++++++++++++++++++++++++++++++- requirements.txt | 2 +- 3 files changed, 148 insertions(+), 5 deletions(-) diff --git a/Pipfile b/Pipfile index a6d6a61..c31fa5b 100644 --- a/Pipfile +++ b/Pipfile @@ -8,7 +8,7 @@ python_version = "3.7" [packages] schematics = "==2.1.*" -kin-base = {ref = "async-kin", git = "https://github.com/kinecosystem/py-kin-base"} +kin-base = "==1.2.0" [dev-packages] codecov = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 111efff..86f25b0 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "9682530ca93d3be6dd87473c2ca99f9e3a29ddb027316f051ef39db5a7271e21" + "sha256": "1c32a22d4c310a6bde3a9991be85feafdd683f9badc3ea6a776803e2c414ec6e" }, "pipfile-spec": 6, "requires": { @@ -16,9 +16,136 @@ ] }, "default": { + "aiohttp": { + "hashes": [ + "sha256:00d198585474299c9c3b4f1d5de1a576cc230d562abc5e4a0e81d71a20a6ca55", + "sha256:0155af66de8c21b8dba4992aaeeabf55503caefae00067a3b1139f86d0ec50ed", + "sha256:09654a9eca62d1bd6d64aa44db2498f60a5c1e0ac4750953fdd79d5c88955e10", + "sha256:199f1d106e2b44b6dacdf6f9245493c7d716b01d0b7fbe1959318ba4dc64d1f5", + "sha256:296f30dedc9f4b9e7a301e5cc963012264112d78a1d3094cd83ef148fdf33ca1", + "sha256:368ed312550bd663ce84dc4b032a962fcb3c7cae099dbbd48663afc305e3b939", + "sha256:40d7ea570b88db017c51392349cf99b7aefaaddd19d2c78368aeb0bddde9d390", + "sha256:629102a193162e37102c50713e2e31dc9a2fe7ac5e481da83e5bb3c0cee700aa", + "sha256:6d5ec9b8948c3d957e75ea14d41e9330e1ac3fed24ec53766c780f82805140dc", + "sha256:87331d1d6810214085a50749160196391a712a13336cd02ce1c3ea3d05bcf8d5", + "sha256:9a02a04bbe581c8605ac423ba3a74999ec9d8bce7ae37977a3d38680f5780b6d", + "sha256:9c4c83f4fa1938377da32bc2d59379025ceeee8e24b89f72fcbccd8ca22dc9bf", + "sha256:9cddaff94c0135ee627213ac6ca6d05724bfe6e7a356e5e09ec57bd3249510f6", + "sha256:a25237abf327530d9561ef751eef9511ab56fd9431023ca6f4803f1994104d72", + "sha256:a5cbd7157b0e383738b8e29d6e556fde8726823dae0e348952a61742b21aeb12", + "sha256:a97a516e02b726e089cffcde2eea0d3258450389bbac48cbe89e0f0b6e7b0366", + "sha256:acc89b29b5f4e2332d65cd1b7d10c609a75b88ef8925d487a611ca788432dfa4", + "sha256:b05bd85cc99b06740aad3629c2585bda7b83bd86e080b44ba47faf905fdf1300", + "sha256:c2bec436a2b5dafe5eaeb297c03711074d46b6eb236d002c13c42f25c4a8ce9d", + "sha256:cc619d974c8c11fe84527e4b5e1c07238799a8c29ea1c1285149170524ba9303", + "sha256:d4392defd4648badaa42b3e101080ae3313e8f4787cb517efd3f5b8157eaefd6", + "sha256:e1c3c582ee11af7f63a34a46f0448fca58e59889396ffdae1f482085061a2889" + ], + "version": "==3.5.4" + }, + "aiohttp-sse-client": { + "hashes": [ + "sha256:468ce4e1120b896d37141d8f9d235fd58212c8024a909ec88c20ce143253cebd", + "sha256:65511b1823f5ba68fdf6c0210689f2432b94878c843384df7b69ce6ba4decad4" + ], + "version": "==0.1.4" + }, + "async-timeout": { + "hashes": [ + "sha256:0c3c816a028d47f659d6ff5c745cb2acf1f966da1fe5c19c77a70282b25f4c5f", + "sha256:4291ca197d287d274d0b6cb5d6f8f8f82d434ed288f962539ff18cc9012f9ea3" + ], + "version": "==3.0.1" + }, + "attrs": { + "hashes": [ + "sha256:69c0dbf2ed392de1cb5ec704444b08a5ef81680a61cb899dc08127123af36a79", + "sha256:f0b870f674851ecbfbbbd364d6b5cbdff9dcedbc7f3f5e18a6891057f21fe399" + ], + "version": "==19.1.0" + }, + "chardet": { + "hashes": [ + "sha256:84ab92ed1c4d4f16916e05906b6b75a6c0fb5db821cc65e70cbd64a3e2a5eaae", + "sha256:fc323ffcaeaed0e0a02bf4d117757b98aed530d9ed4531e3e15460124c106691" + ], + "version": "==3.0.4" + }, + "crc16": { + "hashes": [ + "sha256:1b9f697a93491ae42ed653c1e78ea25a33532afab87b513e6890975450271a01", + "sha256:c1f86aa0390f4baf07d2631b16b979580eae1d9a973a826ce45353a22ee8d396" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin'", + "version": "==0.1.1" + }, + "ed25519": { + "hashes": [ + "sha256:2991b94e1883d1313c956a1e3ced27b8a2fdae23ac40c0d9d0b103d5a70d1d2a" + ], + "markers": "sys_platform != 'win32' and sys_platform != 'cygwin'", + "version": "==1.4" + }, + "idna": { + "hashes": [ + "sha256:c357b3f628cf53ae2c4c05627ecc484553142ca23264e593d327bcde5e9c3407", + "sha256:ea8b7f6188e6fa117537c3df7da9fc686d485087abf6ac197f9c46432f7e4a3c" + ], + "version": "==2.8" + }, "kin-base": { - "git": "https://github.com/kinecosystem/py-kin-base", - "ref": "6bf51c030bea5bae982fd36167d9c0375de5782f" + "hashes": [ + "sha256:2396a81aa11decd4a3cab0cd456fe2dce092c8e6311c0aec28022a04960aeb8c", + "sha256:96d74f20eea0e1a64c6147577a4610dc719b916c412097ad1beb7652333abcf0" + ], + "index": "pypi", + "version": "==1.2.0" + }, + "mnemonic": { + "hashes": [ + "sha256:02a7306a792370f4a0c106c2cf1ce5a0c84b9dbd7e71c6792fdb9ad88a727f1d" + ], + "version": "==0.18" + }, + "multidict": { + "hashes": [ + "sha256:024b8129695a952ebd93373e45b5d341dbb87c17ce49637b34000093f243dd4f", + "sha256:041e9442b11409be5e4fc8b6a97e4bcead758ab1e11768d1e69160bdde18acc3", + "sha256:045b4dd0e5f6121e6f314d81759abd2c257db4634260abcfe0d3f7083c4908ef", + "sha256:047c0a04e382ef8bd74b0de01407e8d8632d7d1b4db6f2561106af812a68741b", + "sha256:068167c2d7bbeebd359665ac4fff756be5ffac9cda02375b5c5a7c4777038e73", + "sha256:148ff60e0fffa2f5fad2eb25aae7bef23d8f3b8bdaf947a65cdbe84a978092bc", + "sha256:1d1c77013a259971a72ddaa83b9f42c80a93ff12df6a4723be99d858fa30bee3", + "sha256:1d48bc124a6b7a55006d97917f695effa9725d05abe8ee78fd60d6588b8344cd", + "sha256:31dfa2fc323097f8ad7acd41aa38d7c614dd1960ac6681745b6da124093dc351", + "sha256:34f82db7f80c49f38b032c5abb605c458bac997a6c3142e0d6c130be6fb2b941", + "sha256:3d5dd8e5998fb4ace04789d1d008e2bb532de501218519d70bb672c4c5a2fc5d", + "sha256:4a6ae52bd3ee41ee0f3acf4c60ceb3f44e0e3bc52ab7da1c2b2aa6703363a3d1", + "sha256:4b02a3b2a2f01d0490dd39321c74273fed0568568ea0e7ea23e02bd1fb10a10b", + "sha256:4b843f8e1dd6a3195679d9838eb4670222e8b8d01bc36c9894d6c3538316fa0a", + "sha256:5de53a28f40ef3c4fd57aeab6b590c2c663de87a5af76136ced519923d3efbb3", + "sha256:61b2b33ede821b94fa99ce0b09c9ece049c7067a33b279f343adfe35108a4ea7", + "sha256:6a3a9b0f45fd75dc05d8e93dc21b18fc1670135ec9544d1ad4acbcf6b86781d0", + "sha256:76ad8e4c69dadbb31bad17c16baee61c0d1a4a73bed2590b741b2e1a46d3edd0", + "sha256:7ba19b777dc00194d1b473180d4ca89a054dd18de27d0ee2e42a103ec9b7d014", + "sha256:7c1b7eab7a49aa96f3db1f716f0113a8a2e93c7375dd3d5d21c4941f1405c9c5", + "sha256:7fc0eee3046041387cbace9314926aa48b681202f8897f8bff3809967a049036", + "sha256:8ccd1c5fff1aa1427100ce188557fc31f1e0a383ad8ec42c559aabd4ff08802d", + "sha256:8e08dd76de80539d613654915a2f5196dbccc67448df291e69a88712ea21e24a", + "sha256:c18498c50c59263841862ea0501da9f2b3659c00db54abfbf823a80787fde8ce", + "sha256:c49db89d602c24928e68c0d510f4fcf8989d77defd01c973d6cbe27e684833b1", + "sha256:ce20044d0317649ddbb4e54dab3c1bcc7483c78c27d3f58ab3d0c7e6bc60d26a", + "sha256:d1071414dd06ca2eafa90c85a079169bfeb0e5f57fd0b45d44c092546fcd6fd9", + "sha256:d3be11ac43ab1a3e979dac80843b42226d5d3cccd3986f2e03152720a4297cd7", + "sha256:db603a1c235d110c860d5f39988ebc8218ee028f07a7cbc056ba6424372ca31b" + ], + "version": "==4.5.2" + }, + "pbkdf2": { + "hashes": [ + "sha256:ac6397369f128212c43064a2b4878038dab78dab41875364554aaf2a684e6979" + ], + "version": "==1.3" }, "schematics": { "hashes": [ @@ -27,6 +154,22 @@ ], "index": "pypi", "version": "==2.1.0" + }, + "yarl": { + "hashes": [ + "sha256:024ecdc12bc02b321bc66b41327f930d1c2c543fa9a561b39861da9388ba7aa9", + "sha256:2f3010703295fbe1aec51023740871e64bb9664c789cba5a6bdf404e93f7568f", + "sha256:3890ab952d508523ef4881457c4099056546593fa05e93da84c7250516e632eb", + "sha256:3e2724eb9af5dc41648e5bb304fcf4891adc33258c6e14e2a7414ea32541e320", + "sha256:5badb97dd0abf26623a9982cd448ff12cb39b8e4c94032ccdedf22ce01a64842", + "sha256:73f447d11b530d860ca1e6b582f947688286ad16ca42256413083d13f260b7a0", + "sha256:7ab825726f2940c16d92aaec7d204cfc34ac26c0040da727cf8ba87255a33829", + "sha256:b25de84a8c20540531526dfbb0e2d2b648c13fd5dd126728c496d7c3fea33310", + "sha256:c6e341f5a6562af74ba55205dbd56d248daf1b5748ec48a0200ba227bb9e33f4", + "sha256:c9bb7c249c4432cd47e75af3864bc02d26c9594f49c82e2a28624417f0ae63b8", + "sha256:e060906c0c585565c718d1c3841747b61c5439af2211e185f6739a9412dfbde1" + ], + "version": "==1.3.0" } }, "develop": { diff --git a/requirements.txt b/requirements.txt index e29c1d0..cf32735 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,2 @@ -kin-base==1.1.0 +kin-base==1.2.0 schematics==2.1.* From 3d7c832bbf040ef7e84fef8de59c7c26268244ab Mon Sep 17 00:00:00 2001 From: ronserruya Date: Tue, 19 Mar 2019 12:57:03 +0200 Subject: [PATCH 07/22] Update intergration tests images --- images/docker-compose.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/images/docker-compose.yml b/images/docker-compose.yml index cc9a0c6..67a9771 100644 --- a/images/docker-compose.yml +++ b/images/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: stellar-core-1: - image: kinecosystem/stellar-core:kinecosystem-v2.0.0-stellar-v9.2.0 + image: kinecosystem/stellar-core:latset ports: - 11626:11626 links: @@ -26,7 +26,7 @@ services: POSTGRES_DB: core stellar-core-2: - image: kinecosystem/stellar-core:kinecosystem-v2.0.0-stellar-v9.2.0 + image: kinecosystem/stellar-core:latest ports: - 11627:11626 links: @@ -49,7 +49,7 @@ services: POSTGRES_DB: core horizon: - image: kinecosystem/horizon:v2.0.0-stellar-v0.12.3 + image: kinecosystem/horizon:latest # using nginx-proxy ports: - 8000:8000 @@ -96,7 +96,7 @@ services: POSTGRES_DB: horizon horizon-nginx-proxy: - image: kinecosystem/horizon-nginx-proxy:85c6b72 + image: kinecosystem/horizon-nginx-proxy:latest ports: - 8008:80 links: From aa3826e370a564daf3cfe138b0b3dd378fe6c30f Mon Sep 17 00:00:00 2001 From: ronserruya Date: Tue, 19 Mar 2019 17:36:54 +0200 Subject: [PATCH 08/22] Fix typo in images --- images/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/docker-compose.yml b/images/docker-compose.yml index 67a9771..c7c4d07 100644 --- a/images/docker-compose.yml +++ b/images/docker-compose.yml @@ -2,7 +2,7 @@ version: "3" services: stellar-core-1: - image: kinecosystem/stellar-core:latset + image: kinecosystem/stellar-core:latest ports: - 11626:11626 links: From 5c99b1c67ef239ffd1a588f1e8f57106337c816c Mon Sep 17 00:00:00 2001 From: ronserruya Date: Wed, 20 Mar 2019 11:42:14 +0200 Subject: [PATCH 09/22] Update requirments --- .travis.yml | 2 ++ Pipfile | 2 +- Pipfile.lock | 8 ++++---- kin/blockchain/channel_manager.py | 8 +++++++- requirements.txt | 3 ++- setup.py | 2 +- 6 files changed, 17 insertions(+), 8 deletions(-) diff --git a/.travis.yml b/.travis.yml index 2aec645..945db2e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,8 @@ sudo: required language: python +dist: xenial python: + - "3.7" - "3.6" services: diff --git a/Pipfile b/Pipfile index c31fa5b..042598c 100644 --- a/Pipfile +++ b/Pipfile @@ -8,7 +8,7 @@ python_version = "3.7" [packages] schematics = "==2.1.*" -kin-base = "==1.2.0" +kin-base = "==1.3.0" [dev-packages] codecov = "*" diff --git a/Pipfile.lock b/Pipfile.lock index 86f25b0..c60ece4 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "1c32a22d4c310a6bde3a9991be85feafdd683f9badc3ea6a776803e2c414ec6e" + "sha256": "43d80c0418d74141fec19031ebd0377b047d79ffe5bc481318921c181bb769df" }, "pipfile-spec": 6, "requires": { @@ -95,11 +95,11 @@ }, "kin-base": { "hashes": [ - "sha256:2396a81aa11decd4a3cab0cd456fe2dce092c8e6311c0aec28022a04960aeb8c", - "sha256:96d74f20eea0e1a64c6147577a4610dc719b916c412097ad1beb7652333abcf0" + "sha256:1be39e39f356fee42157f3105348d6a0de68530ac31d5a5b3dd42eceafae7d55", + "sha256:73260264ee0cf58c9deb9a70f94a93084c82df2628c1ccee392b697cb00191d9" ], "index": "pypi", - "version": "==1.2.0" + "version": "==1.3.0" }, "mnemonic": { "hashes": [ diff --git a/kin/blockchain/channel_manager.py b/kin/blockchain/channel_manager.py index 57bde22..77fc883 100644 --- a/kin/blockchain/channel_manager.py +++ b/kin/blockchain/channel_manager.py @@ -1,12 +1,18 @@ """Contains classes and methods related to channels""" +import sys import random -from contextlib import asynccontextmanager from asyncio.queues import Queue as queue from enum import Enum from typing import List, Optional +# Python 3.6 didnt support asynccontextmanager, so the kin-sdk installs a backport for it +if sys.version_info.minor == 6: + from asyncgenerator import asynccontextmanager +else: + from contextlib import asynccontextmanager + class ChannelManager: """Provide useful methods to interact with the underlying ChannelPool""" diff --git a/requirements.txt b/requirements.txt index cf32735..febcbcf 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,2 +1,3 @@ -kin-base==1.2.0 +kin-base==1.3.0 schematics==2.1.* +async-generator; python_version < '3.7' diff --git a/setup.py b/setup.py index 961e4e2..52749d5 100644 --- a/setup.py +++ b/setup.py @@ -31,5 +31,5 @@ ], install_requires=requires, tests_require=tests_requires, - python_requires='>=3.5.3', + python_requires='>=3.6', ) From 0e921570fae71ec31aabc7129177255e5af7761a Mon Sep 17 00:00:00 2001 From: ronserruya Date: Wed, 20 Mar 2019 11:49:14 +0200 Subject: [PATCH 10/22] Update req-dev --- requirements-dev.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index 7562b4f..e7d7a2e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -codecov==2.0.15 -coverage==4.5.1 -pytest-cov==2.5.1 -pytest==3.4.0 -pytest-asyncio==0.10.0 +codecov +coverage +pytest-cov +pytest +pytest-asyncio From b2e2cb9445e007a1c1e6314b2b6dd46f62ce48b8 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Wed, 20 Mar 2019 12:05:31 +0200 Subject: [PATCH 11/22] Fix module name --- kin/blockchain/channel_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kin/blockchain/channel_manager.py b/kin/blockchain/channel_manager.py index 77fc883..3f623bc 100644 --- a/kin/blockchain/channel_manager.py +++ b/kin/blockchain/channel_manager.py @@ -9,7 +9,7 @@ # Python 3.6 didnt support asynccontextmanager, so the kin-sdk installs a backport for it if sys.version_info.minor == 6: - from asyncgenerator import asynccontextmanager + from async-generator import asynccontextmanager else: from contextlib import asynccontextmanager From baeb8a7d3c393f51440cd81a1ceb31ad9a2bfafd Mon Sep 17 00:00:00 2001 From: ronserruya Date: Wed, 20 Mar 2019 12:08:19 +0200 Subject: [PATCH 12/22] Fix module name --- kin/blockchain/channel_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kin/blockchain/channel_manager.py b/kin/blockchain/channel_manager.py index 3f623bc..1730e70 100644 --- a/kin/blockchain/channel_manager.py +++ b/kin/blockchain/channel_manager.py @@ -9,7 +9,7 @@ # Python 3.6 didnt support asynccontextmanager, so the kin-sdk installs a backport for it if sys.version_info.minor == 6: - from async-generator import asynccontextmanager + from async_generator import asynccontextmanager else: from contextlib import asynccontextmanager From c0b67ed7da005ae19d7f71a86abd0090ab33c2e6 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Wed, 20 Mar 2019 12:13:28 +0200 Subject: [PATCH 13/22] Update network passphrase --- images/docker-compose.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/images/docker-compose.yml b/images/docker-compose.yml index c7c4d07..f8c32fd 100644 --- a/images/docker-compose.yml +++ b/images/docker-compose.yml @@ -63,7 +63,7 @@ services: # available configuration visible at: # https://github.com/stellar/horizon/blob/v0.11.1/src/github.com/stellar/horizon/cmd/horizon/main.go#L33 # - NETWORK_PASSPHRASE: private testnet + NETWORK_PASSPHRASE: "Integration Test Network ; zulucrypto" DATABASE_URL: postgres://stellar:12345678@horizon-db/horizon?sslmode=disable HORIZON_DB_MAX_OPEN_CONNECTIONS: "24" STELLAR_CORE_DATABASE_URL: postgres://stellar:12345678@stellar-core-1-db/core?sslmode=disable From 4e3ee369f684790d20eeb3d245a847a16e329c87 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Sun, 24 Mar 2019 12:50:34 +0200 Subject: [PATCH 14/22] Remove todo --- kin/account.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/kin/account.py b/kin/account.py index 20d10d9..a6603ef 100644 --- a/kin/account.py +++ b/kin/account.py @@ -85,7 +85,7 @@ async def get_status(self, verbose: Optional[bool] = False) -> dict: 'app_id': self.app_id, 'public_address': self.get_public_address(), 'balance': await self.get_balance(), - 'channels': self.channel_manager.get_status(verbose) #TODO: await? + 'channels': self.channel_manager.get_status(verbose) } total_status = { 'client': await self._client.get_config(), From eaa0feb066bad4743739d64e45c3ba8542122361 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Sun, 24 Mar 2019 14:38:39 +0200 Subject: [PATCH 15/22] Add close method to KinClient --- kin/client.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/kin/client.py b/kin/client.py index 59583f3..3c11a90 100644 --- a/kin/client.py +++ b/kin/client.py @@ -44,7 +44,11 @@ async def __aenter__(self): return self async def __aexit__(self, exc_type, exc_val, exc_tb): - await self.horizon.__aexit__(exc_type, exc_val, exc_tb) + await self.close() + + async def close(self) -> None: + """Close the connection to the horizon server""" + await self.horizon.close() def kin_account(self, seed: str, channel_secret_keys: Optional[List[str]] = None, app_id: Optional[str] = ANON_APP_ID) -> KinAccount: From d46d74d51db3c6751a7d07c777518c1fbd166694 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Sun, 24 Mar 2019 15:16:17 +0200 Subject: [PATCH 16/22] Update readme --- README.md | 268 ++++++++++++++++++++++-------------------------------- 1 file changed, 108 insertions(+), 160 deletions(-) diff --git a/README.md b/README.md index 36b97b3..f193d59 100644 --- a/README.md +++ b/README.md @@ -2,14 +2,9 @@ # KIN Python SDK for Kin Blockchain - -## Disclaimer - -The SDK is still in beta. No warranties are given, use on your own discretion. - ## Requirements. -Python >= 3.4 +Python >= 3.6 ## Installation @@ -21,17 +16,27 @@ pip install kin-sdk ### Initialization -The sdk has two main components, KinClient and KinAccount. -**KinClient** - Used to query the blockchain and perform actions that don't require authentication (e.g Get account balance) +The sdk has two main components, KinClient and KinAccount. +**KinClient** - Used to query the blockchain and perform actions that don't require authentication (e.g Get account balance) **KinAccount** - Used to perform authenticated actions on the blockchain (e.g Send payment) To initialize the Kin Client you will need to provide an environment (Test and Production environments are pre-configured) - +The KinClient object can be used with a context manager, or closed manually, to close the connection to the blockchain ```python from kin import KinClient, TEST_ENVIRONMENT +async with KinClient(TEST_ENVIRONMENT) as client: + ... + +OR + client = KinClient(TEST_ENVIRONMENT) +try: + ... +finally: + client.close() + ``` Custom environment can also be used: @@ -62,61 +67,48 @@ Most methods provided by the KinClient to query the blockchain about a specific ### Getting Account Balance ```python -# Get KIN/XLM balance -balance = client.get_account_balance('address') +# Get KIN balance +balance = await client.get_account_balance('address') ``` ### Getting Account Data ```python -account_data = client.get_account_data('address') +account_data = await client.get_account_data('address') ``` ### Checking If an account exists on the blockchain ```python -client.does_account_exists('address') +await client.does_account_exists('address') ``` ### Getting the minimum acceptable fee from the blockchain +Transactions usually require a fee to be processed. +To know what is the minimum fee that the blockchain will accept, use: ```python -# Transactions usually require a fee to be proccessed. -# To know what is the minimum fee that the blockchain will accept, use: -minimum_fee = client.get_minimum_fee() +minimum_fee = await client.get_minimum_fee() ``` ### Getting Transaction Data +Get information about a specific transaction +The 'simple' flag is enabled by default, and dictates what object should be returned +For simple=False: A 'kin.RawTransaction' object will return, +containing some fields that may be confusing and of no use to the user. + +For simple=True: A 'kin.SimpleTransaction' object will return, +containing only the data that the user will need. +However, if the transaction if too complex to be simplified, a 'CantSimplifyError' will be raised ```python -# Get information about a specific transaction -# The 'simple' flag is enabled by defualt, and dectates what object should be returned -# For simple=False: A 'kin.RawTransaction' object will return, -# containig some fields that may be confusing and of no use to the user. - -# For simple=True: A 'kin.SimpleTransaction' object will return, -# containing only the data that the user will need. -# However, if the transaction if too complex to be simplified, a 'CantSimplifyError' will be raised -tx_data = sdk.get_transaction_data(tx_hash, simple=True/False) - -# A transaction will not be simplifed if: -# 1. It contains a memo that is not a text memo -# 2. It contains multiple operations -# 3. It contains a payment that is not of KIN -# 4. Its operation type is not one of 'Payment'/'Create account'. - -# Given the use case of our blockchain, and the tools that we currently provied to interact with it, these conditions should not usually occur. +tx_data = await sdk.get_transaction_data(tx_hash, simple=True/False) ``` -### Verify Kin Payment -This method provides an easy way to verify that a transaction is what you expect it to be -```python -client.verify_kin_payment('tx_hash','sender','destination',amount,memo(optional),check_memo=True/False) -#Lets say that addr1 payed 15 KIN to add2, with the memo 'Enjoy!' +A transaction will not be simplified if: +1. It contains a memo that is not a text memo +2. It contains multiple operations +3. It contains a payment that is not of KIN +4. Its operation type is not one of 'Payment'/'Create account'. -client.verify_kin_payment('tx_hash','addr1','addr2',15,'Enjoy!',True) >> True -client.verify_kin_payment('tx_hash','addr1','addr2',15,'Hello',True) >> False -client.verify_kin_payment('tx_hash','addr1','addr2',15) >> True -client.verify_kin_payment('tx_hash','addr1','addr2',10) >> False -client.verify_kin_payment('tx_hash','addr1','addr3',10) >> False -``` +Given the use case of our blockchain, and the tools that we currently provide to interact with it, these conditions should not usually occur. ### Checking configuration The handy `get_config` method will return some parameters the client was configured with, along with Horizon status: @@ -125,27 +117,21 @@ status = client.get_config() ``` ```json - { - "sdk_version": "2.2.0", - "environment": "TEST", - "horizon": { - "uri": "https://horizon-playground.kininfrastructure.com", - "online": true, - "error": null - }, - "transport": { - "pool_size": 10, - "num_retries": 5, - "request_timeout": 11, - "retry_statuses": [ - 503, - 413, - 429, - 504 - ], - "backoff_factor": 0.5 - } + { + "sdk_version": "2.4.0", + "environment": "TEST", + "horizon": { + "uri": "https://horizon-testnet.kininfrastructure.com", + "online": true, + "error": null + }, + "transport": { + "pool_size": 100, + "num_retries": 3, + "request_timeout": 11, + "backoff_factor": 0.5 } +} ``` - `sdk_version` - the version of this SDK. - `environment` - the environment the SDK was configured with (TEST/PROD/CUSTOM). @@ -157,42 +143,40 @@ status = client.get_config() - `pool_size` - number of pooled connections to Horizon. - `num_retries` - number of retries on failed request. - `request_timeout` - single request timeout. - - `retry_statuses` - a list of statuses to retry on. - `backoff_factor` - a backoff factor to apply between retry attempts. ### Friendbot +If a friendbot endpoint is provided when creating the environment (it is provided with the TEST_ENVIRONMENT), +you will be able to use the friendbot method to call a service that will create an account for you ```python -# If a friendbot endpoint is provided when creating the environment (it is provided with the TEST_ENVIRONMENT), -# you will be able to use the friendbot method to call a service that will create an account for you - -client.friendbot('address') +await client.friendbot('address') ``` ## Account Usage ### Getting Wallet Details +Get the public address of my wallet account. The address is derived from the seed the account was created with. ```python -# Get the public address of my wallet account. The address is derived from the seed the account was created with. address = account.get_public_address() ``` ### Creating a New Account +Create a new account +the KIN amount can be specified in numbers or as a string ```python -# Create a new account -# the KIN amount can be specified in numbers or as a string -tx_hash = account.create_account('address', starting_balance=1000, fee=100) - -# a text memo can also be provided: -tx_hash = account.create_account('address', starting_balance=1000, fee=100, memo_text='Account creation example') +tx_hash = await account.create_account('address', starting_balance=1000, fee=100) +``` +A text memo can also be provided: +```python +tx_hash = await account.create_account('address', starting_balance=1000, fee=100, memo_text='Account creation') ``` ### Sending KIN +The KIN amount can be specified in numbers or as a string ```python -# send KIN -# the KIN amount can be specified in numbers or as a string -tx_hash = account.send_kin('destination', 1000, fee=100, memo_text='order123') +tx_hash = await account.send_kin('destination', 1000, fee=100, memo_text='order123') ``` ### Build/Submit transactions @@ -205,8 +189,8 @@ builder = account.build_send_kin('destination', 1000, fee=100, memo_text='order1 Step 2: Update the transaction ```python # do whatever you want with the builder -with account.channel_manager.get_channel() as channel: - builder.set_channel(channel) +async with account.channel_manager.get_channel() as channel: + await builder.set_channel(channel) builder.sign(channel) # If you used additional channels apart from your main account, # sign with your main account @@ -214,16 +198,15 @@ with account.channel_manager.get_channel() as channel: ``` Step 3: Send the transaction ```python - tx_hash = account.submit_transaction(builder) + tx_hash = await account.submit_transaction(builder) ``` ### Whitelist a transaction +Assuming you are registered as a whitelisted digital service with the Kin Ecosystem (exact details TBD) +You will be able to whitelist transactions for your clients, making it so that their fee will not be deducted +Your clients will send an http request to you containing their tx. +You can then whitelist it, and return it back to the client to send to the blockchain ```python -# Assuming you are registered as a whitelisted digital service with the Kin Ecosystem (exact details TBD) -# You will be able to whitelist transactions for your clients, making it so that their fee will not be deducted -# Your clients will send an http request to you containing their tx. -# You can then whitelist it, and return it back to the client to send to the blockchain - whitelisted_tx = account.whitelist_transaction(client_transaction) # By defualt, any payment sent from you is already considered whitelisted, @@ -231,49 +214,43 @@ whitelisted_tx = account.whitelist_transaction(client_transaction) ``` ### Get account status +Get the status and config of the account +If verbose it set to true, all channels and statuses will be printed ```python -# Get the status and config of the account account.get_status(verbose=False/True) -# If verbose it set to true, all channels and statuses will be printed ``` ```json { "client": { - "sdk_version": "2.2.0", - "environment": "LOCAL", + "sdk_version": "2.4.0", + "environment": "TEST", "horizon": { - "uri": "http://localhost:8000", + "uri": "https://horizon-testnet.kininfrastructure.com", "online": true, "error": null }, "transport": { - "pool_size": 10, - "num_retries": 5, + "pool_size": 100, + "num_retries": 3, "request_timeout": 11, - "retry_statuses": [ - 503, - 413, - 429, - 504 - ], "backoff_factor": 0.5 } }, "account": { "app_id": "anon", - "public_address": "GCLBBAIDP34M4JACPQJUYNSPZCQK7IRHV7ETKV6U53JPYYUIIVDVJJFQ", - "balance": 9999989999199.979, + "public_address": "GBQLWHAH5BRB3PTJEXIKGKI3YYM2DJI32ZOZBR4O5WE7FE2GNSUTF6RP", + "balance": 10000, "channels": { "total_channels": 5, "free_channels": 4, "non_free_channels": 1, "channels": { - "SBS3O5BGCPDIYWTTOV7TGLXFRPFSD6ACBEAEHJUMMPF5DUDF732MX6LL": "free", - "SC65CIJCAWJEJX5IVHDJK6FO6DM5BVPIUX5F7EULIC3C4PF7KTAUHHE2": "free", - "SABWFQ2HOYPQGCWN7INIV2RNZZLAZDOX67R3VHMGQAFF6FA3JIA2E7BB": "free", - "SBBQJTYF6K2TDUJ2LBUSXICUEEX75RXAQZRP6LLVF3JDXK5D4SVYX3X4": "taken", - "SCD36QIV3SFEGZDHRZZXO7MICNMOHSRAOV6L2MQKSW4TO4OTCR4IF2FD": "free" + "SBRHUVGBCXDM2HDSTQ5Y5QLMBCTOTK6GIQ4PDZIMCD3SG3A7MU22ASRV": "free", + "SA6XIHKGWVGUNOWUPCEA2SWBII5JEHK7Q54I2ESZ42NKUX5NYNXPTA4P": "free", + "SB57K5N2JUVXBF3S56OND4WXLZAXMBB7WFV5E5ZQTHOGQQTGCY4ZBWGL": "free", + "SCFXWAXZHM3OJA5XJNW4MIDPRYZHTECXJEOYY5O6JJB523M32OJXD756": "taken", + "SA6YK4SR2KS2RXV7SN6HFVXNO44AA7IQTZ7QKWAWS6TPJ2NCND2JMLY3": "free" } } } @@ -284,10 +261,10 @@ account.get_status(verbose=False/True) These methods are relevant to transactions ### Decode_transaction -```python -# When the client sends you a transaction for whitelisting, it will be encoded. -# If you wish to decode the transaction and verify its details before whitelisting it: +When the client sends you a transaction for whitelisting, it will be encoded. +If you wish to decode the transaction and verify its details before whitelisting it: +```python from kin import decode_transaction decoded_tx = decode_transaction(encoded_tx) @@ -301,7 +278,9 @@ These set of methods allow you to create new keypairs. from kin import Keypair my_keypair = Keypair() -# Or, you can create a keypair from an existing seed +``` +Or, you can create a keypair from an existing seed +```python my_keypair = Keypair('seed') ``` @@ -316,56 +295,30 @@ seed = Keypair.generate_seed() ``` ### Generate a deterministic seed +Given the same seed and salt, the same seed will always be generated ```python -# Given the same seed and salt, the same seed will always be generated seed = Keypair.generate_hd_seed('seed','salt') ``` -### Generate a mnemonic seed: -**Not implemented yet** - ## Monitoring Kin Payments -These methods can be used to monitor the kin payment that an account or accounts is sending/receiving -**Currently, due to a bug on the blockchain frontend, the monitor may also return 1 tx that happened before the monitoring request** - - -The monitor will run in a background thread (accessible via ```monitor.thread```) , -and will call the callback function everytime it finds a kin payment for the given address. +These methods can be used to monitor the kin payment that an account or accounts is sending/receiving ### Monitor a single account Monitoring a single account will continuously get data about this account from the blockchain and filter it. - +An additional "timeout" parameter can be passed to raise a "TimeoutError" if too much time passes between each tx. ```python -def callback_fn(address, tx_data, monitor) - print ('Found tx: {} for address: {}'.format(address,tx_data.id)) - -monitor = client.monitor_account_payments('address', callback_fn) +async for tx in client.monitor_account_payments('address'): + ... ``` ### Monitor multiple accounts -Monitoring multiple accounts will continuously get data about **all** accounts on the blockchain, and will filter it. - -```python -def callback_fn(address, tx_data, monitor) - print ('Found tx: {} for address: {}'.format(address,tx_data.id)) - -monitor = client.monitor_accounts_payments(['address1','address2'], callback_fn) -``` - -You can freely add or remove accounts to this monitor - +Monitoring multiple accounts will continuously get data about **all** accounts on the blockchain, and will filter it to only yield txs for the relevant accounts. +Since this monitor receives a set of addresses, you can freely add/remove address at from it at any point ```python -monitor.add_address('address3') -monitor.remove_address('address1') +addresses = set(['address1','address2']) +async for address, tx in client.monitor_accounts_payments(addresses): + ... ``` -### Stopping a monitor -When you are done monitoring, make sure to stop the monitor, to terminate the thread and the connection to the blockchain. - -```python -monitor.stop() -``` - - ## Channels One of the most sensitive points in Stellar is [transaction sequence](https://www.stellar.org/developers/guides/concepts/transactions.html#sequence-number). @@ -379,30 +332,25 @@ Depending on the nature of your application, here are our recommendations: In this case, the SDK can be instantiated with only the wallet key, the channel accounts are not necessary. 2. You have a single application server that should handle a stream of concurrent transactions. In this case, -you need to make sure that only a single instance of a KinAccount initialized with multiple channel accounts. -This is an important point, because if you use a standard `gunicorn/Flask` setup for example, gunicorn will spawn -several *worker processes*, each containing your Flask application, each containing your KinAccount instance, so multiple -KinAccount instances will exist, having the same channel accounts. The solution is to use gunicorn *thread workers* instead of -*process workers*, for example run gunicorn with `--threads` switch instead of `--workers` switch, so that only -one Flask application is created, containing a single KinAccount instance. +you need to make sure that only a single instance of a KinAccount initialized with multiple channel accounts. 3. You have a number of load-balanced application servers. Here, each application server should a) have the setup outlined above, and b) have its own channel accounts. This way, you ensure you will not have any collisions in your transaction sequences. ### Creating Channels -``` -# The kin sdk allows you to create HD (highly desterministic) channels based on your seed and a passphrase to be used as a salt. -# As long as you use the same seed and passphrase, you will always get the same seeds. +The kin sdk allows you to create HD (highly deterministic) channels based on your seed and a passphrase to be used as a salt. +As long as you use the same seed and passphrase, you will always get the same seeds. +``` import kin.utils channels = utils.create_channels(master_seed, environment, amount, starting_balance, salt) -"channels" will be a list of seeds the sdk created for you, that can be used when initializing the KinAccount object. - -# If you just wish to get the list of the channels generated from your seed + passphrase combination without creating them - +# "channels" will be a list of seeds the sdk created for you, that can be used when initializing the KinAccount object. +``` +If you just wish to get the list of the channels generated from your seed + passphrase combination without creating them +```python channels = utils.get_hd_channels(master_seed, salt, amount) ``` From 75941a1f69bfecddc1de77931f613d9b85429e41 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Mon, 25 Mar 2019 10:20:20 +0200 Subject: [PATCH 17/22] Disable codecov patch warning --- .codecov.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml new file mode 100644 index 0000000..fa348a8 --- /dev/null +++ b/.codecov.yml @@ -0,0 +1,3 @@ +coverage: + status: + patch: off From 91824d1f411c53b1dd715b010eb977f1e3da7ce9 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Mon, 25 Mar 2019 10:55:49 +0200 Subject: [PATCH 18/22] Update setup.py --- setup.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 52749d5..0bcb5a4 100644 --- a/setup.py +++ b/setup.py @@ -5,9 +5,9 @@ exec(open("kin/version.py").read()) with open('requirements.txt') as f: - requires = [line.split(' ')[0] for line in f] + requires = f.readlines() with open('requirements-dev.txt') as f: - tests_requires = [line.split(' ')[0] for line in f] + tests_requires = f.readlines() setup( name='kin-sdk', From a3d8c5dba22f8f561da747cc7ed5e837674d5fa2 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Mon, 25 Mar 2019 11:36:10 +0200 Subject: [PATCH 19/22] Use context manager when creating channels --- kin/utils.py | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/kin/utils.py b/kin/utils.py index a2762de..db34ade 100644 --- a/kin/utils.py +++ b/kin/utils.py @@ -24,38 +24,38 @@ async def create_channels(master_seed: str, environment: Environment, amount: in :return: The list of seeds generated """ - client = KinClient(environment) - base_key = Keypair(master_seed) - if not await client.does_account_exists(base_key.public_address): - raise AccountNotFoundError(base_key.public_address) + async with KinClient(environment) as client: + base_key = Keypair(master_seed) + if not await client.does_account_exists(base_key.public_address): + raise AccountNotFoundError(base_key.public_address) - fee = await client.get_minimum_fee() + fee = await client.get_minimum_fee() - channels = get_hd_channels(master_seed, salt, amount) + channels = get_hd_channels(master_seed, salt, amount) - # Create a builder for the transaction - builder = Builder(client.horizon, environment.name, fee, master_seed) + # Create a builder for the transaction + builder = Builder(client.horizon, environment.name, fee, master_seed) - # Find out if this salt+seed combination was ever used to create channels. - # If so, the user might only be interested in adding channels, - # so we need to find what seed to start from + # Find out if this salt+seed combination was ever used to create channels. + # If so, the user might only be interested in adding channels, + # so we need to find what seed to start from - # First check if the last channel exists, if it does, we don't need to create any channel. - if await client.does_account_exists(Keypair.address_from_seed(channels[-1])): - return channels + # First check if the last channel exists, if it does, we don't need to create any channel. + if await client.does_account_exists(Keypair.address_from_seed(channels[-1])): + return channels - for index, seed in enumerate(channels): - if await client.does_account_exists(Keypair.address_from_seed(seed)): - continue + for index, seed in enumerate(channels): + if await client.does_account_exists(Keypair.address_from_seed(seed)): + continue - # Start creating from the current seed forward - for channel_seed in channels[index:]: - builder.append_create_account_op(Keypair.address_from_seed(channel_seed), str(starting_balance)) + # Start creating from the current seed forward + for channel_seed in channels[index:]: + builder.append_create_account_op(Keypair.address_from_seed(channel_seed), str(starting_balance)) - await builder.update_sequence() - builder.sign() - await builder.submit() - break + await builder.update_sequence() + builder.sign() + await builder.submit() + break return channels From f6744aefc3a05b4d26fd091f2e9a7f38fdbd1322 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Mon, 25 Mar 2019 11:51:31 +0200 Subject: [PATCH 20/22] Remove codecov.yml --- .codecov.yml | 3 --- 1 file changed, 3 deletions(-) delete mode 100644 .codecov.yml diff --git a/.codecov.yml b/.codecov.yml deleted file mode 100644 index fa348a8..0000000 --- a/.codecov.yml +++ /dev/null @@ -1,3 +0,0 @@ -coverage: - status: - patch: off From ca884774740660e08c3cf8039d46f2a57f5d7ce9 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Mon, 25 Mar 2019 16:26:22 +0200 Subject: [PATCH 21/22] Adjust comments to improve docs generation --- kin/account.py | 11 +++++++++++ kin/blockchain/channel_manager.py | 16 ++++++++++------ kin/blockchain/environment.py | 3 ++- kin/blockchain/keypair.py | 5 +++++ kin/client.py | 8 ++++++++ kin/monitors.py | 2 ++ kin/transactions.py | 20 +++++++++++++++----- kin/utils.py | 2 ++ 8 files changed, 55 insertions(+), 12 deletions(-) diff --git a/kin/account.py b/kin/account.py index a6603ef..0057e57 100644 --- a/kin/account.py +++ b/kin/account.py @@ -58,6 +58,7 @@ def get_public_address(self): async def get_balance(self) -> float: """ Get the KIN balance of this KinAccount + :return: the kin balance :raises: KinErrors.AccountNotFoundError if the account does not exist. @@ -77,6 +78,7 @@ async def get_data(self) -> AccountData: async def get_status(self, verbose: Optional[bool] = False) -> dict: """ Get the config and status of this KinAccount object + :param verbose: Should the channels status be verbose :return: The config and status of this KinAccount object :rtype dict @@ -99,6 +101,7 @@ async def get_transaction_history(self, amount: Optional[int] = 10, descending: simple: Optional[bool] = True) -> List[Union[SimplifiedTransaction, RawTransaction]]: """ Get the transaction history for this kin account + :param amount: The maximum number of transactions to get :param descending: The order of the transactions, True will start from the latest one :param cursor: The horizon paging token @@ -115,6 +118,7 @@ async def get_transaction_history(self, amount: Optional[int] = 10, descending: def get_transaction_builder(self, fee: int) -> Builder: """ Get a transaction builder using this account + :param fee: The fee that will be used for the transaction """ return Builder(horizon=self._client.horizon, @@ -180,6 +184,7 @@ async def send_kin(self, address: str, amount: Union[float, str], fee: int, def build_create_account(self, address: str, starting_balance: Union[float, str], fee: int, memo_text: Optional[str] = None) -> Builder: """Build a tx that will create an account identified by the provided address. + :param address: the address of the account to create. :param starting_balance: the starting XLM balance of the account. :param memo_text: (optional) a text to put into transaction memo, up to MEMO_CAP chars. @@ -204,6 +209,7 @@ def build_create_account(self, address: str, starting_balance: Union[float, str] def build_send_kin(self, address: str, amount: Union[float, str], fee: int, memo_text: Optional[str] = None) -> Builder: """Build a tx to send KIN to the account identified by the provided address. + :param address: the account to send asset to. :param amount: the KIN amount to send. :param memo_text: (optional) a text to put into transaction memo. @@ -229,7 +235,9 @@ def build_send_kin(self, address: str, amount: Union[float, str], fee: int, async def submit_transaction(self, tx_builder: Builder) -> str: """ Submit a transaction to the blockchain. + :param kin.Builder tx_builder: The transaction builder + :return: The hash of the transaction. :rtype: str """ @@ -253,6 +261,7 @@ async def submit_transaction(self, tx_builder: Builder) -> str: def monitor_payments(self, timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: """Monitor KIN payment transactions related to this account + :param timeout: How long to wait for each event :raises: ValueError: if the address is in the wrong format @@ -263,6 +272,7 @@ def monitor_payments(self, timeout: Optional[float] = None) -> AsyncGenerator[Si def whitelist_transaction(self, payload: Union[str, dict]) -> str: """ Sign on a transaction to whitelist it + :param payload: the json received from the client :return: a signed transaction encoded as base64 """ @@ -301,6 +311,7 @@ def whitelist_transaction(self, payload: Union[str, dict]) -> str: async def _top_up(self, address: str) -> None: """ Top up a channel with the base account. + :param address: The address to top up """ diff --git a/kin/blockchain/channel_manager.py b/kin/blockchain/channel_manager.py index 1730e70..a77abe0 100644 --- a/kin/blockchain/channel_manager.py +++ b/kin/blockchain/channel_manager.py @@ -20,6 +20,7 @@ class ChannelManager: def __init__(self, channel_seeds: List[str]): """ Crete a channel manager instance + :param channel_seeds: The seeds of the channels to use """ self.channel_pool = ChannelPool(channel_seeds) @@ -28,6 +29,7 @@ def __init__(self, channel_seeds: List[str]): async def get_channel(self) -> str: """ Get an available channel + :return a free channel seed """ @@ -42,6 +44,7 @@ async def get_channel(self) -> str: async def put_channel(self, channel) -> None: """ Set a channel status back to FREE + :param str channel: the channel to set back to FREE """ await self.channel_pool.put(channel) @@ -49,6 +52,7 @@ async def put_channel(self, channel) -> None: def get_status(self, verbose: Optional[bool] = False) -> dict: """ Return the current status of the channel manager + :param verbose: Include all channel seeds and their statuses in the response :return: The status of the channel manager """ @@ -66,6 +70,7 @@ def get_status(self, verbose: Optional[bool] = False) -> dict: class ChannelStatuses(str, Enum): """Contains possible statuses for channels""" + # subclass str to be able to serialize to json FREE = 'free' TAKEN = 'taken' @@ -90,6 +95,7 @@ def __init__(self, channels_seeds): def _get(self) -> str: """ Randomly get an available free channel from the dict + :return: a channel seed """ # Get a list of all free channels @@ -103,6 +109,7 @@ def _get(self) -> str: def _put(self, channel: str) -> None: """ Change a channel status back to FREE + :param str channel: the channel seed """ # Change channel state to free @@ -111,19 +118,16 @@ def _put(self, channel: str) -> None: def qsize(self) -> int: """ Counts free channels in the queue + :return: amount of free channels in the queue """ return len(self.get_free_channels()) def empty(self) -> bool: - """ - Used to check if the queue is empty - """ + """Used to check if the queue is empty""" return len(self.get_free_channels()) == 0 def get_free_channels(self) -> List[str]: - """ - Get a list of channels with "FREE" status - """ + """Get a list of channels with "FREE" status""" return [channel for channel, status in self._queue.items() if status == ChannelStatuses.FREE] diff --git a/kin/blockchain/environment.py b/kin/blockchain/environment.py index d513bf7..bde22c5 100644 --- a/kin/blockchain/environment.py +++ b/kin/blockchain/environment.py @@ -8,10 +8,11 @@ class Environment: - """Environments holds the parameters that will be used to connect to horizon""" def __init__(self, name: str, horizon_endpoint_uri: str, network_passphrase: str, friendbot_url: Optional[str] = None): """ + Environments holds the parameters that will be used to connect to horizon + :param name: Name of the environment. :param horizon_uri: a Horizon endpoint. :param network_passphrase: The passphrase/network_id of the environment. diff --git a/kin/blockchain/keypair.py b/kin/blockchain/keypair.py index b2307ba..f507e1b 100644 --- a/kin/blockchain/keypair.py +++ b/kin/blockchain/keypair.py @@ -17,6 +17,7 @@ class Keypair: def __init__(self, seed: Optional[str] = None): """ # Create an instance of Keypair. + :param seed: (Optional) The secret seed of an account """ self.secret_seed = seed or self.generate_seed() @@ -32,6 +33,7 @@ def __init__(self, seed: Optional[str] = None): def sign(self, data: bytes) -> DecoratedSignature: """ Sign any data using the keypair's private key + :param data: any data to sign :return: a decorated signature """ @@ -42,6 +44,7 @@ def sign(self, data: bytes) -> DecoratedSignature: def address_from_seed(seed: str) -> str: """ Get a public address from a secret seed. + :param seed: The secret seed of an account. :return: A public address. """ @@ -51,6 +54,7 @@ def address_from_seed(seed: str) -> str: def generate_seed() -> str: """ Generate a random secret seed. + :return: A secret seed. """ return BaseKeypair.random().seed().decode() @@ -59,6 +63,7 @@ def generate_seed() -> str: def generate_hd_seed(base_seed: str, salt: str) -> str: """ Generate a highly deterministic seed from a base seed + salt + :param base_seed: The base seed to generate a seed from :param salt: A unique string that will be used to generate the seed :return: a new seed. diff --git a/kin/client.py b/kin/client.py index 3c11a90..dd2545a 100644 --- a/kin/client.py +++ b/kin/client.py @@ -27,6 +27,7 @@ class KinClient: def __init__(self, environment: Environment): """Create a new instance of the KinClient to query the Kin blockchain. + :param environment: an environment for the client to point to. :return: An instance of the KinClient. @@ -54,6 +55,7 @@ def kin_account(self, seed: str, channel_secret_keys: Optional[List[str]] = None app_id: Optional[str] = ANON_APP_ID) -> KinAccount: """ Create a new instance of a KinAccount to perform authenticated operations on the blockchain. + :param seed: The secret seed of the account that will be used :param channel_secret_keys: A list of seeds to be used as channels :param app_id: the unique id of your app @@ -65,6 +67,7 @@ def kin_account(self, seed: str, channel_secret_keys: Optional[List[str]] = None async def get_config(self) -> dict: """Get system configuration data and online status. + :return: a dictionary containing the data :rtype: dict """ @@ -96,6 +99,7 @@ async def get_config(self) -> dict: async def get_minimum_fee(self) -> int: """ Get the current minimum fee acceptable for a tx + :return: The minimum fee """ params = {'order': 'desc', @@ -105,6 +109,7 @@ async def get_minimum_fee(self) -> int: async def get_account_balance(self, address: str) -> float: """ Get the KIN balance of a given account + :param address: the public address of the account to query :return: the balance of the account @@ -123,6 +128,7 @@ async def get_account_balance(self, address: str) -> float: async def does_account_exists(self, address: str) -> bool: """ Find out if a given account exists on the blockchain + :param address: The kin account to query about :return: does the account exists on the blockchain @@ -190,6 +196,7 @@ async def get_account_tx_history(self, address: str, amount: Optional[int] = 10, simple: Optional[bool] = True) -> List[Union[SimplifiedTransaction, RawTransaction]]: """ Get the transaction history for a given account. + :param address: The public address of the account to query :param amount: The maximum number of transactions to get :param descending: The order of the transactions, True will start from the latest one @@ -235,6 +242,7 @@ async def get_account_tx_history(self, address: str, amount: Optional[int] = 10, async def friendbot(self, address: str): """ Use the friendbot service to create and fund an account + :param address: The address to create and fund :return: the hash of the friendbot transaction diff --git a/kin/monitors.py b/kin/monitors.py index 059e234..5f714e9 100644 --- a/kin/monitors.py +++ b/kin/monitors.py @@ -14,6 +14,7 @@ async def single_monitor(kin_client: 'KinClient', address: str, timeout: Optional[float] = None) -> AsyncGenerator[SimplifiedTransaction, None]: """ Monitors a single account for kin payments + :param kin_client: a kin client directed to the correct network :param address: address to watch :param timeout: How long to wait for a new event @@ -42,6 +43,7 @@ async def single_monitor(kin_client: 'KinClient', address: str, async def multi_monitor(kin_client: 'KinClient', addresses: set) -> AsyncGenerator[SimplifiedTransaction, None]: """ Monitors a single account for kin payments + :param kin_client: a kin client directed to the correct network :param addresses: set of addresses to watch """ diff --git a/kin/transactions.py b/kin/transactions.py index 20d0f56..c99bcaa 100644 --- a/kin/transactions.py +++ b/kin/transactions.py @@ -22,9 +22,10 @@ class RawTransaction: - """Class to hold raw info about a transaction""" def __init__(self, horizon_tx_response: dict): """ + Class to hold raw info about a transaction + :param horizon_tx_response: the json response from an horizon query """ # Network_id is left as '' since we override the hash anyway @@ -34,9 +35,12 @@ def __init__(self, horizon_tx_response: dict): class SimplifiedTransaction: - """Class to hold simplified info about a transaction""" - def __init__(self, raw_tx: RawTransaction): + """ + Class to hold simplified info about a transaction + + :param raw_tx: The raw transaction object to simplify + """ self.id = raw_tx.hash self.timestamp = raw_tx.timestamp @@ -55,9 +59,12 @@ def __init__(self, raw_tx: RawTransaction): class SimplifiedOperation: - """Class to hold simplified info about a operation""" - def __init__(self, op_data: Union[CreateAccount, Payment]): + """ + Class to hold simplified info about a operation + + :param op_data: Operation to simplify + """ if isinstance(op_data, Payment): # Raise error if its not a KIN payment if op_data.asset.type != NATIVE_ASSET_TYPE: @@ -86,6 +93,7 @@ class OperationTypes(Enum): def build_memo(app_id: str, memo: Union[str, None]) -> str: """ Build a memo for a tx that fits the pre-defined template + :param app_id: The app_id to include in the memo :param memo: The memo to include :return: the finished memo @@ -100,6 +108,7 @@ def build_memo(app_id: str, memo: Union[str, None]) -> str: def decode_transaction(b64_tx: str, network_id: str, simple: Optional[bool] = True) -> Union[SimplifiedTransaction, RawTransaction]: """ Decode a base64 transaction envelop + :param b64_tx: a transaction envelop encoded in base64 :param simple: should the tx be simplified :param network_id: the network_id for the transaction @@ -130,6 +139,7 @@ def calculate_tx_hash(tx: BaseTransaction, network_passphrase_hash: bytes) -> st 1. A sha256 hash of the network_id + 2. The xdr representation of ENVELOP_TYPE_TX + 3. The xdr representation of the transaction + :param tx: The builder's transaction object :param network_passphrase_hash: The network passphrase hash :return: The hex encoded transaction hash diff --git a/kin/utils.py b/kin/utils.py index db34ade..61a3201 100644 --- a/kin/utils.py +++ b/kin/utils.py @@ -16,6 +16,7 @@ async def create_channels(master_seed: str, environment: Environment, amount: in starting_balance: float, salt: str) -> List[str]: """ Create HD seeds based on a master seed and salt + :param master_seed: The master seed that creates the seeds :param environment: The blockchain environment to create the seeds on :param amount: Number of seeds to create (Up to 100) @@ -63,6 +64,7 @@ async def create_channels(master_seed: str, environment: Environment, amount: in def get_hd_channels(master_seed: str, salt: str, amount: int) -> List[str]: """ Get a list of channels generated based on a seed and salt + :param master_seed: the base seed that created the channels :param salt: A string to be used to generate the seeds :param amount: Number of seeds to generate (Up to 100) From 779c6a263cd7392116cb5d03ea111b9d6fcad9b0 Mon Sep 17 00:00:00 2001 From: ronserruya Date: Wed, 27 Mar 2019 11:15:41 +0200 Subject: [PATCH 22/22] lock req-dev versions --- requirements-dev.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/requirements-dev.txt b/requirements-dev.txt index e7d7a2e..2c59dd8 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,5 +1,5 @@ -codecov -coverage -pytest-cov -pytest -pytest-asyncio +codecov==2.0.15 +coverage==4.5.3 +pytest-cov==2.6.1 +pytest==4.3.1 +pytest-asyncio==0.10.0