diff --git a/.travis.yml b/.travis.yml index 33763b03..916012f4 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,11 +1,9 @@ language: python python: - - '2.7' - - '3.5' - '3.6' - '3.7' install: - - pip install .[docs-builder] + - pip install .[docs-builder,test-runner] - pip install docutils pygments # Used to check package metadata. script: - python setup.py check --strict --metadata --restructuredtext diff --git a/README.md b/README.md index c13c6b72..390e9a5a 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Please report any issues in our [issue tracker](https://github.com/iotaledger/io ## Prerequisites -To install the IOTA Python client library and its dependencies, you need Python version 3.7, 3.6, 3.5, or 2.7 installed on your device. +To install the IOTA Python client library and its dependencies, you need Python version 3.7 or 3.6 installed on your device. ## Installation diff --git a/docs/README.rst b/docs/README.rst index a3a89889..740ed236 100644 --- a/docs/README.rst +++ b/docs/README.rst @@ -24,7 +24,7 @@ If you encounter any issues while using PyOTA, please report them using the ============ Dependencies ============ -PyOTA is compatible with Python 3.7, 3.6, 3.5 and 2.7 +PyOTA is compatible with Python 3.7 and 3.6. ============= Install PyOTA diff --git a/docs/getting_started.rst b/docs/getting_started.rst index 574ebb55..c8a01873 100644 --- a/docs/getting_started.rst +++ b/docs/getting_started.rst @@ -1,6 +1,6 @@ Installation ============ -PyOTA is compatible with Python 3.7, 3.6, 3.5 and 2.7. +PyOTA is compatible with Python 3.7 and 3.6. Install PyOTA using `pip`: diff --git a/examples/hello_world.py b/examples/hello_world.py index bf24d34a..b21b8c3f 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -11,8 +11,7 @@ from pprint import pprint from sys import argv from typing import Text - -from requests.exceptions import ConnectionError +from httpx.exceptions import NetworkError from six import text_type from iota import BadApiResponse, StrictIota, __version__ @@ -24,7 +23,7 @@ def main(uri): try: node_info = api.get_node_info() - except ConnectionError as e: + except NetworkError as e: print( "Hm. {uri} isn't responding; is the node running?".format(uri=uri) ) diff --git a/iota/__init__.py b/iota/__init__.py index 8ab602bb..65e1da73 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -35,6 +35,7 @@ from .types import * from .transaction import * from .adapter import * +from .api_async import * from .api import * from .trits import * diff --git a/iota/adapter/__init__.py b/iota/adapter/__init__.py index 45862008..ae0a12f5 100644 --- a/iota/adapter/__init__.py +++ b/iota/adapter/__init__.py @@ -9,8 +9,8 @@ from logging import DEBUG, Logger from socket import getdefaulttimeout as get_default_timeout from typing import Container, Dict, List, Optional, Text, Tuple, Union - -from requests import Response, auth, codes, request +from httpx import AsyncClient, Response, codes, auth +import asyncio from six import PY2, binary_type, iteritems, moves as compat, text_type, \ add_metaclass @@ -59,6 +59,15 @@ # noinspection PyCompatibility,PyUnresolvedReferences from urllib.parse import SplitResult +def async_return(result): + """ + Turns 'result' into a `Future` object with 'result' value. + + Important for mocking, as we can await the mock's return value. + """ + f = asyncio.Future() + f.set_result(result) + return f class BadApiResponse(ValueError): """ @@ -271,6 +280,7 @@ def __init__(self, uri, timeout=None, authentication=None): # type: (Union[Text, SplitResult], Optional[int]) -> None super(HttpAdapter, self).__init__() + self.client = AsyncClient() self.timeout = timeout self.authentication = authentication @@ -331,13 +341,13 @@ def get_uri(self): # type: () -> Text return self.uri.geturl() - def send_request(self, payload, **kwargs): + async def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict kwargs.setdefault('headers', {}) for key, value in iteritems(self.DEFAULT_HEADERS): kwargs['headers'].setdefault(key, value) - response = self._send_http_request( + response = await self._send_http_request( # Use a custom JSON encoder that knows how to convert Tryte # values. payload=JsonEncoder().encode(payload), @@ -346,9 +356,9 @@ def send_request(self, payload, **kwargs): **kwargs ) - return self._interpret_response(response, payload, {codes['ok']}) + return self._interpret_response(response, payload, {codes['OK']}) - def _send_http_request(self, url, payload, method='post', **kwargs): + async def _send_http_request(self, url, payload, method='post', **kwargs): # type: (Text, Optional[Text], Text, dict) -> Response """ Sends the actual HTTP request. @@ -380,8 +390,7 @@ def _send_http_request(self, url, payload, method='post', **kwargs): 'request_url': url, }, ) - - response = request(method=method, url=url, data=payload, **kwargs) + response = await self.client.request(method=method, url=url, data=payload, **kwargs) self._log( level=DEBUG, @@ -474,9 +483,9 @@ def _interpret_response(self, response, payload, expected_status): error = None try: - if response.status_code == codes['bad_request']: + if response.status_code == codes['BAD_REQUEST']: error = decoded['error'] - elif response.status_code == codes['internal_server_error']: + elif response.status_code == codes['INTERNAL_SERVER_ERROR']: error = decoded['exception'] except KeyError: pass @@ -585,7 +594,10 @@ def seed_response(self, command, response): self.responses[command].append(response) return self - def send_request(self, payload, **kwargs): + async def send_request(self, payload, **kwargs): + """ + Mimic asynchronous behavior of `HttpAdapter.send_request`. + """ # type: (dict, dict) -> dict # Store a snapshot so that we can inspect the request later. self.requests.append(dict(payload)) @@ -627,4 +639,4 @@ def send_request(self, payload, **kwargs): raise with_context(BadApiResponse(error), context={'request': payload}) - return response + return await async_return(response) diff --git a/iota/adapter/wrappers.py b/iota/adapter/wrappers.py index c4f4b6cb..9fff6720 100644 --- a/iota/adapter/wrappers.py +++ b/iota/adapter/wrappers.py @@ -144,8 +144,8 @@ def get_adapter(self, command): """ return self.routes.get(command, self.adapter) - def send_request(self, payload, **kwargs): + async def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict command = payload.get('command') - return self.get_adapter(command).send_request(payload, **kwargs) + return await self.get_adapter(command).send_request(payload, **kwargs) diff --git a/iota/api.py b/iota/api.py index b4208907..4976a30a 100644 --- a/iota/api.py +++ b/iota/api.py @@ -1,16 +1,10 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, \ - unicode_literals - from typing import Dict, Iterable, Optional, Text from iota import AdapterSpec, Address, BundleHash, ProposedTransaction, Tag, \ - TransactionHash, TransactionTrytes, TryteString, TrytesCompatible -from iota.adapter import BaseAdapter, resolve_adapter -from iota.commands import BaseCommand, CustomCommand, core, extended -from iota.commands.extended.helpers import Helpers + TransactionHash, TransactionTrytes, TryteString from iota.crypto.addresses import AddressGenerator -from iota.crypto.types import Seed +from iota.api_async import AsyncStrictIota, AsyncIota +import asyncio __all__ = [ 'InvalidCommand', @@ -25,9 +19,33 @@ class InvalidCommand(ValueError): """ pass -class StrictIota(object): +# There is a compact and easy way to create the synchronous version of the async +# classes: + +# import inspect +# def make_synchronous(new_name, async_class: type): +# def make_sync(method): +# def sync_version(*args, **kwargs): +# return asyncio.get_event_loop().run_until_complete(method(*args, **kwargs)) +# return sync_version + +# return type(new_name, (async_class,), { +# name: make_sync(method) if inspect.iscoroutinefunction(method) else method +# for name, method in inspect.getmembers(async_class) +# }) + +# # create the sync version of the class +# Iota = make_synchronous('Iota', AsyncIota) + +# While this approach would work, no IDE static analysis would pick up the +# method definitions or docstrings for the new `Iota` class, meaning no +# suggestions, intellisense, code completion, etc. for the user. +# Therefore we keep the manual approach. + + +class StrictIota(AsyncStrictIota): """ - API to send HTTP requests for communicating with an IOTA node. + Synchronous API to send HTTP requests for communicating with an IOTA node. This implementation only exposes the "core" API methods. For a more feature-complete implementation, use :py:class:`Iota` instead. @@ -72,69 +90,8 @@ def __init__(self, adapter, devnet=False, local_pow=False): See :ref:`Optional Local Pow` for more info and :ref:`find out` how to use it. - - """ - super(StrictIota, self).__init__() - - if not isinstance(adapter, BaseAdapter): - adapter = resolve_adapter(adapter) - - self.adapter = adapter # type: BaseAdapter - # Note that the `local_pow` parameter is passed to adapter, - # the api class has no notion about it. The reason being, - # that this parameter is used in `AttachToTangeCommand` calls, - # that is called from various api calls (`attach_to_tangle`, - # `send_trytes` or `send_transfer`). Inside `AttachToTangeCommand`, - # we no longer have access to the attributes of the API class, therefore - # `local_pow` needs to be associated with the adapter. - # Logically, `local_pow` will decide if the api call does pow - # via pyota-pow extension, or sends the request to a node. - # But technically, the parameter belongs to the adapter. - self.adapter.set_local_pow(local_pow) - self.devnet = devnet - - def create_command(self, command): - # type: (Text) -> CustomCommand - """ - Creates a pre-configured CustomCommand instance. - - This method is useful for invoking undocumented or experimental - methods, or if you just want to troll your node for awhile. - - :param Text command: - The name of the command to create. - """ - return CustomCommand(self.adapter, command) - - def set_local_pow(self, local_pow): - # type: (bool) -> None - """ - Sets the :py:attr:`local_pow` attribute of the adapter of the api - instance. If it is ``True``, :py:meth:`attach_to_tangle` command calls - external interface to perform proof of work, instead of sending the - request to a node. - - By default, :py:attr:`local_pow` is set to ``False``. - This particular method is needed if one wants to change - local_pow behavior dynamically. - - :param bool local_pow: - Whether to perform pow locally. - - :returns: None - - """ - self.adapter.set_local_pow(local_pow) - - @property - def default_min_weight_magnitude(self): - # type: () -> int - """ - Returns the default ``min_weight_magnitude`` value to use for - API requests. - """ - return 9 if self.devnet else 14 + super().__init__(adapter, devnet, local_pow) def add_neighbors(self, uris): # type: (Iterable[Text]) -> dict @@ -165,7 +122,12 @@ def add_neighbors(self, uris): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#addneighbors """ - return core.AddNeighborsCommand(self.adapter)(uris=uris) + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().add_neighbors(uris) + ) def attach_to_tangle( self, @@ -211,14 +173,16 @@ def attach_to_tangle( - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#attachtotangle """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - return core.AttachToTangleCommand(self.adapter)( - trunkTransaction=trunk_transaction, - branchTransaction=branch_transaction, - minWeightMagnitude=min_weight_magnitude, - trytes=trytes, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().attach_to_tangle( + trunk_transaction, + branch_transaction, + trytes, + min_weight_magnitude, + ) ) def broadcast_transactions(self, trytes): @@ -244,7 +208,14 @@ def broadcast_transactions(self, trytes): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#broadcasttransactions """ - return core.BroadcastTransactionsCommand(self.adapter)(trytes=trytes) + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().broadcast_transactions( + trytes, + ) + ) def check_consistency(self, tails): # type: (Iterable[TransactionHash]) -> dict @@ -276,8 +247,13 @@ def check_consistency(self, tails): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#checkconsistency """ - return core.CheckConsistencyCommand(self.adapter)( - tails=tails, + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().check_consistency( + tails, + ) ) def find_transactions( @@ -323,11 +299,16 @@ def find_transactions( - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#findtransactions """ - return core.FindTransactionsCommand(self.adapter)( - bundles=bundles, - addresses=addresses, - tags=tags, - approvees=approvees, + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().find_transactions( + bundles, + addresses, + tags, + approvees, + ) ) def get_balances( @@ -378,10 +359,15 @@ def get_balances( - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getbalances """ - return core.GetBalancesCommand(self.adapter)( - addresses=addresses, - threshold=threshold, - tips=tips, + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_balances( + addresses, + threshold, + tips, + ) ) def get_inclusion_states(self, transactions, tips): @@ -416,9 +402,14 @@ def get_inclusion_states(self, transactions, tips): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getinclusionstates """ - return core.GetInclusionStatesCommand(self.adapter)( - transactions=transactions, - tips=tips, + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_inclusion_states( + transactions, + tips, + ) ) def get_missing_transactions(self): @@ -441,7 +432,12 @@ def get_missing_transactions(self): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getmissingtransactions """ - return core.GetMissingTransactionsCommand(self.adapter)() + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_missing_transactions() + ) def get_neighbors(self): # type: () -> dict @@ -474,7 +470,12 @@ def get_neighbors(self): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getneighbors """ - return core.GetNeighborsCommand(self.adapter)() + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_neighbors() + ) def get_node_api_configuration(self): # type: () -> dict @@ -498,7 +499,12 @@ def get_node_api_configuration(self): - https://docs.iota.org/docs/node-software/0.1/iri/references/iri-configuration-options - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getnodeapiconfiguration """ - return core.GetNodeAPIConfigurationCommand(self.adapter)() + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_node_api_configuration() + ) def get_node_info(self): # type: () -> dict @@ -557,7 +563,12 @@ def get_node_info(self): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getnodeinfo """ - return core.GetNodeInfoCommand(self.adapter)() + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_node_info() + ) def get_tips(self): # type: () -> dict @@ -580,7 +591,12 @@ def get_tips(self): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#gettips - https://docs.iota.org/docs/dev-essentials/0.1/references/glossary """ - return core.GetTipsCommand(self.adapter)() + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_tips() + ) def get_transactions_to_approve(self, depth, reference=None): # type: (int, Optional[TransactionHash]) -> dict @@ -616,9 +632,14 @@ def get_transactions_to_approve(self, depth, reference=None): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#gettransactionstoapprove """ - return core.GetTransactionsToApproveCommand(self.adapter)( - depth=depth, - reference=reference, + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_transactions_to_approve( + depth, + reference, + ) ) def get_trytes(self, hashes): @@ -647,7 +668,14 @@ def get_trytes(self, hashes): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#gettrytes """ - return core.GetTrytesCommand(self.adapter)(hashes=hashes) + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_trytes( + hashes, + ) + ) def interrupt_attaching_to_tangle(self): # type: () -> dict @@ -667,7 +695,12 @@ def interrupt_attaching_to_tangle(self): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#interruptattachingtotangle """ - return core.InterruptAttachingToTangleCommand(self.adapter)() + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().interrupt_attaching_to_tangle() + ) def remove_neighbors(self, uris): # type: (Iterable[Text]) -> dict @@ -693,7 +726,12 @@ def remove_neighbors(self, uris): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#removeneighbors """ - return core.RemoveNeighborsCommand(self.adapter)(uris=uris) + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().remove_neighbors(uris) + ) def store_transactions(self, trytes): # type: (Iterable[TryteString]) -> dict @@ -720,7 +758,12 @@ def store_transactions(self, trytes): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#storetransactions """ - return core.StoreTransactionsCommand(self.adapter)(trytes=trytes) + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().store_transactions(trytes) + ) def were_addresses_spent_from(self, addresses): # type: (Iterable[Address]) -> dict @@ -749,15 +792,18 @@ def were_addresses_spent_from(self, addresses): - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#wereaddressesspentfrom """ - return core.WereAddressesSpentFromCommand(self.adapter)( - addresses=addresses, + + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().were_addresses_spent_from(addresses) ) -class Iota(StrictIota): +class Iota(StrictIota, AsyncIota): """ - Implements the core API, plus additional wrapper methods for common - operations. + Implements the synchronous core API, plus additional synchronous wrapper + methods for common operations. :param AdapterSpec adapter: URI string or BaseAdapter instance. @@ -801,10 +847,8 @@ def __init__(self, adapter, seed=None, devnet=False, local_pow=False): .. note:: This value is never transferred to the node/network. """ - super(Iota, self).__init__(adapter, devnet, local_pow) - - self.seed = Seed(seed) if seed else Seed.random() - self.helpers = Helpers(self) + # Explicitly call AsyncIota's init, as we need the seed + AsyncIota.__init__(self, adapter, seed, devnet, local_pow) def broadcast_and_store(self, trytes): # type: (Iterable[TransactionTrytes]) -> dict @@ -827,7 +871,11 @@ def broadcast_and_store(self, trytes): - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore """ - return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes) + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().broadcast_and_store(trytes) + ) def broadcast_bundle(self, tail_transaction_hash): # type (TransactionHash) -> dict @@ -852,7 +900,11 @@ def broadcast_bundle(self, tail_transaction_hash): - https://github.com/iotaledger/iota.js/blob/next/api_reference.md#module_core.broadcastBundle """ - return extended.BroadcastBundleCommand(self.adapter)(tail_hash=tail_transaction_hash) + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().broadcast_bundle(tail_transaction_hash) + ) def find_transaction_objects( self, @@ -866,7 +918,7 @@ def find_transaction_objects( A more extensive version of :py:meth:`find_transactions` that returns transaction objects instead of hashes. - Effectively, this is :py:meth:`find_transactions` + + Effectively, this is :py:meth:`find_transactions` + :py:meth:`get_trytes` + converting the trytes into transaction objects. @@ -899,11 +951,15 @@ def find_transaction_objects( } """ - return extended.FindTransactionObjectsCommand(self.adapter)( - bundles=bundles, - addresses=addresses, - tags=tags, - approvees=approvees, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().find_transaction_objects( + bundles, + addresses, + tags, + approvees, + ) ) def get_account_data(self, start=0, stop=None, inclusion_states=False, security_level=None): @@ -975,12 +1031,15 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False, security_ } """ - return extended.GetAccountDataCommand(self.adapter)( - seed=self.seed, - start=start, - stop=stop, - inclusionStates=inclusion_states, - security_level=security_level + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_account_data( + start, + stop, + inclusion_states, + security_level, + ) ) def get_bundles(self, transactions): @@ -1009,7 +1068,11 @@ def get_bundles(self, transactions): - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle """ - return extended.GetBundlesCommand(self.adapter)(transactions=transactions) + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_bundles(transactions) + ) def get_inputs( self, @@ -1115,12 +1178,15 @@ def get_inputs( - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs """ - return extended.GetInputsCommand(self.adapter)( - seed=self.seed, - start=start, - stop=stop, - threshold=threshold, - securityLevel=security_level + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_inputs( + start, + stop, + threshold, + security_level, + ) ) def get_latest_inclusion(self, hashes): @@ -1145,7 +1211,11 @@ def get_latest_inclusion(self, hashes): } """ - return extended.GetLatestInclusionCommand(self.adapter)(hashes=hashes) + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_latest_inclusion(hashes) + ) def get_new_addresses( self, @@ -1208,12 +1278,15 @@ def get_new_addresses( - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress """ - return extended.GetNewAddressesCommand(self.adapter)( - count=count, - index=index, - securityLevel=security_level, - checksum=checksum, - seed=self.seed, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_new_addresses( + count=count, + index=index, + security_level=security_level, + checksum=checksum, + ) ) def get_transaction_objects( @@ -1242,8 +1315,10 @@ def get_transaction_objects( List of Transaction objects that match the input. } """ - return extended.GetTransactionObjectsCommand(self.adapter)( - hashes=hashes, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_transaction_objects(hashes) ) def get_transfers(self, start=0, stop=None, inclusion_states=False): @@ -1299,11 +1374,14 @@ def get_transfers(self, start=0, stop=None, inclusion_states=False): - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers """ - return extended.GetTransfersCommand(self.adapter)( - seed=self.seed, - start=start, - stop=stop, - inclusionStates=inclusion_states, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().get_transfers( + start, + stop, + inclusion_states, + ) ) def is_promotable( @@ -1341,8 +1419,10 @@ def is_promotable( References: - https://github.com/iotaledger/iota.js/blob/next/api_reference.md#module_core.isPromotable """ - return extended.IsPromotableCommand(self.adapter)( - tails=tails, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().is_promotable(tails) ) def prepare_transfer( @@ -1399,12 +1479,15 @@ def prepare_transfer( - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers """ - return extended.PrepareTransferCommand(self.adapter)( - seed=self.seed, - transfers=transfers, - inputs=inputs, - changeAddress=change_address, - securityLevel=security_level, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().prepare_transfer( + transfers, + inputs, + change_address, + security_level, + ) ) def promote_transaction( @@ -1438,13 +1521,14 @@ def promote_transaction( The newly-published bundle. } """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - - return extended.PromoteTransactionCommand(self.adapter)( - transaction=transaction, - depth=depth, - minWeightMagnitude=min_weight_magnitude, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().promote_transaction( + transaction, + depth, + min_weight_magnitude, + ) ) def replay_bundle( @@ -1484,13 +1568,14 @@ def replay_bundle( - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#replaytransfer """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - - return extended.ReplayBundleCommand(self.adapter)( - transaction=transaction, - depth=depth, - minWeightMagnitude=min_weight_magnitude, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().replay_bundle( + transaction, + depth, + min_weight_magnitude, + ) ) def send_transfer( @@ -1553,17 +1638,17 @@ def send_transfer( - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtransfer """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - - return extended.SendTransferCommand(self.adapter)( - seed=self.seed, - depth=depth, - transfers=transfers, - inputs=inputs, - changeAddress=change_address, - minWeightMagnitude=min_weight_magnitude, - securityLevel=security_level, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().send_transfer( + transfers, + depth, + inputs, + change_address, + min_weight_magnitude, + security_level, + ) ) def send_trytes(self, trytes, depth=3, min_weight_magnitude=None): @@ -1597,13 +1682,14 @@ def send_trytes(self, trytes, depth=3, min_weight_magnitude=None): - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - - return extended.SendTrytesCommand(self.adapter)( - trytes=trytes, - depth=depth, - minWeightMagnitude=min_weight_magnitude, + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().send_trytes( + trytes, + depth, + min_weight_magnitude, + ) ) def is_reattachable(self, addresses): @@ -1633,8 +1719,12 @@ def is_reattachable(self, addresses): } """ - return extended.IsReattachableCommand(self.adapter)( - addresses=addresses + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().is_reattachable( + addresses, + ) ) def traverse_bundle(self, tail_hash): @@ -1661,6 +1751,10 @@ def traverse_bundle(self, tail_hash): } """ - return extended.TraverseBundleCommand(self.adapter)( - transaction=tail_hash + # Execute original coroutine inside an event loop to make this method + # synchronous + return asyncio.get_event_loop().run_until_complete( + super().traverse_bundle( + tail_hash, + ) ) diff --git a/iota/api_async.py b/iota/api_async.py new file mode 100644 index 00000000..705e2e70 --- /dev/null +++ b/iota/api_async.py @@ -0,0 +1,1658 @@ +from typing import Dict, Iterable, Optional, Text + +from iota import AdapterSpec, Address, BundleHash, ProposedTransaction, Tag, \ + TransactionHash, TransactionTrytes, TryteString, TrytesCompatible +from iota.adapter import BaseAdapter, resolve_adapter +from iota.commands import BaseCommand, CustomCommand, core, extended +from iota.crypto.addresses import AddressGenerator +from iota.crypto.types import Seed +import asyncio + +__all__ = [ + 'AsyncIota', + 'AsyncStrictIota', +] + + +class AsyncStrictIota: + """ + Asynchronous API to send HTTP requests for communicating with an IOTA node. + + This implementation only exposes the "core" API methods. For a more + feature-complete implementation, use :py:class:`Iota` instead. + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference + + :param AdapterSpec adapter: + URI string or BaseAdapter instance. + + :param Optional[bool] devnet: + Whether to use devnet settings for this instance. + On the devnet, minimum weight magnitude is set to 9, on mainnet + it is 1 by default. + + :param Optional[bool] local_pow: + Whether to perform proof-of-work locally by redirecting all calls + to :py:meth:`attach_to_tangle` to + `ccurl pow interface `_. + + See :ref:`Optional Local Pow` for more info and + :ref:`find out` how to use it. + + """ + + def __init__(self, adapter, devnet=False, local_pow=False): + # type: (AdapterSpec, bool, bool) -> None + """ + :param AdapterSpec adapter: + URI string or BaseAdapter instance. + + :param bool devnet: + Whether to use devnet settings for this instance. + On the devnet, minimum weight magnitude is set to 9, on mainnet + it is 1 by default. + + :param Optional[bool] local_pow: + Whether to perform proof-of-work locally by redirecting all calls + to :py:meth:`attach_to_tangle` to + `ccurl pow interface `_. + + See :ref:`Optional Local Pow` for more info and + :ref:`find out` how to use it. + """ + super().__init__() + + if not isinstance(adapter, BaseAdapter): + adapter = resolve_adapter(adapter) + + self.adapter = adapter # type: BaseAdapter + # Note that the `local_pow` parameter is passed to adapter, + # the api class has no notion about it. The reason being, + # that this parameter is used in `AttachToTangeCommand` calls, + # that is called from various api calls (`attach_to_tangle`, + # `send_trytes` or `send_transfer`). Inside `AttachToTangeCommand`, + # we no longer have access to the attributes of the API class, therefore + # `local_pow` needs to be associated with the adapter. + # Logically, `local_pow` will decide if the api call does pow + # via pyota-pow extension, or sends the request to a node. + # But technically, the parameter belongs to the adapter. + self.adapter.set_local_pow(local_pow) + self.devnet = devnet + + def create_command(self, command): + # type: (Text) -> CustomCommand + """ + Creates a pre-configured CustomCommand instance. + + This method is useful for invoking undocumented or experimental + methods, or if you just want to troll your node for awhile. + + :param Text command: + The name of the command to create. + + """ + return CustomCommand(self.adapter, command) + + def set_local_pow(self, local_pow): + # type: (bool) -> None + """ + Sets the :py:attr:`local_pow` attribute of the adapter of the api + instance. If it is ``True``, :py:meth:`~Iota.attach_to_tangle` command calls + external interface to perform proof of work, instead of sending the + request to a node. + + By default, :py:attr:`local_pow` is set to ``False``. + This particular method is needed if one wants to change + local_pow behavior dynamically. + + :param bool local_pow: + Whether to perform pow locally. + + :returns: None + + """ + self.adapter.set_local_pow(local_pow) + + @property + def default_min_weight_magnitude(self): + # type: () -> int + """ + Returns the default ``min_weight_magnitude`` value to use for + API requests. + """ + return 9 if self.devnet else 14 + + async def add_neighbors(self, uris): + # type: (Iterable[Text]) -> dict + """ + Add one or more neighbors to the node. Lasts until the node is + restarted. + + :param Iterable[Text] uris: + Use format ``://:``. + Example: ``add_neighbors(['udp://example.com:14265'])`` + + .. note:: + These URIs are for node-to-node communication (e.g., + weird things will happen if you specify a node's HTTP + API URI here). + + :return: + ``dict`` with the following structure:: + + { + 'addedNeighbors': int, + Total number of added neighbors. + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#addneighbors + """ + return await core.AddNeighborsCommand(self.adapter)(uris=uris) + + async def attach_to_tangle( + self, + trunk_transaction, # type: TransactionHash + branch_transaction, # type: TransactionHash + trytes, # type: Iterable[TryteString] + min_weight_magnitude=None, # type: Optional[int] + ): + # type: (...) -> dict + """ + Attaches the specified transactions (trytes) to the Tangle by + doing Proof of Work. You need to supply branchTransaction as + well as trunkTransaction (basically the tips which you're going + to validate and reference with this transaction) - both of which + you'll get through the :py:meth:`get_transactions_to_approve` API call. + + The returned value is a different set of tryte values which you + can input into :py:meth:`broadcast_transactions` and + :py:meth:`store_transactions`. + + :param TransactionHash trunk_transaction: + Trunk transaction hash. + + :param TransactionHash branch_transaction: + Branch transaction hash. + + :param Iterable[TransactionTrytes] trytes: + List of transaction trytes in the bundle to be attached. + + :param Optional[int] min_weight_magnitude: + Minimum weight magnitude to be used for attaching trytes. + 14 by default on mainnet, 9 on devnet/devnet. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': List[TransactionTrytes], + Transaction trytes that include a valid nonce field. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#attachtotangle + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return await core.AttachToTangleCommand(self.adapter)( + trunkTransaction=trunk_transaction, + branchTransaction=branch_transaction, + minWeightMagnitude=min_weight_magnitude, + trytes=trytes, + ) + + async def broadcast_transactions(self, trytes): + # type: (Iterable[TryteString]) -> dict + """ + Broadcast a list of transactions to all neighbors. + + The input trytes for this call are provided by + :py:meth:`attach_to_tangle`. + + :param Iterable[TransactionTrytes] trytes: + List of transaction trytes to be broadcast. + + :return: + ``dict`` with the following structure:: + + { + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#broadcasttransactions + """ + return await core.BroadcastTransactionsCommand(self.adapter)(trytes=trytes) + + async def check_consistency(self, tails): + # type: (Iterable[TransactionHash]) -> dict + """ + Used to ensure tail resolves to a consistent ledger which is + necessary to validate before attempting promotion. Checks + transaction hashes for promotability. + + This is called with a pending transaction (or more of them) and + it will tell you if it is still possible for this transaction + (or all the transactions simultaneously if you give more than + one) to be confirmed, or not (because it conflicts with another + already confirmed transaction). + + :param Iterable[TransactionHash] tails: + Transaction hashes. Must be tail transactions. + + :return: + ``dict`` with the following structure:: + + { + 'state': bool, + Whether tails resolve to consistent ledger. + 'info': Text, + This field will only exist if 'state' is ``False``. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#checkconsistency + """ + return await core.CheckConsistencyCommand(self.adapter)( + tails=tails, + ) + + async def find_transactions( + self, + bundles=None, # type: Optional[Iterable[BundleHash]] + addresses=None, # type: Optional[Iterable[Address]] + tags=None, # type: Optional[Iterable[Tag]] + approvees=None, # type: Optional[Iterable[TransactionHash]] + ): + # type: (...) -> dict + """ + Find the transactions which match the specified input and + return. + + All input values are lists, for which a list of return values + (transaction hashes), in the same order, is returned for all + individual elements. + + Using multiple of these input fields returns the intersection of + the values. + + :param Optional[Iterable[BundleHash] bundles: + List of bundle IDs. + + :param Optional[Iterable[Address]] addresses: + List of addresses. + + :param Optional[Iterable[Tag]] tags: + List of tags. + + :param Optional[Iterable[TransactionHash]] approvees: + List of approvee transaction IDs. + + :return: + ``dict`` with the following structure:: + + { + 'hashes': List[TransationHash], + Found transactions. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#findtransactions + """ + return await core.FindTransactionsCommand(self.adapter)( + bundles=bundles, + addresses=addresses, + tags=tags, + approvees=approvees, + ) + + async def get_balances( + self, + addresses, # type: Iterable[Address] + threshold=100, # type: int + tips=None, # type: Optional[Iterable[TransactionHash]] + ): + # type: (...) -> dict + """ + Similar to :py:meth:`get_inclusion_states`. Returns the + confirmed balance which a list of addresses have at the latest + confirmed milestone. + + In addition to the balances, it also returns the milestone as + well as the index with which the confirmed balance was + determined. The balances are returned as a list in the same + order as the addresses were provided as input. + + :param Iterable[Address] addresses: + List of addresses to get the confirmed balance for. + + :param int threshold: + Confirmation threshold between 0 and 100. + + :param Optional[Iterable[TransactionHash]] tips: + Tips whose history of transactions to traverse to find the balance. + + :return: + ``dict`` with the following structure:: + + { + 'balances': List[int], + List of balances in the same order as the addresses + parameters that were passed to the endpoint. + 'references': List[TransactionHash], + The referencing tips. If no tips parameter was passed + to the endpoint, this field contains the hash of the + latest milestone that confirmed the balance. + 'milestoneIndex': int, + The index of the milestone that confirmed the most + recent balance. + 'duration': int, + Number of milliseconds it took to process the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getbalances + """ + return await core.GetBalancesCommand(self.adapter)( + addresses=addresses, + threshold=threshold, + tips=tips, + ) + + async def get_inclusion_states(self, transactions, tips): + # type: (Iterable[TransactionHash], Iterable[TransactionHash]) -> dict + """ + Get the inclusion states of a set of transactions. This is for + determining if a transaction was accepted and confirmed by the + network or not. You can search for multiple tips (and thus, + milestones) to get past inclusion states of transactions. + + :param Iterable[TransactionHash] transactions: + List of transactions you want to get the inclusion state + for. + + :param Iterable[TransactionHash] tips: + List of tips (including milestones) you want to search for + the inclusion state. + + :return: + ``dict`` with the following structure:: + + { + 'states': List[bool], + List of boolean values in the same order as the + transactions parameters. A ``True`` value means the + transaction was confirmed. + 'duration': int, + Number of milliseconds it took to process the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getinclusionstates + """ + return await core.GetInclusionStatesCommand(self.adapter)( + transactions=transactions, + tips=tips, + ) + + async def get_missing_transactions(self): + # type: () -> dict + """ + Returns all transaction hashes that a node is currently requesting + from its neighbors. + + :return: + ``dict`` with the following structure:: + + { + 'hashes': List[TransactionHash], + Array of missing transaction hashes. + 'duration': int, + Number of milliseconds it took to process the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getmissingtransactions + """ + return await core.GetMissingTransactionsCommand(self.adapter)() + + async def get_neighbors(self): + # type: () -> dict + """ + Returns the set of neighbors the node is connected with, as well + as their activity count. + + The activity counter is reset after restarting IRI. + + :return: + ``dict`` with the following structure:: + + { + 'neighbors': List[dict], + Array of objects, including the following fields with + example values: + "address": "/8.8.8.8:14265", + "numberOfAllTransactions": 158, + "numberOfRandomTransactionRequests": 271, + "numberOfNewTransactions": 956, + "numberOfInvalidTransactions": 539, + "numberOfStaleTransactions": 663, + "numberOfSentTransactions": 672, + "connectiontype": "TCP" + 'duration': int, + Number of milliseconds it took to process the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getneighbors + """ + return await core.GetNeighborsCommand(self.adapter)() + + async def get_node_api_configuration(self): + # type: () -> dict + """ + Returns a node's API configuration settings. + + :return: + ``dict`` with the following structure:: + + { + '': type, + Configuration parameters for a node. + ... + ... + ... + + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/iri-configuration-options + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getnodeapiconfiguration + """ + return await core.GetNodeAPIConfigurationCommand(self.adapter)() + + async def get_node_info(self): + # type: () -> dict + """ + Returns information about the node. + + :return: + ``dict`` with the following structure:: + + { + 'appName': Text, + Name of the IRI network. + 'appVersion': Text, + Version of the IRI. + 'jreAvailableProcessors': int, + Available CPU cores on the node. + 'jreFreeMemory': int, + Amount of free memory in the Java virtual machine. + 'jreMaxMemory': int, + Maximum amount of memory that the Java virtual machine + can use, + 'jreTotalMemory': int, + Total amount of memory in the Java virtual machine. + 'jreVersion': Text, + The version of the Java runtime environment. + 'latestMilestone': TransactionHash + Transaction hash of the latest milestone. + 'latestMilestoneIndex': int, + Index of the latest milestone. + 'latestSolidSubtangleMilestone': TransactionHash, + Transaction hash of the latest solid milestone. + 'latestSolidSubtangleMilestoneIndex': int, + Index of the latest solid milestone. + 'milestoneStartIndex': int, + Start milestone for the current version of the IRI. + 'neighbors': int, + Total number of connected neighbor nodes. + 'packetsQueueSize': int, + Size of the packet queue. + 'time': int, + Current UNIX timestamp. + 'tips': int, + Number of tips in the network. + 'transactionsToRequest': int, + Total number of transactions that the node is missing in + its ledger. + 'features': List[Text], + Enabled configuration options. + 'coordinatorAddress': Address, + Address (Merkle root) of the Coordinator. + 'duration': int, + Number of milliseconds it took to process the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#getnodeinfo + """ + return await core.GetNodeInfoCommand(self.adapter)() + + async def get_tips(self): + # type: () -> dict + """ + Returns the list of tips (transactions which have no other + transactions referencing them). + + :return: + ``dict`` with the following structure:: + + { + 'hashes': List[TransactionHash], + List of tip transaction hashes. + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#gettips + - https://docs.iota.org/docs/dev-essentials/0.1/references/glossary + """ + return await core.GetTipsCommand(self.adapter)() + + async def get_transactions_to_approve(self, depth, reference=None): + # type: (int, Optional[TransactionHash]) -> dict + """ + Tip selection which returns ``trunkTransaction`` and + ``branchTransaction``. + + :param int depth: + Number of milestones to go back to start the tip selection algorithm. + + The higher the depth value, the more "babysitting" the node + will perform for the network (as it will confirm more + transactions that way). + + :param TransactionHash reference: + Transaction hash from which to start the weighted random walk. + Use this parameter to make sure the returned tip transaction hashes + approve a given reference transaction. + + :return: + ``dict`` with the following structure:: + + { + 'trunkTransaction': TransactionHash, + Valid trunk transaction hash. + 'branchTransaction': TransactionHash, + Valid branch transaction hash. + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#gettransactionstoapprove + """ + return await core.GetTransactionsToApproveCommand(self.adapter)( + depth=depth, + reference=reference, + ) + + async def get_trytes(self, hashes): + # type: (Iterable[TransactionHash]) -> dict + """ + Returns the raw transaction data (trytes) of one or more + transactions. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': List[TransactionTrytes], + List of transaction trytes for the given transaction + hashes (in the same order as the parameters). + 'duration': int, + Number of milliseconds it took to complete the request. + } + + .. note:: + If a node doesn't have the trytes for a given transaction hash in + its ledger, the value at the index of that transaction hash is either + ``null`` or a string of 9s. + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#gettrytes + """ + return await core.GetTrytesCommand(self.adapter)(hashes=hashes) + + async def interrupt_attaching_to_tangle(self): + # type: () -> dict + """ + Interrupts and completely aborts the :py:meth:`attach_to_tangle` + process. + + :return: + ``dict`` with the following structure:: + + { + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#interruptattachingtotangle + """ + return await core.InterruptAttachingToTangleCommand(self.adapter)() + + async def remove_neighbors(self, uris): + # type: (Iterable[Text]) -> dict + """ + Removes one or more neighbors from the node. Lasts until the + node is restarted. + + :param Text uris: + Use format ``://:``. + Example: `remove_neighbors(['udp://example.com:14265'])` + + :return: + ``dict`` with the following structure:: + + { + 'removedNeighbors': int, + Total number of removed neighbors. + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#removeneighbors + """ + return await core.RemoveNeighborsCommand(self.adapter)(uris=uris) + + async def store_transactions(self, trytes): + # type: (Iterable[TryteString]) -> dict + """ + Store transactions into local storage of the node. + + The input trytes for this call are provided by + :py:meth:`attach_to_tangle`. + + :param TransactionTrytes trytes: + Valid transaction trytes returned by :py:meth:`attach_to_tangle`. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': TransactionTrytes, + Stored trytes. + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#storetransactions + """ + return await core.StoreTransactionsCommand(self.adapter)(trytes=trytes) + + async def were_addresses_spent_from(self, addresses): + # type: (Iterable[Address]) -> dict + """ + Check if a list of addresses was ever spent from, in the current + epoch, or in previous epochs. + + If an address has a pending transaction, it's also considered 'spent'. + + :param Iterable[Address] addresses: + List of addresses to check. + + :return: + ``dict`` with the following structure:: + + { + 'states': List[bool], + States of the specified addresses in the same order as + the values in the addresses parameter. A ``True`` value + means that the address has been spent from. + 'duration': int, + Number of milliseconds it took to complete the request. + } + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference#wereaddressesspentfrom + """ + return await core.WereAddressesSpentFromCommand(self.adapter)( + addresses=addresses, + ) + +class AsyncIota(AsyncStrictIota): + """ + Implements the async core API, plus additional async wrapper methods for + common operations. + + :param AdapterSpec adapter: + URI string or BaseAdapter instance. + + :param Optional[Seed] seed: + Seed used to generate new addresses. + If not provided, a random one will be generated. + + .. note:: + This value is never transferred to the node/network. + + :param Optional[bool] devnet: + Whether to use devnet settings for this instance. + On the devnet, minimum weight magnitude is decreased, on mainnet + it is 14 by default. + + For more info on the Mainnet and the Devnet, visit + `the official docs site`. + + :param Optional[bool] local_pow: + Whether to perform proof-of-work locally by redirecting all calls + to :py:meth:`attach_to_tangle` to + `ccurl pow interface `_. + + See :ref:`Optional Local Pow` for more info and + :ref:`find out` how to use it. + + References: + + - https://docs.iota.org/docs/node-software/0.1/iri/references/api-reference + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md + """ + + def __init__(self, adapter, seed=None, devnet=False, local_pow=False): + # type: (AdapterSpec, Optional[TrytesCompatible], bool, bool) -> None + """ + :param seed: + Seed used to generate new addresses. + If not provided, a random one will be generated. + + .. note:: + This value is never transferred to the node/network. + """ + super().__init__(adapter, devnet, local_pow) + + self.seed = Seed(seed) if seed else Seed.random() + + async def broadcast_and_store(self, trytes): + # type: (Iterable[TransactionTrytes]) -> dict + """ + Broadcasts and stores a set of transaction trytes. + + :param Iterable[TransactionTrytes] trytes: + Transaction trytes to broadcast and store. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': List[TransactionTrytes], + List of TransactionTrytes that were broadcast. + Same as the input ``trytes``. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore + """ + return await extended.BroadcastAndStoreCommand(self.adapter)( + trytes=trytes, + ) + + async def broadcast_bundle(self, tail_transaction_hash): + # type (TransactionHash) -> dict + """ + Re-broadcasts all transactions in a bundle given the tail transaction hash. + It might be useful when transactions did not properly propagate, + particularly in the case of large bundles. + + :param TransactionHash tail_transaction_hash: + Tail transaction hash of the bundle. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': List[TransactionTrytes], + List of TransactionTrytes that were broadcast. + } + + References: + + - https://github.com/iotaledger/iota.js/blob/next/api_reference.md#module_core.broadcastBundle + """ + + return await extended.BroadcastBundleCommand(self.adapter)( + tail_hash=tail_transaction_hash, + ) + + async def find_transaction_objects( + self, + bundles=None, # type: Optional[Iterable[BundleHash]] + addresses=None, # type: Optional[Iterable[Address]] + tags=None, # type: Optional[Iterable[Tag]] + approvees=None, # type: Optional[Iterable[TransactionHash]] + ): + # type: (...) -> dict + """ + A more extensive version of :py:meth:`find_transactions` that + returns transaction objects instead of hashes. + + Effectively, this is :py:meth:`find_transactions` + + :py:meth:`get_trytes` + converting the trytes into + transaction objects. + + It accepts the same parameters as :py:meth:`find_transactions`. + + Find the transactions which match the specified input. + All input values are lists, for which a list of return values + (transaction hashes), in the same order, is returned for all + individual elements. Using multiple of these input fields returns the + intersection of the values. + + :param Optional[Iterable[BundleHash]] bundles: + List of bundle IDs. + + :param Optional[Iterable[Address]] addresses: + List of addresses. + + :param Optional[Iterable[Tag]] tags: + List of tags. + + :param Optional[Iterable[TransactionHash]] approvees: + List of approvee transaction IDs. + + :return: + ``dict`` with the following structure:: + + { + 'transactions': List[Transaction], + List of Transaction objects that match the input. + } + + """ + return await extended.FindTransactionObjectsCommand(self.adapter)( + bundles=bundles, + addresses=addresses, + tags=tags, + approvees=approvees, + ) + + async def get_account_data(self, start=0, stop=None, inclusion_states=False, security_level=None): + # type: (int, Optional[int], bool, Optional[int]) -> dict + """ + More comprehensive version of :py:meth:`get_transfers` that + returns addresses and account balance in addition to bundles. + + This function is useful in getting all the relevant information + of your account. + + :param int start: + Starting key index. + + :param Optional[int] stop: + Stop before this index. + + Note that this parameter behaves like the ``stop`` attribute + in a :py:class:`slice` object; the stop index is *not* + included in the result. + + If ``None`` (default), then this method will check every + address until it finds one that is unused. + + .. note:: + An unused address is an address that **has not been spent from** + and **has no transactions** referencing it on the Tangle. + + A snapshot removes transactions from the Tangle. As a + consequence, after a snapshot, it may happen that this API does + not return the correct account data with ``stop`` being ``None``. + + As a workaround, you can save your used addresses and their + ``key_index`` attribute in a local database. Use the + ``start`` and ``stop`` parameters to tell the API from where to + start checking and where to stop. + + :param bool inclusion_states: + Whether to also fetch the inclusion states of the transfers. + + This requires an additional API call to the node, so it is + disabled by default. + + :param Optional[int] security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + + :return: + ``dict`` with the following structure:: + + { + 'addresses': List[Address], + List of generated addresses. + + Note that this list may include unused + addresses. + + 'balance': int, + Total account balance. Might be 0. + + 'bundles': List[Bundle], + List of bundles with transactions to/from this + account. + } + + """ + return await extended.GetAccountDataCommand(self.adapter)( + seed=self.seed, + start=start, + stop=stop, + inclusionStates=inclusion_states, + security_level=security_level + ) + + async def get_bundles(self, transactions): + # type: (Iterable[TransactionHash]) -> dict + """ + Returns the bundle(s) associated with the specified transaction + hashes. + + :param Iterable[TransactionHash] transactions: + Transaction hashes. Must be a tail transaction. + + :return: + ``dict`` with the following structure:: + + { + 'bundles': List[Bundle], + List of matching bundles. Note that this value is + always a list, even if only one bundle was found. + } + + :raise :py:class:`iota.adapter.BadApiResponse`: + - if any of the bundles fails validation. + - if any of the bundles is not visible on the Tangle. + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle + """ + return await extended.GetBundlesCommand(self.adapter)( + transactions=transactions, + ) + + async def get_inputs( + self, + start=0, + stop=None, + threshold=None, + security_level=None, + ): + # type: (int, Optional[int], Optional[int], Optional[int]) -> dict + """ + Gets all possible inputs of a seed and returns them, along with + the total balance. + + This is either done deterministically (by generating all + addresses until :py:meth:`find_transactions` returns an empty + result), or by providing a key range to search. + + :param int start: + Starting key index. + Defaults to 0. + + :param Optional[int] stop: + Stop before this index. + + Note that this parameter behaves like the ``stop`` attribute + in a :py:class:`slice` object; the stop index is *not* + included in the result. + + If ``None`` (default), then this method will not stop until + it finds an unused address. + + .. note:: + An unused address is an address that **has not been spent from** + and **has no transactions** referencing it on the Tangle. + + A snapshot removes transactions from the Tangle. As a + consequence, after a snapshot, it may happen that this API does + not return the correct inputs with ``stop`` being ``None``. + + As a workaround, you can save your used addresses and their + ``key_index`` attribute in a local database. Use the + ``start`` and ``stop`` parameters to tell the API from where to + start checking for inputs and where to stop. + + :param Optional[int] threshold: + If set, determines the minimum threshold for a successful + result: + + - As soon as this threshold is reached, iteration will stop. + - If the command runs out of addresses before the threshold + is reached, an exception is raised. + + .. note:: + This method does not attempt to "optimize" the result + (e.g., smallest number of inputs, get as close to + ``threshold`` as possible, etc.); it simply accumulates + inputs in order until the threshold is met. + + If ``threshold`` is 0, the first address in the key range + with a non-zero balance will be returned (if it exists). + + If ``threshold`` is ``None`` (default), this method will + return **all** inputs in the specified key range. + + :param Optional[int] security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + + :return: + ``dict`` with the following structure:: + + { + 'inputs': List[Address], + Addresses with nonzero balances that can be used + as inputs. + + 'totalBalance': int, + Aggregate balance from all matching addresses. + } + + Note that each :py:class:`Address` in the result has its + :py:attr:`Address.balance` attribute set. + + Example: + + .. code-block:: python + + response = iota.get_inputs(...) + + input0 = response['inputs'][0] # type: Address + input0.balance # 42 + + :raise: + - :py:class:`iota.adapter.BadApiResponse` if ``threshold`` + is not met. Not applicable if ``threshold`` is ``None``. + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs + """ + return await extended.GetInputsCommand(self.adapter)( + seed=self.seed, + start=start, + stop=stop, + threshold=threshold, + securityLevel=security_level + ) + + async def get_latest_inclusion(self, hashes): + # type: (Iterable[TransactionHash]) -> Dict[TransactionHash, bool] + """ + Fetches the inclusion state for the specified transaction + hashes, as of the latest milestone that the node has processed. + + Effectively, this is :py:meth:`get_node_info` + + :py:meth:`get_inclusion_states`. + + :param Iterable[TransactionHash] hashes: + List of transaction hashes. + + :return: + ``dict`` with the following structure:: + + { + "states": Dict[TransactionHash, bool] + ``dict`` with one boolean per transaction hash in + ``hashes``. + } + + """ + return await extended.GetLatestInclusionCommand(self.adapter)(hashes=hashes) + + async def get_new_addresses( + self, + index=0, + count=1, + security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL, + checksum=False, + ): + # type: (int, int, int, bool) -> dict + """ + Generates one or more new addresses from the seed. + + :param int index: + The key index of the first new address to generate (must be + >= 0). + + :param int count: + Number of addresses to generate (must be >= 1). + + .. tip:: + This is more efficient than calling :py:meth:`get_new_addresses` + inside a loop. + + If ``None``, this method will progressively generate + addresses and scan the Tangle until it finds one that has no + transactions referencing it and was never spent from. + + .. note:: + A snapshot removes transactions from the Tangle. As a + consequence, after a snapshot, it may happen that when ``count`` + is ``None``, this API call returns a "new" address that used to + have transactions before the snapshot. + As a workaround, you can save your used addresses and their + ``key_index`` attribute in a local database. Use the + ``index`` parameter to tell the API from where to start + generating and checking new addresses. + + :param int security_level: + Number of iterations to use when generating new addresses. + + Larger values take longer, but the resulting signatures are + more secure. + + This value must be between 1 and 3, inclusive. + + :param bool checksum: + Specify whether to return the address with the checksum. + Defaults to ``False``. + + :return: + ``dict`` with the following structure:: + + { + 'addresses': List[Address], + Always a list, even if only one address was + generated. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress + """ + return await extended.GetNewAddressesCommand(self.adapter)( + count=count, + index=index, + securityLevel=security_level, + checksum=checksum, + seed=self.seed, + ) + + async def get_transaction_objects( + self, + hashes, # type: [Iterable[TransactionHash]] + ): + # type: (...) -> dict + """ + Fetches transaction objects from the Tangle given their + transaction IDs (hashes). + + Effectively, this is :py:meth:`get_trytes` + + converting the trytes into transaction objects. + + Similar to :py:meth:`find_transaction_objects`, but accepts + list of transaction hashes as input. + + :param Iterable[TransactionHash] hashes: + List of transaction IDs (transaction hashes). + + :return: + ``dict`` with the following structure:: + + { + 'transactions': List[Transaction], + List of Transaction objects that match the input. + } + """ + return await extended.GetTransactionObjectsCommand(self.adapter)( + hashes=hashes, + ) + + async def get_transfers(self, start=0, stop=None, inclusion_states=False): + # type: (int, Optional[int], bool) -> dict + """ + Returns all transfers associated with the seed. + + :param int start: + Starting key index. + + :param Optional[int] stop: + Stop before this index. + + Note that this parameter behaves like the ``stop`` attribute + in a :py:class:`slice` object; the stop index is *not* + included in the result. + + If ``None`` (default), then this method will check every + address until it finds one that is unused. + + .. note:: + An unused address is an address that **has not been spent from** + and **has no transactions** referencing it on the Tangle. + + A snapshot removes transactions from the Tangle. As a + consequence, after a snapshot, it may happen that this API does + not return the expected transfers with ``stop`` being ``None``. + + As a workaround, you can save your used addresses and their + ``key_index`` attribute in a local database. Use the + ``start`` and ``stop`` parameters to tell the API from where to + start checking for transfers and where to stop. + + :param bool inclusion_states: + Whether to also fetch the inclusion states of the transfers. + + This requires an additional API call to the node, so it is + disabled by default. + + :return: + ``dict`` with the following structure:: + + { + 'bundles': List[Bundle], + Matching bundles, sorted by tail transaction + timestamp. + + This value is always a list, even if only one + bundle was found. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers + """ + return await extended.GetTransfersCommand(self.adapter)( + seed=self.seed, + start=start, + stop=stop, + inclusionStates=inclusion_states, + ) + + async def is_promotable( + self, + tails, # type: Iterable[TransactionHash] + ): + # type: (Iterable(TransactionHash)] -> dict + """ + Checks if tail transaction(s) is promotable by calling + :py:meth:`check_consistency` and verifying that ``attachmentTimestamp`` + is above a lower bound. + Lower bound is calculated based on number of milestones issued + since transaction attachment. + + :param Iterable(TransactionHash) tails: + List of tail transaction hashes. + + :return: + The return type mimics that of :py:meth:`check_consistency`. + ``dict`` with the following structure:: + + { + 'promotable': bool, + If ``True``, all tails are promotable. If ``False``, see + `info` field. + + 'info': Optional(List[Text]) + If `promotable` is ``False``, this contains info about what + went wrong. + Note that when 'promotable' is ``True``, 'info' does not + exist. + + } + + References: + - https://github.com/iotaledger/iota.js/blob/next/api_reference.md#module_core.isPromotable + """ + return await extended.IsPromotableCommand(self.adapter)( + tails=tails, + ) + + async def prepare_transfer( + self, + transfers, # type: Iterable[ProposedTransaction] + inputs=None, # type: Optional[Iterable[Address]] + change_address=None, # type: Optional[Address] + security_level=None, # type: Optional[int] + ): + # type: (...) -> dict + """ + Prepares transactions to be broadcast to the Tangle, by + generating the correct bundle, as well as choosing and signing + the inputs (for value transfers). + + :param Iterable[ProposedTransaction] transfers: + Transaction objects to prepare. + + :param Optional[Iterable[Address]] inputs: + List of addresses used to fund the transfer. + Ignored for zero-value transfers. + + If not provided, addresses will be selected automatically by + scanning the Tangle for unspent inputs. Depending on how + many transfers you've already sent with your seed, this + process could take awhile. + + :param Optional[Address] change_address: + If inputs are provided, any unspent amount will be sent to + this address. + + If not specified, a change address will be generated + automatically. + + :param Optional[int] security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': List[TransactionTrytes], + Raw trytes for the transactions in the bundle, + ready to be provided to :py:meth:`send_trytes`. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers + """ + return await extended.PrepareTransferCommand(self.adapter)( + seed=self.seed, + transfers=transfers, + inputs=inputs, + changeAddress=change_address, + securityLevel=security_level, + ) + + async def promote_transaction( + self, + transaction, + depth=3, + min_weight_magnitude=None, + ): + # type: (TransactionHash, int, Optional[int]) -> dict + """ + Promotes a transaction by adding spam on top of it. + + :param TransactionHash transaction: + Transaction hash. Must be a tail transaction. + + :param int depth: + Depth at which to attach the bundle. + Defaults to 3. + + :param Optional[int] min_weight_magnitude: + Min weight magnitude, used by the node to calibrate Proof of + Work. + + If not provided, a default value will be used. + + :return: + ``dict`` with the following structure:: + + { + 'bundle': Bundle, + The newly-published bundle. + } + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return await extended.PromoteTransactionCommand(self.adapter)( + transaction=transaction, + depth=depth, + minWeightMagnitude=min_weight_magnitude, + ) + + async def replay_bundle( + self, + transaction, + depth=3, + min_weight_magnitude=None, + ): + # type: (TransactionHash, int, Optional[int]) -> dict + """ + Takes a tail transaction hash as input, gets the bundle + associated with the transaction and then replays the bundle by + attaching it to the Tangle. + + :param TransactionHash transaction: + Transaction hash. Must be a tail. + + :param int depth: + Depth at which to attach the bundle. + Defaults to 3. + + :param Optional[int] min_weight_magnitude: + Min weight magnitude, used by the node to calibrate Proof of + Work. + + If not provided, a default value will be used. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': List[TransactionTrytes], + Raw trytes that were published to the Tangle. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#replaytransfer + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return await extended.ReplayBundleCommand(self.adapter)( + transaction=transaction, + depth=depth, + minWeightMagnitude=min_weight_magnitude, + ) + + async def send_transfer( + self, + transfers, # type: Iterable[ProposedTransaction] + depth=3, # type: int + inputs=None, # type: Optional[Iterable[Address]] + change_address=None, # type: Optional[Address] + min_weight_magnitude=None, # type: Optional[int] + security_level=None, # type: Optional[int] + ): + # type: (...) -> dict + """ + Prepares a set of transfers and creates the bundle, then + attaches the bundle to the Tangle, and broadcasts and stores the + transactions. + + :param Iterable[ProposedTransaction] transfers: + Transfers to include in the bundle. + + :param int depth: + Depth at which to attach the bundle. + Defaults to 3. + + :param Optional[Iterable[Address]] inputs: + List of inputs used to fund the transfer. + Not needed for zero-value transfers. + + :param Optional[Address] change_address: + If inputs are provided, any unspent amount will be sent to + this address. + + If not specified, a change address will be generated + automatically. + + :param Optional[int] min_weight_magnitude: + Min weight magnitude, used by the node to calibrate Proof of + Work. + + If not provided, a default value will be used. + + :param Optional[int] security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + + :return: + ``dict`` with the following structure:: + + { + 'bundle': Bundle, + The newly-published bundle. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtransfer + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return await extended.SendTransferCommand(self.adapter)( + seed=self.seed, + depth=depth, + transfers=transfers, + inputs=inputs, + changeAddress=change_address, + minWeightMagnitude=min_weight_magnitude, + securityLevel=security_level, + ) + + async def send_trytes(self, trytes, depth=3, min_weight_magnitude=None): + # type: (Iterable[TransactionTrytes], int, Optional[int]) -> dict + """ + Attaches transaction trytes to the Tangle, then broadcasts and + stores them. + + :param Iterable[TransactionTrytes] trytes: + Transaction encoded as a tryte sequence. + + :param int depth: + Depth at which to attach the bundle. + Defaults to 3. + + :param Optional[int] min_weight_magnitude: + Min weight magnitude, used by the node to calibrate Proof of + Work. + + If not provided, a default value will be used. + + :return: + ``dict`` with the following structure:: + + { + 'trytes': List[TransactionTrytes], + Raw trytes that were published to the Tangle. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return await extended.SendTrytesCommand(self.adapter)( + trytes=trytes, + depth=depth, + minWeightMagnitude=min_weight_magnitude, + ) + + async def is_reattachable(self, addresses): + # type: (Iterable[Address]) -> dict + """ + This API function helps you to determine whether you should + replay a transaction or make a new one (either with the same + input, or a different one). + + This method takes one or more input addresses (i.e. from spent + transactions) as input and then checks whether any transactions + with a value transferred are confirmed. + + If yes, it means that this input address has already been + successfully used in a different transaction, and as such you + should no longer replay the transaction. + + :param Iterable[Address] addresses: + List of addresses. + + :return: + ``dict`` with the following structure:: + + { + 'reattachable': List[bool], + Always a list, even if only one address was queried. + } + + """ + return await extended.IsReattachableCommand(self.adapter)( + addresses=addresses + ) + + async def traverse_bundle(self, tail_hash): + # type: (TransactionHash) -> dict + """ + Fetches and traverses a bundle from the Tangle given a tail transaction + hash. + Recursively traverse the Tangle, collecting transactions until + we hit a new bundle. + + This method is (usually) faster than :py:meth:`find_transactions`, and + it ensures we don't collect transactions from replayed bundles. + + :param TransactionHash tail_hash: + Tail transaction hash of the bundle. + + :return: + ``dict`` with the following structure:: + + { + 'bundle': List[Bundle], + List of matching bundles. Note that this value is + always a list, even if only one bundle was found. + } + + """ + return await extended.TraverseBundleCommand(self.adapter)( + transaction=tail_hash + ) \ No newline at end of file diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 39695bc8..d78eb1d9 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -43,7 +43,7 @@ def __init__(self, adapter): self.request = None # type: dict self.response = None # type: dict - def __call__(self, **kwargs): + async def __call__(self, **kwargs): # type: (**Any) -> dict """ Sends the command to the node. @@ -64,7 +64,7 @@ def __call__(self, **kwargs): if replacement is not None: self.request = replacement - self.response = self._execute(self.request) + self.response = await self._execute(self.request) replacement = self._prepare_response(self.response) if replacement is not None: @@ -83,7 +83,7 @@ def reset(self): self.request = None # type: dict self.response = None # type: dict - def _execute(self, request): + async def _execute(self, request): # type: (dict) -> dict """ Sends the request object to the adapter and returns the response. @@ -92,7 +92,7 @@ def _execute(self, request): before it is sent (note: this will modify the request object). """ request['command'] = self.command - return self.adapter.send_request(request) + return await self.adapter.send_request(request) @abstract_method def _prepare_request(self, request): diff --git a/iota/commands/core/attach_to_tangle.py b/iota/commands/core/attach_to_tangle.py index 33250bc7..7fed0612 100644 --- a/iota/commands/core/attach_to_tangle.py +++ b/iota/commands/core/attach_to_tangle.py @@ -7,6 +7,7 @@ from iota import TransactionHash, TransactionTrytes from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes +from iota.adapter import async_return __all__ = [ 'AttachToTangleCommand', @@ -27,7 +28,7 @@ def get_request_filter(self): def get_response_filter(self): return AttachToTangleResponseFilter() - def _execute(self, request): + async def _execute(self, request): if self.adapter.local_pow is True: from pow import ccurl_interface powed_trytes = ccurl_interface.attach_to_tangle( @@ -36,9 +37,9 @@ def _execute(self, request): request['trunkTransaction'], request['minWeightMagnitude'] ) - return {'trytes': powed_trytes} + return await async_return({'trytes': powed_trytes}) else: - return super(FilterCommand, self)._execute(request) + return await super(FilterCommand, self)._execute(request) class AttachToTangleRequestFilter(RequestFilter): def __init__(self): diff --git a/iota/commands/extended/broadcast_and_store.py b/iota/commands/extended/broadcast_and_store.py index c28526bd..50840b59 100644 --- a/iota/commands/extended/broadcast_and_store.py +++ b/iota/commands/extended/broadcast_and_store.py @@ -6,6 +6,7 @@ from iota.commands.core.broadcast_transactions import \ BroadcastTransactionsCommand from iota.commands.core.store_transactions import StoreTransactionsCommand +import asyncio __all__ = [ 'BroadcastAndStoreCommand', @@ -26,9 +27,13 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): - BroadcastTransactionsCommand(self.adapter)(**request) - StoreTransactionsCommand(self.adapter)(**request) + async def _execute(self, request): + # Submit the two coroutines to the already running event loop + await asyncio.gather( + BroadcastTransactionsCommand(self.adapter)(**request), + StoreTransactionsCommand(self.adapter)(**request), + ) + return { 'trytes': request['trytes'], } diff --git a/iota/commands/extended/broadcast_bundle.py b/iota/commands/extended/broadcast_bundle.py index b7d84b34..76deceb3 100644 --- a/iota/commands/extended/broadcast_bundle.py +++ b/iota/commands/extended/broadcast_bundle.py @@ -31,13 +31,13 @@ def get_response_filter(self): # Return value is filtered before hitting us. pass - def _execute(self, request): + async def _execute(self, request): # Given tail hash, fetches the bundle from the tangle # and validates it. # Returns List[List[TransactionTrytes]] # (outer list has one item in current implementation) - bundle = GetBundlesCommand(self.adapter)(transactions=[request['tail_hash']]) - BroadcastTransactionsCommand(self.adapter)(trytes=bundle[0]) + bundle = await GetBundlesCommand(self.adapter)(transactions=[request['tail_hash']]) + await BroadcastTransactionsCommand(self.adapter)(trytes=bundle[0]) return { 'trytes': bundle[0], } diff --git a/iota/commands/extended/find_transaction_objects.py b/iota/commands/extended/find_transaction_objects.py index e0dd0960..5bd4d442 100644 --- a/iota/commands/extended/find_transaction_objects.py +++ b/iota/commands/extended/find_transaction_objects.py @@ -23,7 +23,7 @@ class FindTransactionObjectsCommand(FindTransactionsCommand): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): bundles = request\ .get('bundles') # type: Optional[Iterable[BundleHash]] addresses = request\ @@ -33,7 +33,7 @@ def _execute(self, request): approvees = request\ .get('approvees') # type: Optional[Iterable[TransactionHash]] - ft_response = FindTransactionsCommand(adapter=self.adapter)( + ft_response = await FindTransactionsCommand(adapter=self.adapter)( bundles=bundles, addresses=addresses, tags=tags, @@ -43,7 +43,7 @@ def _execute(self, request): hashes = ft_response['hashes'] transactions = [] if hashes: - gt_response = GetTrytesCommand(adapter=self.adapter)(hashes=hashes) + gt_response = await GetTrytesCommand(adapter=self.adapter)(hashes=hashes) transactions = list(map( Transaction.from_tryte_string, diff --git a/iota/commands/extended/get_account_data.py b/iota/commands/extended/get_account_data.py index 46dd8d1d..4e1fcd2e 100644 --- a/iota/commands/extended/get_account_data.py +++ b/iota/commands/extended/get_account_data.py @@ -36,7 +36,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): inclusion_states = request['inclusionStates'] # type: bool seed = request['seed'] # type: Seed start = request['start'] # type: int @@ -47,7 +47,7 @@ def _execute(self, request): my_addresses = [] # type: List[Address] my_hashes = [] # type: List[TransactionHash] - for addy, hashes in iter_used_addresses(self.adapter, seed, start, security_level): + async for addy, hashes in iter_used_addresses(self.adapter, seed, start, security_level): my_addresses.append(addy) my_hashes.extend(hashes) else: @@ -56,13 +56,13 @@ def _execute(self, request): my_addresses = ( AddressGenerator(seed, security_level).get_addresses(start, stop - start) ) - my_hashes = ft_command(addresses=my_addresses).get('hashes') or [] + my_hashes = (await ft_command(addresses=my_addresses)).get('hashes') or [] account_balance = 0 if my_addresses: # Load balances for the addresses that we generated. gb_response = ( - GetBalancesCommand(self.adapter)(addresses=my_addresses) + await GetBalancesCommand(self.adapter)(addresses=my_addresses) ) for i, balance in enumerate(gb_response['balances']): @@ -76,7 +76,7 @@ def _execute(self, request): 'balance': account_balance, 'bundles': - get_bundles_from_transaction_hashes( + await get_bundles_from_transaction_hashes( adapter=self.adapter, transaction_hashes=my_hashes, inclusion_states=inclusion_states, diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index cb1acb3b..56970143 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -10,6 +10,7 @@ from iota.exceptions import with_context from iota.transaction.validator import BundleValidator from iota.filters import Trytes +import asyncio __all__ = [ 'GetBundlesCommand', @@ -30,16 +31,13 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): transaction_hashes = request['transactions'] # type: Iterable[TransactionHash] - bundles = [] - - # Fetch bundles one-by-one - for tx_hash in transaction_hashes: - bundle = TraverseBundleCommand(self.adapter)( + async def fetch_and_validate(tx_hash): + bundle = (await TraverseBundleCommand(self.adapter)( transaction=tx_hash - )['bundles'][0] # Currently 1 bundle only + ))['bundles'][0] # Currently 1 bundle only validator = BundleValidator(bundle) @@ -55,7 +53,12 @@ def _execute(self, request): }, ) - bundles.append(bundle) + return bundle + + # Fetch bundles asynchronously + bundles = await asyncio.gather( + *[fetch_and_validate(tx_hash) for tx_hash in transaction_hashes] + ) return { 'bundles': bundles, diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index 0349c0c1..f21a46fb 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -34,7 +34,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): stop = request['stop'] # type: Optional[int] seed = request['seed'] # type: Seed start = request['start'] # type: int @@ -43,7 +43,7 @@ def _execute(self, request): # Determine the addresses we will be scanning. if stop is None: - addresses = [addy for addy, _ in iter_used_addresses( + addresses = [addy async for addy, _ in iter_used_addresses( adapter=self.adapter, seed=seed, start=start, @@ -59,7 +59,7 @@ def _execute(self, request): if addresses: # Load balances for the addresses that we generated. - gb_response = GetBalancesCommand(self.adapter)(addresses=addresses) + gb_response = await GetBalancesCommand(self.adapter)(addresses=addresses) else: gb_response = {'balances': []} diff --git a/iota/commands/extended/get_latest_inclusion.py b/iota/commands/extended/get_latest_inclusion.py index cf96bbf6..eaf7c17a 100644 --- a/iota/commands/extended/get_latest_inclusion.py +++ b/iota/commands/extended/get_latest_inclusion.py @@ -31,12 +31,12 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): hashes = request['hashes'] # type: List[TransactionHash] - gni_response = GetNodeInfoCommand(self.adapter)() + gni_response = await GetNodeInfoCommand(self.adapter)() - gis_response = GetInclusionStatesCommand(self.adapter)( + gis_response = await GetInclusionStatesCommand(self.adapter)( transactions=hashes, tips=[gni_response['latestSolidSubtangleMilestone']], ) diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index 65f3c577..49141b29 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -14,6 +14,7 @@ from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import SecurityLevel, Trytes +import asyncio __all__ = [ 'GetNewAddressesCommand', @@ -34,7 +35,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): checksum = request['checksum'] # type: bool count = request['count'] # type: Optional[int] index = request['index'] # type: int @@ -43,7 +44,7 @@ def _execute(self, request): return { 'addresses': - self._find_addresses( + await self._find_addresses( seed, index, count, @@ -52,7 +53,7 @@ def _execute(self, request): ), } - def _find_addresses(self, seed, index, count, security_level, checksum): + async def _find_addresses(self, seed, index, count, security_level, checksum): # type: (Seed, int, Optional[int], int, bool) -> List[Address] """ Find addresses matching the command parameters. @@ -64,16 +65,18 @@ def _find_addresses(self, seed, index, count, security_level, checksum): for addy in generator.create_iterator(start=index): # We use addy.address here because the commands do # not work on an address with a checksum - response = WereAddressesSpentFromCommand(self.adapter)( - addresses=[addy.address], + # Execute two checks concurrently + responses = await asyncio.gather( + WereAddressesSpentFromCommand(self.adapter)( + addresses=[addy.address], + ), + FindTransactionsCommand(self.adapter)( + addresses=[addy.address], + ), ) - if response['states'][0]: - continue - - response = FindTransactionsCommand(self.adapter)( - addresses=[addy.address], - ) - if response.get('hashes'): + # responses[0] -> was it spent from? + # responses[1] -> any transaction found? + if responses[0]['states'][0] or responses[1].get('hashes'): continue return [addy] diff --git a/iota/commands/extended/get_transaction_objects.py b/iota/commands/extended/get_transaction_objects.py index 4f475647..cc4070c7 100644 --- a/iota/commands/extended/get_transaction_objects.py +++ b/iota/commands/extended/get_transaction_objects.py @@ -30,13 +30,13 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): hashes = request\ .get('hashes') # type: Iterable[TransactionHash] transactions = [] if hashes: - gt_response = GetTrytesCommand(adapter=self.adapter)(hashes=hashes) + gt_response = await GetTrytesCommand(adapter=self.adapter)(hashes=hashes) transactions = list(map( Transaction.from_tryte_string, diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index a8e5e9a7..d7b1d701 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -34,7 +34,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): inclusion_states = request['inclusionStates'] # type: bool seed = request['seed'] # type: Seed start = request['start'] # type: int @@ -44,12 +44,14 @@ def _execute(self, request): # transaction hashes. if stop is None: my_hashes = list(chain(*( - hashes - for _, hashes in iter_used_addresses(self.adapter, seed, start) + [ + hashes async for _, hashes in + iter_used_addresses(self.adapter, seed, start) + ] ))) else: ft_response = \ - FindTransactionsCommand(self.adapter)( + await FindTransactionsCommand(self.adapter)( addresses= AddressGenerator(seed).get_addresses(start, stop - start), ) @@ -58,7 +60,7 @@ def _execute(self, request): return { 'bundles': - get_bundles_from_transaction_hashes( + await get_bundles_from_transaction_hashes( adapter=self.adapter, transaction_hashes=my_hashes, inclusion_states=inclusion_states, diff --git a/iota/commands/extended/helpers.py b/iota/commands/extended/helpers.py deleted file mode 100644 index 6f9a5a73..00000000 --- a/iota/commands/extended/helpers.py +++ /dev/null @@ -1,28 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, \ - unicode_literals - -from iota.transaction.types import TransactionHash - - -class Helpers(object): - """ - Adds additional helper functions that aren't part of the core or - extended API. - - See https://github.com/iotaledger/iota.py/pull/124 for more - context. - """ - - def __init__(self, api): - self.api = api - - def is_promotable(self, tail): - # type: (TransactionHash) -> bool - """ - Determines if a tail transaction is promotable. - - :param tail: - Transaction hash. Must be a tail transaction. - """ - return self.api.check_consistency(tails=[tail])['state'] diff --git a/iota/commands/extended/is_promotable.py b/iota/commands/extended/is_promotable.py index cda97691..4a623d29 100644 --- a/iota/commands/extended/is_promotable.py +++ b/iota/commands/extended/is_promotable.py @@ -51,7 +51,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): tails = request['tails'] # First, check consistency @@ -59,7 +59,7 @@ def _execute(self, request): # - The node isn't missing the transaction's branch or trunk transactions # - The transaction's bundle is valid # - The transaction's branch and trunk transactions are valid - cc_response = CheckConsistencyCommand(self.adapter)( + cc_response = await CheckConsistencyCommand(self.adapter)( tails=tails, ) @@ -72,7 +72,7 @@ def _execute(self, request): transactions = [ Transaction.from_tryte_string(x) for x in - GetTrytesCommand(self.adapter)(hashes=tails)['trytes'] + (await GetTrytesCommand(self.adapter)(hashes=tails))['trytes'] ] response = { diff --git a/iota/commands/extended/is_reattachable.py b/iota/commands/extended/is_reattachable.py index 3b5e83a6..13aeeb96 100644 --- a/iota/commands/extended/is_reattachable.py +++ b/iota/commands/extended/is_reattachable.py @@ -29,13 +29,13 @@ def get_request_filter(self): def get_response_filter(self): return IsReattachableResponseFilter() - def _execute(self, request): + async def _execute(self, request): addresses = request['addresses'] # type: List[Address] # fetch full transaction objects - transactions = FindTransactionObjectsCommand(adapter=self.adapter)( + transactions = (await FindTransactionObjectsCommand(adapter=self.adapter)( addresses=addresses, - )['transactions'] + ))['transactions'] # Map and filter transactions which have zero value. # If multiple transactions for the same address are returned, @@ -52,7 +52,7 @@ def _execute(self, request): } # Fetch inclusion states. - inclusion_states = GetLatestInclusionCommand(adapter=self.adapter)( + inclusion_states = await GetLatestInclusionCommand(adapter=self.adapter)( hashes=list(transaction_map.values()), ) inclusion_states = inclusion_states['states'] diff --git a/iota/commands/extended/prepare_transfer.py b/iota/commands/extended/prepare_transfer.py index dd725b83..ed77525d 100644 --- a/iota/commands/extended/prepare_transfer.py +++ b/iota/commands/extended/prepare_transfer.py @@ -36,7 +36,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): # Required parameters. seed = request['seed'] # type: Seed bundle = ProposedBundle(request['transfers']) @@ -53,7 +53,7 @@ def _execute(self, request): if proposed_inputs is None: # No inputs provided. Scan addresses for unspent # inputs. - gi_response = GetInputsCommand(self.adapter)( + gi_response = await GetInputsCommand(self.adapter)( seed=seed, threshold=want_to_spend, securityLevel=security_level, @@ -66,7 +66,7 @@ def _execute(self, request): available_to_spend = 0 confirmed_inputs = [] # type: List[Address] - gb_response = GetBalancesCommand(self.adapter)( + gb_response = await GetBalancesCommand(self.adapter)( addresses=[i.address for i in proposed_inputs], ) @@ -105,10 +105,10 @@ def _execute(self, request): if bundle.balance < 0: if not change_address: change_address = \ - GetNewAddressesCommand(self.adapter)( + (await GetNewAddressesCommand(self.adapter)( seed=seed, securityLevel=security_level, - )['addresses'][0] + ))['addresses'][0] bundle.send_unspent_inputs_to(change_address) diff --git a/iota/commands/extended/promote_transaction.py b/iota/commands/extended/promote_transaction.py index b7c03d25..564d1472 100644 --- a/iota/commands/extended/promote_transaction.py +++ b/iota/commands/extended/promote_transaction.py @@ -30,12 +30,12 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): depth = request['depth'] # type: int min_weight_magnitude = request['minWeightMagnitude'] # type: int transaction = request['transaction'] # type: TransactionHash - cc_response = CheckConsistencyCommand(self.adapter)(tails=[transaction]) + cc_response = await CheckConsistencyCommand(self.adapter)(tails=[transaction]) if cc_response['state'] is False: raise BadApiResponse( 'Transaction {transaction} is not promotable. ' @@ -47,7 +47,7 @@ def _execute(self, request): value=0, ) - return SendTransferCommand(self.adapter)( + return await SendTransferCommand(self.adapter)( seed=spam_transfer.address, depth=depth, transfers=[spam_transfer], diff --git a/iota/commands/extended/replay_bundle.py b/iota/commands/extended/replay_bundle.py index 3d5e3139..c0eb3e61 100644 --- a/iota/commands/extended/replay_bundle.py +++ b/iota/commands/extended/replay_bundle.py @@ -29,18 +29,18 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): depth = request['depth'] # type: int min_weight_magnitude = request['minWeightMagnitude'] # type: int transaction = request['transaction'] # type: TransactionHash - gb_response = GetBundlesCommand(self.adapter)(transactions=[transaction]) + gb_response = await GetBundlesCommand(self.adapter)(transactions=[transaction]) # Note that we only replay the first bundle returned by # ``getBundles``. bundle = gb_response['bundles'][0] # type: Bundle - return SendTrytesCommand(self.adapter)( + return await SendTrytesCommand(self.adapter)( depth=depth, minWeightMagnitude=min_weight_magnitude, trytes=bundle.as_tryte_strings(), diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index ad3def01..8efa4825 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -32,7 +32,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): change_address = request['changeAddress'] # type: Optional[Address] depth = request['depth'] # type: int inputs = request['inputs'] # type: Optional[List[Address]] @@ -42,7 +42,7 @@ def _execute(self, request): reference = request['reference'] # type: Optional[TransactionHash] security_level = request['securityLevel'] # int - pt_response = PrepareTransferCommand(self.adapter)( + pt_response = await PrepareTransferCommand(self.adapter)( changeAddress=change_address, inputs=inputs, seed=seed, @@ -50,7 +50,7 @@ def _execute(self, request): securityLevel=security_level, ) - st_response = SendTrytesCommand(self.adapter)( + st_response = await SendTrytesCommand(self.adapter)( depth=depth, minWeightMagnitude=min_weight_magnitude, trytes=pt_response['trytes'], diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index 16dac7c0..91269325 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -33,7 +33,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): depth = request['depth'] # type: int min_weight_magnitude = request['minWeightMagnitude'] # type: int trytes = request['trytes'] # type: List[TryteString] @@ -41,12 +41,12 @@ def _execute(self, request): # Call ``getTransactionsToApprove`` to locate trunk and branch # transactions so that we can attach the bundle to the Tangle. - gta_response = GetTransactionsToApproveCommand(self.adapter)( + gta_response = await GetTransactionsToApproveCommand(self.adapter)( depth=depth, reference=reference, ) - att_response = AttachToTangleCommand(self.adapter)( + att_response = await AttachToTangleCommand(self.adapter)( branchTransaction=gta_response.get('branchTransaction'), trunkTransaction=gta_response.get('trunkTransaction'), @@ -57,7 +57,7 @@ def _execute(self, request): # ``trytes`` now have POW! trytes = att_response['trytes'] - BroadcastAndStoreCommand(self.adapter)(trytes=trytes) + await BroadcastAndStoreCommand(self.adapter)(trytes=trytes) return { 'trytes': trytes, diff --git a/iota/commands/extended/traverse_bundle.py b/iota/commands/extended/traverse_bundle.py index d1176dbf..a4ddc947 100644 --- a/iota/commands/extended/traverse_bundle.py +++ b/iota/commands/extended/traverse_bundle.py @@ -32,10 +32,10 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): txn_hash = request['transaction'] # type: TransactionHash - bundle = Bundle(self._traverse_bundle(txn_hash, None)) + bundle = Bundle(await self._traverse_bundle(txn_hash, None)) # No bundle validation @@ -43,7 +43,7 @@ def _execute(self, request): 'bundles' : [bundle] } - def _traverse_bundle(self, txn_hash, target_bundle_hash): + async def _traverse_bundle(self, txn_hash, target_bundle_hash): """ Recursively traverse the Tangle, collecting transactions until we hit a new bundle. @@ -51,9 +51,9 @@ def _traverse_bundle(self, txn_hash, target_bundle_hash): This method is (usually) faster than ``findTransactions``, and it ensures we don't collect transactions from replayed bundles. """ - trytes = ( - GetTrytesCommand(self.adapter)(hashes=[txn_hash])['trytes'] - ) # type: List[TryteString] + trytes =(await GetTrytesCommand(self.adapter)( + hashes=[txn_hash]) + )['trytes'] # type: List[TryteString] # If no tx was found by the node for txn_hash, it returns 9s, # so we check here if it returned all 9s trytes. @@ -99,7 +99,7 @@ def _traverse_bundle(self, txn_hash, target_bundle_hash): # Recursively follow the trunk transaction, to fetch the next # transaction in the bundle. - return [transaction] + self._traverse_bundle( + return [transaction] + await self._traverse_bundle( transaction.trunk_transaction_hash, target_bundle_hash ) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index 3292a164..09898508 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -19,7 +19,7 @@ from iota.crypto.types import Seed -def iter_used_addresses( +async def iter_used_addresses( adapter, # type: BaseAdapter seed, # type: Seed start, # type: int @@ -32,6 +32,10 @@ def iter_used_addresses( This is basically the opposite of invoking ``getNewAddresses`` with ``count=None``. + + .. important:: + This is an async generator! + """ if security_level is None: security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL @@ -40,12 +44,12 @@ def iter_used_addresses( wasf_command = WereAddressesSpentFromCommand(adapter) for addy in AddressGenerator(seed, security_level).create_iterator(start): - ft_response = ft_command(addresses=[addy]) + ft_response = await ft_command(addresses=[addy]) if ft_response['hashes']: yield addy, ft_response['hashes'] else: - wasf_response = wasf_command(addresses=[addy]) + wasf_response = await wasf_command(addresses=[addy]) if wasf_response['states'][0]: yield addy, [] else: @@ -56,7 +60,7 @@ def iter_used_addresses( wasf_command.reset() -def get_bundles_from_transaction_hashes( +async def get_bundles_from_transaction_hashes( adapter, transaction_hashes, inclusion_states, @@ -70,13 +74,11 @@ def get_bundles_from_transaction_hashes( if not transaction_hashes: return [] - my_bundles = [] # type: List[Bundle] - # Sort transactions into tail and non-tail. tail_transaction_hashes = set() non_tail_bundle_hashes = set() - gt_response = GetTrytesCommand(adapter)(hashes=transaction_hashes) + gt_response = await GetTrytesCommand(adapter)(hashes=transaction_hashes) all_transactions = list(map( Transaction.from_tryte_string, gt_response['trytes'], @@ -92,9 +94,9 @@ def get_bundles_from_transaction_hashes( non_tail_bundle_hashes.add(txn.bundle_hash) if non_tail_bundle_hashes: - for txn in FindTransactionObjectsCommand(adapter=adapter)( + for txn in (await FindTransactionObjectsCommand(adapter=adapter)( bundles=list(non_tail_bundle_hashes), - )['transactions']: + ))['transactions']: if txn.is_tail: if txn.hash not in tail_transaction_hashes: all_transactions.append(txn) @@ -109,7 +111,7 @@ def get_bundles_from_transaction_hashes( # Attach inclusion states, if requested. if inclusion_states: - gli_response = GetLatestInclusionCommand(adapter)( + gli_response = await GetLatestInclusionCommand(adapter)( hashes=list(tail_transaction_hashes), ) @@ -117,17 +119,15 @@ def get_bundles_from_transaction_hashes( txn.is_confirmed = gli_response['states'].get(txn.hash) # Find the bundles for each transaction. - for txn in tail_transactions: - gb_response = GetBundlesCommand(adapter)(transactions=[txn.hash]) - txn_bundles = gb_response['bundles'] # type: List[Bundle] + txn_bundles = (await GetBundlesCommand(adapter)( + transactions=[txn.hash for txn in tail_transactions] + ))['bundles'] # type: List[Bundle] - if inclusion_states: - for bundle in txn_bundles: - bundle.is_confirmed = txn.is_confirmed - - my_bundles.extend(txn_bundles) + if inclusion_states: + for bundle, txn in zip(txn_bundles, tail_transactions): + bundle.is_confirmed = txn.is_confirmed return list(sorted( - my_bundles, + txn_bundles, key=lambda bundle_: bundle_.tail_transaction.timestamp, )) diff --git a/iota/multisig/api.py b/iota/multisig/api.py index 0ad14b8d..d345138d 100644 --- a/iota/multisig/api.py +++ b/iota/multisig/api.py @@ -4,21 +4,23 @@ from typing import Iterable, Optional -from iota import Address, Iota, ProposedTransaction +from iota import Address, Iota, AsyncIota, ProposedTransaction from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Digest from iota.multisig import commands from iota.multisig.types import MultisigAddress +import asyncio __all__ = [ 'MultisigIota', + 'AsyncMultisigIota', ] -class MultisigIota(Iota): +class AsyncMultisigIota(AsyncIota): """ Extends the IOTA API so that it can send multi-signature - transactions. + transactions. Asynchronous API. .. caution:: Make sure you understand how multisig works before attempting to @@ -31,7 +33,7 @@ class MultisigIota(Iota): - https://github.com/iotaledger/wiki/blob/master/multisigs.md """ - def create_multisig_address(self, digests): + async def create_multisig_address(self, digests): # type: (Iterable[Digest]) -> dict """ Generates a multisig address from a collection of digests. @@ -52,11 +54,11 @@ def create_multisig_address(self, digests): The generated multisig address. } """ - return commands.CreateMultisigAddressCommand(self.adapter)( + return await commands.CreateMultisigAddressCommand(self.adapter)( digests=digests, ) - def get_digests( + async def get_digests( self, index=0, count=1, @@ -92,14 +94,14 @@ def get_digests( was generated. } """ - return commands.GetDigestsCommand(self.adapter)( + return await commands.GetDigestsCommand(self.adapter)( seed=self.seed, index=index, count=count, securityLevel=security_level, ) - def get_private_keys( + async def get_private_keys( self, index=0, count=1, @@ -141,14 +143,14 @@ def get_private_keys( - :py:class:`iota.crypto.signing.KeyGenerator` - https://github.com/iotaledger/wiki/blob/master/multisigs.md#how-m-of-n-works """ - return commands.GetPrivateKeysCommand(self.adapter)( + return await commands.GetPrivateKeysCommand(self.adapter)( seed=self.seed, index=index, count=count, securityLevel=security_level, ) - def prepare_multisig_transfer( + async def prepare_multisig_transfer( self, transfers, # type: Iterable[ProposedTransaction] multisig_input, # type: MultisigAddress @@ -234,8 +236,237 @@ def prepare_multisig_transfer( proof of work (``attachToTangle``) and broadcast the bundle using :py:meth:`iota.api.Iota.send_trytes`. """ - return commands.PrepareMultisigTransferCommand(self.adapter)( + return await commands.PrepareMultisigTransferCommand(self.adapter)( changeAddress=change_address, multisigInput=multisig_input, transfers=transfers, ) + +class MultisigIota(Iota, AsyncMultisigIota): + """ + Extends the IOTA API so that it can send multi-signature + transactions. Synchronous API. + + .. caution:: + Make sure you understand how multisig works before attempting to + use it. If you are not careful, you could easily compromise the + security of your private keys, send IOTAs to unspendable + addresses, etc. + + References: + + - https://github.com/iotaledger/wiki/blob/master/multisigs.md + """ + + def create_multisig_address(self, digests): + # type: (Iterable[Digest]) -> dict + """ + Generates a multisig address from a collection of digests. + + :param digests: + Digests to use to create the multisig address. + + .. important:: + In order to spend IOTAs from a multisig address, the + signature must be generated from the corresponding private + keys in the exact same order. + + :return: + Dict with the following items:: + + { + 'address': MultisigAddress, + The generated multisig address. + } + """ + return asyncio.get_event_loop().run_until_complete( + super(MultisigIota, self).create_multisig_address(digests) + ) + + def get_digests( + self, + index=0, + count=1, + security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL, + ): + # type: (int, int, int) -> dict + """ + Generates one or more key digests from the seed. + + Digests are safe to share; use them to generate multisig + addresses. + + :param index: + The starting key index. + + :param count: + Number of digests to generate. + + :param security_level: + Number of iterations to use when generating new addresses. + + Larger values take longer, but the resulting signatures are + more secure. + + This value must be between 1 and 3, inclusive. + + :return: + Dict with the following items:: + + { + 'digests': List[Digest], + Always contains a list, even if only one digest + was generated. + } + """ + return asyncio.get_event_loop().run_until_complete( + super(MultisigIota, self).get_digests( + index, + count, + security_level, + ) + ) + + def get_private_keys( + self, + index=0, + count=1, + security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL, + ): + # type: (int, int, int) -> dict + """ + Generates one or more private keys from the seed. + + As the name implies, private keys should not be shared. + However, in a few cases it may be necessary (e.g., for M-of-N + transactions). + + :param index: + The starting key index. + + :param count: + Number of keys to generate. + + :param security_level: + Number of iterations to use when generating new keys. + + Larger values take longer, but the resulting signatures are + more secure. + + This value must be between 1 and 3, inclusive. + + :return: + Dict with the following items:: + + { + 'keys': List[PrivateKey], + Always contains a list, even if only one key was + generated. + } + + References: + + - :py:class:`iota.crypto.signing.KeyGenerator` + - https://github.com/iotaledger/wiki/blob/master/multisigs.md#how-m-of-n-works + """ + return asyncio.get_event_loop().run_until_complete( + super(MultisigIota, self).get_private_keys( + index, + count, + security_level, + ) + ) + + def prepare_multisig_transfer( + self, + transfers, # type: Iterable[ProposedTransaction] + multisig_input, # type: MultisigAddress + change_address=None, # type: Optional[Address] + ): + # type: (...) -> dict + """ + Prepares a bundle that authorizes the spending of IOTAs from a + multisig address. + + .. note:: + This method is used exclusively to spend IOTAs from a + multisig address. + + If you want to spend IOTAs from non-multisig addresses, or + if you want to create 0-value transfers (i.e., that don't + require inputs), use + :py:meth:`iota.api.Iota.prepare_transfer` instead. + + :param transfers: + Transaction objects to prepare. + + .. important:: + Must include at least one transaction that spends IOTAs + (i.e., has a nonzero ``value``). If you want to prepare + a bundle that does not spend any IOTAs, use + :py:meth:`iota.api.prepare_transfer` instead. + + :param multisig_input: + The multisig address to use as the input for the transfers. + + .. note:: + This method only supports creating a bundle with a + single multisig input. + + If you would like to spend from multiple multisig + addresses in the same bundle, create the + :py:class:`iota.multisig.transaction.ProposedMultisigBundle` + object manually. + + :param change_address: + If inputs are provided, any unspent amount will be sent to + this address. + + If the bundle has no unspent inputs, ``change_address` is + ignored. + + .. important:: + Unlike :py:meth:`iota.api.Iota.prepare_transfer`, this + method will NOT generate a change address automatically. + If there are unspent inputs and ``change_address`` is + empty, an exception will be raised. + + This is because multisig transactions typically involve + multiple individuals, and it would be unfair to the + participants if we generated a change address + automatically using the seed of whoever happened to run + the ``prepare_multisig_transfer`` method! + + .. danger:: + Note that this protective measure is not a + substitute for due diligence! + + Always verify the details of every transaction in a + bundle (including the change transaction) before + signing the input(s)! + + :return: + Dict containing the following values:: + + { + 'trytes': List[TransactionTrytes], + Finalized bundle, as trytes. + The input transactions are not signed. + } + + In order to authorize the spending of IOTAs from the multisig + input, you must generate the correct private keys and invoke + the :py:meth:`iota.crypto.types.PrivateKey.sign_input_at` + method for each key, in the correct order. + + Once the correct signatures are applied, you can then perform + proof of work (``attachToTangle``) and broadcast the bundle + using :py:meth:`iota.api.Iota.send_trytes`. + """ + return asyncio.get_event_loop().run_until_complete( + super(MultisigIota, self).prepare_multisig_transfer( + transfers, + multisig_input, + change_address, + ) + ) \ No newline at end of file diff --git a/iota/multisig/commands/create_multisig_address.py b/iota/multisig/commands/create_multisig_address.py index 06ff70ce..9aa81b50 100644 --- a/iota/multisig/commands/create_multisig_address.py +++ b/iota/multisig/commands/create_multisig_address.py @@ -32,7 +32,9 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + # There is no async operation going on here, but the base class is async, + # so from the outside, we have to act like we are doing async. + async def _execute(self, request): digests = request['digests'] # type: List[Digest] builder = MultisigAddressBuilder() diff --git a/iota/multisig/commands/get_digests.py b/iota/multisig/commands/get_digests.py index 91256b90..48cbba65 100644 --- a/iota/multisig/commands/get_digests.py +++ b/iota/multisig/commands/get_digests.py @@ -32,13 +32,15 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + # There is no async operation going on here, but the base class is async, + # so from the outside, we have to act like we are doing async. + async def _execute(self, request): count = request['count'] # type: Optional[int] index = request['index'] # type: int seed = request['seed'] # type: Seed security_level = request['securityLevel'] # type: int - gpk_result = GetPrivateKeysCommand(self.adapter)( + gpk_result = await GetPrivateKeysCommand(self.adapter)( seed=seed, count=count, index=index, diff --git a/iota/multisig/commands/get_private_keys.py b/iota/multisig/commands/get_private_keys.py index 2a58abf7..58b9f0fa 100644 --- a/iota/multisig/commands/get_private_keys.py +++ b/iota/multisig/commands/get_private_keys.py @@ -33,7 +33,9 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + # There is no async operation going on here, but the base class is async, + # so from the outside, we have to act like we are doing async. + async def _execute(self, request): count = request['count'] # type: Optional[int] index = request['index'] # type: int seed = request['seed'] # type: Seed diff --git a/iota/multisig/commands/prepare_multisig_transfer.py b/iota/multisig/commands/prepare_multisig_transfer.py index 7d43c2cc..cc339de8 100644 --- a/iota/multisig/commands/prepare_multisig_transfer.py +++ b/iota/multisig/commands/prepare_multisig_transfer.py @@ -35,7 +35,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _execute(self, request): + async def _execute(self, request): change_address = request['changeAddress'] # type: Optional[Address] multisig_input = request['multisigInput'] # type: MultisigAddress transfers = request['transfers'] # type: List[ProposedTransaction] @@ -44,7 +44,7 @@ def _execute(self, request): want_to_spend = bundle.balance if want_to_spend > 0: - gb_response = GetBalancesCommand(self.adapter)( + gb_response = await GetBalancesCommand(self.adapter)( addresses=[multisig_input], ) diff --git a/setup.py b/setup.py index fca22f64..cd239280 100644 --- a/setup.py +++ b/setup.py @@ -28,6 +28,7 @@ # either automatically (``python setup.py test``) or manually # (``pip install -e .[test-runner]``). tests_require = [ + 'aiounittest', 'mock; python_version < "3.0"', 'nose', ] @@ -62,21 +63,17 @@ install_requires=[ 'filters; python_version < "3.5"', + 'httpx', 'phx-filters; python_version >= "3.5"', 'pysha3', - - # ``security`` extra wasn't introduced until 2.4.1 - # http://docs.python-requests.org/en/latest/community/updates/#id35 - 'requests[security] >= 2.4.1', - 'six', 'typing; python_version < "3.0"', ], extras_require={ 'ccurl': ['pyota-ccurl'], - 'pow': ['pyota-pow >= 1.0.2'], 'docs-builder': ['sphinx', 'sphinx_rtd_theme'], + 'pow': ['pyota-pow >= 1.0.2'], # tox is able to run the tests in parallel since version 3.7 'test-runner': ['tox >= 3.7'] + tests_require, }, @@ -91,10 +88,7 @@ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Topic :: Software Development :: Libraries :: Python Modules', diff --git a/test/__init__.py b/test/__init__.py index 0b9065db..5d3d1366 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -14,3 +14,6 @@ # noinspection PyUnresolvedReferences import mock from mock import MagicMock, patch + +# Executes async test case within a loop +from aiounittest import async_test diff --git a/test/adapter/wrappers_test.py b/test/adapter/wrappers_test.py index f8ec7e59..e58cc113 100644 --- a/test/adapter/wrappers_test.py +++ b/test/adapter/wrappers_test.py @@ -6,10 +6,12 @@ from iota.adapter import HttpAdapter, MockAdapter from iota.adapter.wrappers import RoutingWrapper +from test import async_test class RoutingWrapperTestCase(TestCase): - def test_routing(self): + @async_test + async def test_routing(self): """ Routing commands to different adapters. """ @@ -27,18 +29,18 @@ def test_routing(self): pow_adapter.seed_response('interruptAttachingToTangle', {'id': 'pow2'}) self.assertDictEqual( - wrapper.send_request({'command': 'attachToTangle'}), + await wrapper.send_request({'command': 'attachToTangle'}), {'id': 'pow1'}, ) self.assertDictEqual( - wrapper.send_request({'command': 'interruptAttachingToTangle'}), + await wrapper.send_request({'command': 'interruptAttachingToTangle'}), {'id': 'pow2'}, ) # Any commands that aren't routed go to the default adapter. self.assertDictEqual( - wrapper.send_request({'command': 'getNodeInfo'}), + await wrapper.send_request({'command': 'getNodeInfo'}), {'id': 'default1'}, ) diff --git a/test/adapter_test.py b/test/adapter_test.py index 7ea8ca4e..4b4c569c 100644 --- a/test/adapter_test.py +++ b/test/adapter_test.py @@ -7,12 +7,12 @@ from typing import Text from unittest import TestCase -import requests +import httpx from iota import BadApiResponse, InvalidUri, TryteString -from iota.adapter import API_VERSION, HttpAdapter, MockAdapter, resolve_adapter +from iota.adapter import API_VERSION, HttpAdapter, MockAdapter, \ + resolve_adapter, async_return from six import BytesIO, text_type -from test import mock - +from test import mock, async_test class ResolveAdapterTestCase(TestCase): """ @@ -55,20 +55,15 @@ def test_unknown_protocol(self): def create_http_response(content, status=200): - # type: (Text, int) -> requests.Response + # type: (Text, int) -> httpx.Response """ Creates an HTTP Response object for a test. - - References: - - :py:meth:`requests.adapters.HTTPAdapter.build_response` """ - response = requests.Response() - - response.encoding = 'utf-8' - response.status_code = status - response.raw = BytesIO(content.encode('utf-8')) - - return response + return httpx.Response( + status, + request=httpx.Request('post','https://localhost:14265/'), + content=content + ) class HttpAdapterTestCase(TestCase): @@ -134,7 +129,8 @@ def test_configure_error_udp(self): with self.assertRaises(InvalidUri): HttpAdapter.configure('udp://localhost:14265') - def test_success_response(self): + @async_test + async def test_success_response(self): """ Simulates sending a command to the node and getting a success response. @@ -145,11 +141,11 @@ def test_success_response(self): expected_result = {'message': 'Hello, IOTA!'} mocked_response = create_http_response(json.dumps(expected_result)) - mocked_sender = mock.Mock(return_value=mocked_response) + mocked_sender = mock.Mock(return_value=async_return(mocked_response)) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): - result = adapter.send_request(payload) + result = await adapter.send_request(payload) self.assertEqual(result, expected_result) @@ -164,7 +160,8 @@ def test_success_response(self): url = adapter.node_url, ) - def test_error_response(self): + @async_test + async def test_error_response(self): """ Simulates sending a command to the node and getting an error response. @@ -182,19 +179,20 @@ def test_error_response(self): }), ) - mocked_sender = mock.Mock(return_value=mocked_response) + mocked_sender = mock.Mock(return_value=async_return(mocked_response)) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - adapter.send_request({'command': 'helloWorld'}) + await adapter.send_request({'command': 'helloWorld'}) self.assertEqual( - text_type(context.exception), + str(context.exception), '400 response from node: {error}'.format(error=error_message), ) - def test_exception_response(self): + @async_test + async def test_exception_response(self): """ Simulates sending a command to the node and getting an exception response. @@ -212,19 +210,20 @@ def test_exception_response(self): }), ) - mocked_sender = mock.Mock(return_value=mocked_response) + mocked_sender = mock.Mock(return_value=async_return(mocked_response)) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - adapter.send_request({'command': 'helloWorld'}) + await adapter.send_request({'command': 'helloWorld'}) self.assertEqual( text_type(context.exception), '500 response from node: {error}'.format(error=error_message), ) - def test_non_200_status(self): + @async_test + async def test_non_200_status(self): """ The node sends back a non-200 response that we don't know how to handle. @@ -238,19 +237,20 @@ def test_non_200_status(self): content = json.dumps(decoded_response), ) - mocked_sender = mock.Mock(return_value=mocked_response) + mocked_sender = mock.Mock(return_value=async_return(mocked_response)) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - adapter.send_request({'command': 'helloWorld'}) + await adapter.send_request({'command': 'helloWorld'}) self.assertEqual( text_type(context.exception), '429 response from node: {decoded}'.format(decoded=decoded_response), ) - def test_empty_response(self): + @async_test + async def test_empty_response(self): """ The response is empty. """ @@ -258,19 +258,20 @@ def test_empty_response(self): mocked_response = create_http_response('') - mocked_sender = mock.Mock(return_value=mocked_response) + mocked_sender = mock.Mock(return_value=async_return(mocked_response)) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - adapter.send_request({'command': 'helloWorld'}) + await adapter.send_request({'command': 'helloWorld'}) self.assertEqual( text_type(context.exception), 'Empty 200 response from node.', ) - def test_non_json_response(self): + @async_test + async def test_non_json_response(self): """ The response is not JSON. """ @@ -279,19 +280,20 @@ def test_non_json_response(self): invalid_response = 'EHLO iotatoken.com' # Erm... mocked_response = create_http_response(invalid_response) - mocked_sender = mock.Mock(return_value=mocked_response) + mocked_sender = mock.Mock(return_value=async_return(mocked_response)) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - adapter.send_request({'command': 'helloWorld'}) + await adapter.send_request({'command': 'helloWorld'}) self.assertEqual( text_type(context.exception), 'Non-JSON 200 response from node: ' + invalid_response, ) - def test_non_object_response(self): + @async_test + async def test_non_object_response(self): """ The response is valid JSON, but it's not an object. """ @@ -300,12 +302,12 @@ def test_non_object_response(self): invalid_response = ['message', 'Hello, IOTA!'] mocked_response = create_http_response(json.dumps(invalid_response)) - mocked_sender = mock.Mock(return_value=mocked_response) + mocked_sender = mock.Mock(return_value=async_return(mocked_response)) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - adapter.send_request({'command': 'helloWorld'}) + await adapter.send_request({'command': 'helloWorld'}) self.assertEqual( text_type(context.exception), @@ -315,24 +317,36 @@ def test_non_object_response(self): ), ) - @mock.patch('iota.adapter.request') - def test_default_timeout(self, request_mock): - # create dummy response - request_mock.return_value = mock.Mock(text='{ "dummy": "payload"}', status_code=200) - + @async_test + async def test_default_timeout(self): # create adapter mock_payload = {'dummy': 'payload'} adapter = HttpAdapter('http://localhost:14265') - # test with default timeout - adapter.send_request(payload=mock_payload) - _, kwargs = request_mock.call_args + # mock for returning dummy response + mocked_request = mock.Mock( + return_value=async_return( + mock.Mock(text='{ "dummy": "payload"}', status_code=200) + ) + ) + + # noinspection PyUnresolvedReferences + with mock.patch('iota.adapter.AsyncClient.request', mocked_request): + # test with default timeout + await adapter.send_request(payload=mock_payload) + + # Was the default timeout correctly injected into the request? + _, kwargs = mocked_request.call_args self.assertEqual(kwargs['timeout'], socket.getdefaulttimeout()) - @mock.patch('iota.adapter.request') - def test_instance_attribute_timeout(self, request_mock): - # create dummy response - request_mock.return_value = mock.Mock(text='{ "dummy": "payload"}', status_code=200) + @async_test + async def test_instance_attribute_timeout(self): + # mock for returning dummy response + mocked_request = mock.Mock( + return_value=async_return( + mock.Mock(text='{ "dummy": "payload"}', status_code=200) + ) + ) # create adapter mock_payload = {'dummy': 'payload'} @@ -340,14 +354,19 @@ def test_instance_attribute_timeout(self, request_mock): # test with explicit attribute adapter.timeout = 77 - adapter.send_request(payload=mock_payload) - _, kwargs = request_mock.call_args + with mock.patch('iota.adapter.AsyncClient.request', mocked_request): + await adapter.send_request(payload=mock_payload) + _, kwargs = mocked_request.call_args self.assertEqual(kwargs['timeout'], 77) - @mock.patch('iota.adapter.request') - def test_argument_overriding_attribute_timeout(self, request_mock): - # create dummy response - request_mock.return_value = mock.Mock(text='{ "dummy": "payload"}', status_code=200) + @async_test + async def test_argument_overriding_attribute_timeout(self): + # mock for returning dummy response + mocked_request = mock.Mock( + return_value=async_return( + mock.Mock(text='{ "dummy": "payload"}', status_code=200) + ) + ) # create adapter mock_payload = {'dummy': 'payload'} @@ -355,14 +374,19 @@ def test_argument_overriding_attribute_timeout(self, request_mock): # test with timeout in kwargs adapter.timeout = 77 - adapter.send_request(payload=mock_payload, timeout=88) - _, kwargs = request_mock.call_args + with mock.patch('iota.adapter.AsyncClient.request', mocked_request): + await adapter.send_request(payload=mock_payload, timeout=88) + _, kwargs = mocked_request.call_args self.assertEqual(kwargs['timeout'], 88) - @mock.patch('iota.adapter.request') - def test_argument_overriding_init_timeout(self, request_mock): - # create dummy response - request_mock.return_value = mock.Mock(text='{ "dummy": "payload"}', status_code=200) + @async_test + async def test_argument_overriding_init_timeout(self): + # mock for returning dummy response + mocked_request = mock.Mock( + return_value=async_return( + mock.Mock(text='{ "dummy": "payload"}', status_code=200) + ) + ) # create adapter mock_payload = {'dummy': 'payload'} @@ -370,13 +394,14 @@ def test_argument_overriding_init_timeout(self, request_mock): # test with timeout at adapter creation adapter = HttpAdapter('http://localhost:14265', timeout=99) - adapter.send_request(payload=mock_payload) - _, kwargs = request_mock.call_args + with mock.patch('iota.adapter.AsyncClient.request', mocked_request): + await adapter.send_request(payload=mock_payload) + _, kwargs = mocked_request.call_args self.assertEqual(kwargs['timeout'], 99) # noinspection SpellCheckingInspection - @staticmethod - def test_trytes_in_request(): + @async_test + async def test_trytes_in_request(self): """ Sending a request that includes trytes. """ @@ -384,11 +409,11 @@ def test_trytes_in_request(): # Response is not important for this test; we just need to make # sure that the request is converted correctly. - mocked_sender = mock.Mock(return_value=create_http_response('{}')) + mocked_sender = mock.Mock(return_value=async_return(create_http_response('{}'))) # noinspection PyUnresolvedReferences with mock.patch.object(adapter, '_send_http_request', mocked_sender): - adapter.send_request({ + await adapter.send_request({ 'command': 'helloWorld', 'trytes': [ TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA'), diff --git a/test/api_test.py b/test/api_test.py index 95084f4b..745b4f2c 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -11,6 +11,7 @@ from iota.adapter import MockAdapter from iota.commands import CustomCommand from iota.commands.core.get_node_info import GetNodeInfoCommand +from test import async_test class CustomCommandTestCase(TestCase): @@ -21,7 +22,8 @@ def setUp(self): self.adapter = MockAdapter() self.command = CustomCommand(self.adapter, self.name) - def test_call(self): + @async_test + async def test_call(self): """ Sending a custom command. """ @@ -29,7 +31,7 @@ def test_call(self): self.adapter.seed_response('helloWorld', expected_response) - response = self.command() + response = await self.command() self.assertEqual(response, expected_response) self.assertTrue(self.command.called) @@ -39,7 +41,8 @@ def test_call(self): [{'command': 'helloWorld'}], ) - def test_call_with_parameters(self): + @async_test + async def test_call_with_parameters(self): """ Sending a custom command with parameters. """ @@ -47,7 +50,7 @@ def test_call_with_parameters(self): self.adapter.seed_response('helloWorld', expected_response) - response = self.command(foo='bar', baz='luhrmann') + response = await self.command(foo='bar', baz='luhrmann') self.assertEqual(response, expected_response) self.assertTrue(self.command.called) @@ -57,24 +60,26 @@ def test_call_with_parameters(self): [{'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}], ) - def test_call_error_already_called(self): + @async_test + async def test_call_error_already_called(self): """ A command can only be called once. """ self.adapter.seed_response('helloWorld', {}) - self.command() + await self.command() with self.assertRaises(RuntimeError): - self.command(extra='params') + await self.command(extra='params') self.assertDictEqual(self.command.request, {'command': 'helloWorld'}) - def test_call_reset(self): + @async_test + async def test_call_reset(self): """ Resetting a command allows it to be called more than once. """ self.adapter.seed_response('helloWorld', {'message': 'Hello, IOTA!'}) - self.command() + await self.command() self.command.reset() @@ -84,7 +89,7 @@ def test_call_reset(self): expected_response = {'message': 'Welcome back!'} self.adapter.seed_response('helloWorld', expected_response) - response = self.command(foo='bar') + response = await self.command(foo='bar') self.assertDictEqual(response, expected_response) self.assertDictEqual(self.command.response, expected_response) diff --git a/test/commands/core/add_neighbors_test.py b/test/commands/core/add_neighbors_test.py index 985c3acb..a65a6f3f 100644 --- a/test/commands/core/add_neighbors_test.py +++ b/test/commands/core/add_neighbors_test.py @@ -6,11 +6,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import StrictIota -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.add_neighbors import AddNeighborsCommand from iota.filters import NodeUri -from test import patch, MagicMock +from test import patch, MagicMock, async_test class AddNeighborsRequestFilterTestCase(BaseFilterTestCase): @@ -151,20 +151,42 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.add_neighbors.AddNeighborsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: - api = StrictIota(self.adapter) + api = Iota(self.adapter) response = api.add_neighbors('test_uri') self.assertTrue(mocked_command.called) + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.add_neighbors.AddNeighborsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.add_neighbors('test_uri') + + self.assertTrue(mocked_command.called) + self.assertEqual( response, 'You found me!' diff --git a/test/commands/core/attach_to_tangle_test.py b/test/commands/core/attach_to_tangle_test.py index 604057e9..83212953 100644 --- a/test/commands/core/attach_to_tangle_test.py +++ b/test/commands/core/attach_to_tangle_test.py @@ -6,12 +6,13 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash, TransactionTrytes, TryteString -from iota.adapter import MockAdapter +from iota import Iota, TransactionHash, TransactionTrytes, TryteString, \ + AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.attach_to_tangle import AttachToTangleCommand from iota.filters import Trytes from six import binary_type, text_type -from test import patch, MagicMock +from test import patch, MagicMock, async_test class AttachToTangleRequestFilterTestCase(BaseFilterTestCase): @@ -432,17 +433,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.attach_to_tangle.AttachToTangleCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.attach_to_tangle('trunk', 'branch', 'trytes') self.assertTrue(mocked_command.called) @@ -451,3 +451,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.attach_to_tangle.AttachToTangleCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.attach_to_tangle('trunk', 'branch', 'trytes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/broadcast_transactions_test.py b/test/commands/core/broadcast_transactions_test.py index e5c3b73e..2d20440c 100644 --- a/test/commands/core/broadcast_transactions_test.py +++ b/test/commands/core/broadcast_transactions_test.py @@ -8,12 +8,12 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type -from iota import Iota, TransactionTrytes, TryteString -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota, TransactionTrytes, TryteString +from iota.adapter import MockAdapter, async_return from iota.commands.core.broadcast_transactions import \ BroadcastTransactionsCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class BroadcastTransactionsRequestFilterTestCase(BaseFilterTestCase): filter_type = BroadcastTransactionsCommand(MockAdapter()).get_request_filter @@ -186,17 +186,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.broadcast_transactions.BroadcastTransactionsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.broadcast_transactions('trytes') self.assertTrue(mocked_command.called) @@ -205,3 +204,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.broadcast_transactions.BroadcastTransactionsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.broadcast_transactions('trytes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/check_consistency_test.py b/test/commands/core/check_consistency_test.py index 57517df0..b328788a 100644 --- a/test/commands/core/check_consistency_test.py +++ b/test/commands/core/check_consistency_test.py @@ -7,11 +7,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash, TryteString -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota, TransactionHash, TryteString +from iota.adapter import MockAdapter, async_return from iota.commands.core.check_consistency import CheckConsistencyCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class CheckConsistencyRequestFilterTestCase(BaseFilterTestCase): @@ -200,17 +200,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.check_consistency.CheckConsistencyCommand.__call__', - MagicMock(return_value='You found me!') - ) as mocked_command: + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.check_consistency('tails') self.assertTrue(mocked_command.called) @@ -220,7 +219,30 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.check_consistency.CheckConsistencyCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.check_consistency('tails') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Successfully checking consistency. """ @@ -229,7 +251,7 @@ def test_happy_path(self): 'state': True, }) - response = self.command(tails=[self.hash1, self.hash2]) + response = await self.command(tails=[self.hash1, self.hash2]) self.assertDictEqual( response, @@ -239,7 +261,8 @@ def test_happy_path(self): } ) - def test_info_with_false_state(self): + @async_test + async def test_info_with_false_state(self): """ `info` field exists when `state` is False. """ @@ -249,7 +272,7 @@ def test_info_with_false_state(self): 'info': 'Additional information', }) - response = self.command(tails=[self.hash1, self.hash2]) + response = await self.command(tails=[self.hash1, self.hash2]) self.assertDictEqual( response, @@ -258,4 +281,4 @@ def test_info_with_false_state(self): 'state': False, 'info': 'Additional information', } - ) + ) \ No newline at end of file diff --git a/test/commands/core/find_transactions_test.py b/test/commands/core/find_transactions_test.py index 3d5a72cb..cf4c7951 100644 --- a/test/commands/core/find_transactions_test.py +++ b/test/commands/core/find_transactions_test.py @@ -8,12 +8,13 @@ from filters.test import BaseFilterTestCase from six import text_type -from iota import Address, Iota, Tag, BundleHash, TransactionHash, TryteString -from iota.adapter import MockAdapter +from iota import Address, Iota, Tag, BundleHash, TransactionHash, TryteString, \ + AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.find_transactions import FindTransactionsCommand, \ FindTransactionsRequestFilter from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class FindTransactionsRequestFilterTestCase(BaseFilterTestCase): @@ -561,18 +562,39 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ - with patch('iota.commands.core.check_consistency.CheckConsistencyCommand.__call__', - MagicMock(return_value='You found me!') + with patch('iota.commands.core.find_transactions.FindTransactionsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. - response = api.check_consistency('tails') + response = api.find_transactions('addresses') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.find_transactions.FindTransactionsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.find_transactions('addresses') self.assertTrue(mocked_command.called) diff --git a/test/commands/core/get_balances_test.py b/test/commands/core/get_balances_test.py index bfd1450d..7bb5c911 100644 --- a/test/commands/core/get_balances_test.py +++ b/test/commands/core/get_balances_test.py @@ -7,11 +7,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, Iota, TryteString -from iota.adapter import MockAdapter +from iota import Address, Iota, TryteString, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.get_balances import GetBalancesCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetBalancesRequestFilterTestCase(BaseFilterTestCase): @@ -351,17 +351,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_balances.GetBalancesCommand.__call__', - MagicMock(return_value='You found me!') - ) as mocked_command: + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_balances('addresses') self.assertTrue(mocked_command.called) @@ -370,3 +369,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_balances.GetBalancesCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_balances('addresses') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/get_inclusion_states_test.py b/test/commands/core/get_inclusion_states_test.py index 873a49a1..31fb77f9 100644 --- a/test/commands/core/get_inclusion_states_test.py +++ b/test/commands/core/get_inclusion_states_test.py @@ -6,12 +6,12 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash, TryteString -from iota.adapter import MockAdapter +from iota import Iota, TransactionHash, TryteString, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.get_inclusion_states import GetInclusionStatesCommand from iota.filters import Trytes from six import binary_type, text_type -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetInclusionStatesRequestFilterTestCase(BaseFilterTestCase): @@ -259,17 +259,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_inclusion_states.GetInclusionStatesCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_inclusion_states('transactions', 'tips') self.assertTrue(mocked_command.called) @@ -278,3 +277,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_inclusion_states.GetInclusionStatesCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_inclusion_states('transactions', 'tips') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/get_missing_transactions_test.py b/test/commands/core/get_missing_transactions_test.py index 0c5adb00..859c69b3 100644 --- a/test/commands/core/get_missing_transactions_test.py +++ b/test/commands/core/get_missing_transactions_test.py @@ -7,10 +7,10 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash -from iota.adapter import MockAdapter +from iota import Iota, TransactionHash, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core import GetMissingTransactionsCommand -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetMissingTransactionsRequestFilterTestCase(BaseFilterTestCase): @@ -109,17 +109,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_missing_transactions.GetMissingTransactionsCommand.__call__', - MagicMock(return_value='You found me!') - ) as mocked_command: + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_missing_transactions() self.assertTrue(mocked_command.called) @@ -128,3 +127,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_missing_transactions.GetMissingTransactionsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_missing_transactions() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/get_neighbors_test.py b/test/commands/core/get_neighbors_test.py index 83e71172..4a890e6c 100644 --- a/test/commands/core/get_neighbors_test.py +++ b/test/commands/core/get_neighbors_test.py @@ -6,10 +6,10 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.get_neighbors import GetNeighborsCommand -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetNeighborsRequestFilterTestCase(BaseFilterTestCase): @@ -49,17 +49,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_neighbors.GetNeighborsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_neighbors() self.assertTrue(mocked_command.called) @@ -68,3 +67,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_neighbors.GetNeighborsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_neighbors() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/get_node_api_configuration_test.py b/test/commands/core/get_node_api_configuration_test.py index 9f154873..a24794f7 100644 --- a/test/commands/core/get_node_api_configuration_test.py +++ b/test/commands/core/get_node_api_configuration_test.py @@ -7,10 +7,10 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core import GetNodeAPIConfigurationCommand -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetNodeAPIConfigurationRequestFilterTestCase(BaseFilterTestCase): @@ -53,17 +53,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_node_api_configuration.GetNodeAPIConfigurationCommand.__call__', - MagicMock(return_value='You found me!') - ) as mocked_command: + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_node_api_configuration() self.assertTrue(mocked_command.called) @@ -72,3 +71,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_node_api_configuration.GetNodeAPIConfigurationCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_node_api_configuration() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/get_node_info_test.py b/test/commands/core/get_node_info_test.py index 9927ed74..b04a780e 100644 --- a/test/commands/core/get_node_info_test.py +++ b/test/commands/core/get_node_info_test.py @@ -6,10 +6,10 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash -from iota.adapter import MockAdapter +from iota import Iota, TransactionHash, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.get_node_info import GetNodeInfoCommand -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetNodeInfoRequestFilterTestCase(BaseFilterTestCase): @@ -122,17 +122,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_node_info.GetNodeInfoCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_node_info() self.assertTrue(mocked_command.called) @@ -141,3 +140,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_node_info.GetNodeInfoCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_node_info() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/get_tips_test.py b/test/commands/core/get_tips_test.py index 5dfb191d..11cb03b2 100644 --- a/test/commands/core/get_tips_test.py +++ b/test/commands/core/get_tips_test.py @@ -7,11 +7,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, Iota -from iota.adapter import MockAdapter +from iota import Address, Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.get_tips import GetTipsCommand from iota.transaction.types import TransactionHash -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetTipsRequestFilterTestCase(BaseFilterTestCase): @@ -120,17 +120,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_tips.GetTipsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_tips() self.assertTrue(mocked_command.called) @@ -140,6 +139,28 @@ def test_wireup(self): 'You found me!' ) + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_tips.GetTipsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_tips() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + def test_type_coercion(self): """ The result is coerced to the proper type. diff --git a/test/commands/core/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py index b5158000..652831bd 100644 --- a/test/commands/core/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -6,12 +6,12 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash -from iota.adapter import MockAdapter +from iota import Iota, TransactionHash, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.get_transactions_to_approve import \ GetTransactionsToApproveCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetTransactionsToApproveRequestFilterTestCase(BaseFilterTestCase): @@ -227,17 +227,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_transactions_to_approve.GetTransactionsToApproveCommand.__call__', - MagicMock(return_value='You found me!') - ) as mocked_command: + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_transactions_to_approve('depth') self.assertTrue(mocked_command.called) @@ -246,3 +245,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_transactions_to_approve.GetTransactionsToApproveCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_transactions_to_approve('depth') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/get_trytes_test.py b/test/commands/core/get_trytes_test.py index 2cf36bb0..99eba599 100644 --- a/test/commands/core/get_trytes_test.py +++ b/test/commands/core/get_trytes_test.py @@ -7,11 +7,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash, TryteString -from iota.adapter import MockAdapter +from iota import Iota, TransactionHash, TryteString, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.get_trytes import GetTrytesCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetTrytesRequestFilterTestCase(BaseFilterTestCase): @@ -242,17 +242,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.get_trytes.GetTrytesCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.get_trytes('hashes') self.assertTrue(mocked_command.called) @@ -261,3 +260,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.get_trytes.GetTrytesCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.get_trytes('hashes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/interrupt_attaching_to_tangle_test.py b/test/commands/core/interrupt_attaching_to_tangle_test.py index 3bcb8c76..c5e5cde3 100644 --- a/test/commands/core/interrupt_attaching_to_tangle_test.py +++ b/test/commands/core/interrupt_attaching_to_tangle_test.py @@ -6,11 +6,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.interrupt_attaching_to_tangle import \ InterruptAttachingToTangleCommand -from test import patch, MagicMock +from test import patch, MagicMock, async_test class InterruptAttachingToTangleRequestFilterTestCase(BaseFilterTestCase): @@ -48,17 +48,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.interrupt_attaching_to_tangle.InterruptAttachingToTangleCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.interrupt_attaching_to_tangle() self.assertTrue(mocked_command.called) @@ -67,3 +66,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.interrupt_attaching_to_tangle.InterruptAttachingToTangleCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.interrupt_attaching_to_tangle() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/remove_neighbors_test.py b/test/commands/core/remove_neighbors_test.py index 32ca749e..571745b6 100644 --- a/test/commands/core/remove_neighbors_test.py +++ b/test/commands/core/remove_neighbors_test.py @@ -6,11 +6,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.remove_neighbors import RemoveNeighborsCommand from iota.filters import NodeUri -from test import patch, MagicMock +from test import patch, MagicMock, async_test class RemoveNeighborsRequestFilterTestCase(BaseFilterTestCase): @@ -153,17 +153,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.remove_neighbors.RemoveNeighborsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.remove_neighbors('uris') self.assertTrue(mocked_command.called) @@ -172,3 +171,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.remove_neighbors.RemoveNeighborsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.remove_neighbors('uris') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/store_transactions_test.py b/test/commands/core/store_transactions_test.py index b1a87d1f..a034fce2 100644 --- a/test/commands/core/store_transactions_test.py +++ b/test/commands/core/store_transactions_test.py @@ -8,11 +8,11 @@ from filters.test import BaseFilterTestCase from six import text_type -from iota import Iota, TransactionTrytes, TryteString -from iota.adapter import MockAdapter +from iota import Iota, TransactionTrytes, TryteString, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core.store_transactions import StoreTransactionsCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class StoreTransactionsRequestFilterTestCase(BaseFilterTestCase): @@ -187,17 +187,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.store_transactions.StoreTransactionsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.store_transactions('trytes') self.assertTrue(mocked_command.called) @@ -206,3 +205,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.store_transactions.StoreTransactionsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.store_transactions('trytes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/core/were_addresses_spent_from_test.py b/test/commands/core/were_addresses_spent_from_test.py index 399a197b..e7b1c283 100644 --- a/test/commands/core/were_addresses_spent_from_test.py +++ b/test/commands/core/were_addresses_spent_from_test.py @@ -7,11 +7,11 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, Iota, TryteString -from iota.adapter import MockAdapter +from iota import Address, Iota, TryteString, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.core import WereAddressesSpentFromCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class WereAddressesSpentFromRequestFilterTestCase(BaseFilterTestCase): @@ -169,17 +169,16 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.core.were_addresses_spent_from.WereAddressesSpentFromCommand.__call__', - MagicMock(return_value='You found me!') - ) as mocked_command: + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: api = Iota(self.adapter) - # Don't need to call with proper args here. response = api.were_addresses_spent_from('addresses') self.assertTrue(mocked_command.called) @@ -188,3 +187,25 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.core.were_addresses_spent_from.WereAddressesSpentFromCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + response = await api.were_addresses_spent_from('addresses') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/extended/broadcast_and_store_test.py b/test/commands/extended/broadcast_and_store_test.py index 9354784e..6cd19f1c 100644 --- a/test/commands/extended/broadcast_and_store_test.py +++ b/test/commands/extended/broadcast_and_store_test.py @@ -6,11 +6,10 @@ from six import text_type -from iota import Iota, TransactionTrytes -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota, TransactionTrytes +from iota.adapter import MockAdapter, async_return from iota.commands.extended.broadcast_and_store import BroadcastAndStoreCommand -from test import patch, MagicMock - +from test import patch, MagicMock, async_test class BroadcastAndStoreCommandTestCase(TestCase): # noinspection SpellCheckingInspection @@ -27,12 +26,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.broadcast_and_store.BroadcastAndStoreCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -47,7 +46,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.broadcast_and_store.BroadcastAndStoreCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.broadcast_and_store('trytes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Successful invocation of ``broadcastAndStore``. """ @@ -65,6 +88,6 @@ def test_happy_path(self): TransactionTrytes(self.trytes2), ] - response = self.command(trytes=trytes) + response = await self.command(trytes=trytes) self.assertDictEqual(response, {'trytes': trytes}) diff --git a/test/commands/extended/broadcast_bundle_test.py b/test/commands/extended/broadcast_bundle_test.py index 3d047801..a4af43e7 100644 --- a/test/commands/extended/broadcast_bundle_test.py +++ b/test/commands/extended/broadcast_bundle_test.py @@ -8,11 +8,11 @@ from filters.test import BaseFilterTestCase from iota import Address, BadApiResponse, Bundle, BundleHash, Fragment, Hash, \ - Iota, Tag, Transaction, TransactionHash, TransactionTrytes, Nonce -from iota.adapter import MockAdapter + Iota, AsyncIota, Tag, Transaction, TransactionHash, TransactionTrytes, Nonce +from iota.adapter import MockAdapter, async_return from iota.commands.extended.broadcast_bundle import BroadcastBundleCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test # RequestFilterTestCase code reused from get_bundles_test.py @@ -142,12 +142,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.broadcast_bundle.BroadcastBundleCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -161,8 +161,32 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.broadcast_bundle.BroadcastBundleCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.broadcast_bundle('trytes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) - def test_happy_path(self): + @async_test + async def test_happy_path(self): """ Test command flow executes as expected. """ @@ -174,22 +198,23 @@ def test_happy_path(self): # whole command, so no filter is applied. It is safe because it is tested # elsewhere. with patch('iota.commands.extended.get_bundles.GetBundlesCommand.__call__', - MagicMock(return_value=[self.trytes])) as mocked_get_bundles: + MagicMock(return_value=async_return([self.trytes]))) as mocked_get_bundles: # We could seed a reponse to our MockAdapter, but then the returned value # from `GetBundlesCommand` shall be valid to pass # BroadcastTransactionRequestFilter. # Anyway, nature loves symmetry and so do we. with patch('iota.commands.core.BroadcastTransactionsCommand.__call__', - MagicMock(return_value=[])) as mocked_broadcast: + MagicMock(return_value=async_return([]))) as mocked_broadcast: - response = self.command(tail_hash=self.tail) + response = await self.command(tail_hash=self.tail) self.assertEqual( response['trytes'], self.trytes ) - def test_happy_path_multiple_bundle(self): + @async_test + async def test_happy_path_multiple_bundle(self): """ Test if command returns the correct bundle if underlying `get_bundles` returns multiple bundles. @@ -199,12 +224,12 @@ def test_happy_path_multiple_bundle(self): # BroadcastTransactionsCommand either. # Note that GetBundlesCommand returns multiple bundles! with patch('iota.commands.extended.get_bundles.GetBundlesCommand.__call__', - MagicMock(return_value=[self.trytes, self.trytes_dummy]) + MagicMock(return_value=async_return([self.trytes, self.trytes_dummy])) ) as mocked_get_bundles: with patch('iota.commands.core.BroadcastTransactionsCommand.__call__', - MagicMock(return_value=[])) as mocked_broadcast: + MagicMock(return_value=async_return([]))) as mocked_broadcast: - response = self.command(tail_hash=self.tail) + response = await self.command(tail_hash=self.tail) # Expect only the first bundle self.assertEqual( diff --git a/test/commands/extended/find_transaction_objects.py b/test/commands/extended/find_transaction_objects_test.py similarity index 72% rename from test/commands/extended/find_transaction_objects.py rename to test/commands/extended/find_transaction_objects_test.py index e87b3805..ddc15e07 100644 --- a/test/commands/extended/find_transaction_objects.py +++ b/test/commands/extended/find_transaction_objects_test.py @@ -4,9 +4,10 @@ from unittest import TestCase -from iota import Iota, MockAdapter, Transaction +from iota import Iota, AsyncIota, MockAdapter, Transaction from iota.commands.extended import FindTransactionObjectsCommand -from test import patch, MagicMock, mock +from iota.adapter import async_return +from test import patch, MagicMock, mock, async_test class FindTransactionObjectsCommandTestCase(TestCase): @@ -70,12 +71,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.find_transaction_objects.FindTransactionObjectsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -90,7 +91,53 @@ def test_wireup(self): 'You found me!' ) - def test_transaction_found(self): + def test_wireup(self): + """ + Verify that the command is wired up correctly. (sync) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.find_transaction_objects.FindTransactionObjectsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = Iota(self.adapter) + + # Don't need to call with proper args here. + response = api.find_transaction_objects('bundle') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.find_transaction_objects.FindTransactionObjectsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.find_transaction_objects('bundle') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_transaction_found(self): """ A transaction is found with the inputs. A transaction object is returned @@ -98,29 +145,30 @@ def test_transaction_found(self): with mock.patch( 'iota.commands.core.find_transactions.FindTransactionsCommand.' '_execute', - mock.Mock(return_value={'hashes': [self.transaction_hash, ]}), + mock.Mock(return_value=async_return({'hashes': [self.transaction_hash, ]})), ): with mock.patch( 'iota.commands.core.get_trytes.GetTrytesCommand._execute', - mock.Mock(return_value={'trytes': [self.trytes, ]}), + mock.Mock(return_value=async_return({'trytes': [self.trytes, ]})), ): - response = self.command(addresses=[self.address]) + response = await self.command(addresses=[self.address]) self.assertEqual(len(response['transactions']), 1) transaction = response['transactions'][0] self.assertIsInstance(transaction, Transaction) self.assertEqual(transaction.address, self.address) - def test_no_transactions_fround(self): + @async_test + async def test_no_transactions_fround(self): """ No transaction is found with the inputs. An empty list is returned """ with mock.patch( 'iota.commands.core.find_transactions.FindTransactionsCommand.' '_execute', - mock.Mock(return_value={'hashes': []}), + mock.Mock(return_value=async_return({'hashes': []})), ): - response = self.command(addresses=[self.address]) + response = await self.command(addresses=[self.address]) self.assertDictEqual( response, diff --git a/test/commands/extended/get_account_data_test.py b/test/commands/extended/get_account_data_test.py index 1a0e524a..0d53adf1 100644 --- a/test/commands/extended/get_account_data_test.py +++ b/test/commands/extended/get_account_data_test.py @@ -8,14 +8,14 @@ from filters.test import BaseFilterTestCase from six import binary_type -from iota import Address, Bundle, Iota, TransactionHash -from iota.adapter import MockAdapter +from iota import Address, Bundle, Iota, AsyncIota, TransactionHash +from iota.adapter import MockAdapter, async_return from iota.commands.extended.get_account_data import GetAccountDataCommand, \ GetAccountDataRequestFilter from iota.crypto.types import Seed from iota.filters import Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetAccountDataRequestFilterTestCase(BaseFilterTestCase): @@ -322,6 +322,18 @@ def test_fail_inclusion_states_wrong_type(self): }, ) +class AsyncIter: + """ + Class for mocking async generators. + + Used here to mock the return values of `iter_used_addresses`. + """ + def __init__(self, items): + self.items = items + + async def __aiter__(self): + for item in self.items: + yield item class GetAccountDataCommandTestCase(TestCase): # noinspection SpellCheckingInspection @@ -362,12 +374,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.get_account_data.GetAccountDataCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -382,12 +394,36 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.get_account_data.GetAccountDataCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_account_data() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Loading account data for an account. """ # noinspection PyUnusedLocal - def mock_iter_used_addresses(adapter, seed, start, security_level): + async def mock_iter_used_addresses(adapter, seed, start, security_level): """ Mocks the ``iter_used_addresses`` function, so that we can simulate its functionality without actually connecting to the @@ -399,12 +435,12 @@ def mock_iter_used_addresses(adapter, seed, start, security_level): yield self.addy1, [self.hash1] yield self.addy2, [self.hash2] - mock_get_balances = mock.Mock(return_value={'balances': [42, 0]}) + mock_get_balances = mock.Mock(return_value=async_return({'balances': [42, 0]})) # Not particularly realistic, but good enough to prove that the # mocked function was invoked correctly. bundles = [Bundle(), Bundle()] - mock_get_bundles_from_transaction_hashes = mock.Mock(return_value=bundles) + mock_get_bundles_from_transaction_hashes = mock.Mock(return_value=async_return(bundles)) with mock.patch( 'iota.commands.extended.get_account_data.iter_used_addresses', @@ -418,7 +454,7 @@ def mock_iter_used_addresses(adapter, seed, start, security_level): 'iota.commands.core.get_balances.GetBalancesCommand._execute', mock_get_balances, ): - response = self.command(seed=Seed.random()) + response = await self.command(seed=Seed.random()) self.assertDictEqual( response, @@ -430,15 +466,16 @@ def mock_iter_used_addresses(adapter, seed, start, security_level): }, ) - def test_no_transactions(self): + @async_test + async def test_no_transactions(self): """ Loading account data for a seed that hasn't been used yet. """ with mock.patch( 'iota.commands.extended.get_account_data.iter_used_addresses', - mock.Mock(return_value=[]), + mock.Mock(return_value=AsyncIter([])), ): - response = self.command(seed=Seed.random()) + response = await self.command(seed=Seed.random()) self.assertDictEqual( response, @@ -450,20 +487,21 @@ def test_no_transactions(self): }, ) - def test_balance_is_found_for_address_without_transaction(self): + @async_test + async def test_balance_is_found_for_address_without_transaction(self): """ If an address has a balance, no transactions and was spent from, the balance should still be found and returned. """ with mock.patch( 'iota.commands.extended.get_account_data.iter_used_addresses', - mock.Mock(return_value=[(self.addy1, [])]), + mock.Mock(return_value=AsyncIter([(self.addy1, [])])), ): self.adapter.seed_response('getBalances', { 'balances': [42], }) - response = self.command(seed=Seed.random()) + response = await self.command(seed=Seed.random()) self.assertDictEqual( response, diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py index 4c91e85b..f079bec3 100644 --- a/test/commands/extended/get_bundles_test.py +++ b/test/commands/extended/get_bundles_test.py @@ -8,11 +8,11 @@ from filters.test import BaseFilterTestCase from iota import Address, BadApiResponse, Bundle, \ - Iota, TransactionHash, TransactionTrytes -from iota.adapter import MockAdapter + Iota, AsyncIota, TransactionHash, TransactionTrytes +from iota.adapter import MockAdapter, async_return from iota.commands.extended.get_bundles import GetBundlesCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetBundlesRequestFilterTestCase(BaseFilterTestCase): @@ -330,12 +330,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.get_bundles.GetBundlesCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -350,7 +350,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.get_bundles.GetBundlesCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_bundles('transactions') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Get a bundle with multiple transactions. """ @@ -363,7 +387,7 @@ def test_happy_path(self): 'trytes': [self.spam_trytes], }) - response = self.command(transactions = [self.tx_hash]) + response = await self.command(transactions = [self.tx_hash]) self.maxDiff = None original_bundle = Bundle.from_tryte_strings(self.bundle_trytes) @@ -372,7 +396,8 @@ def test_happy_path(self): original_bundle.as_json_compatible(), ) - def test_happy_path_multiple_bundles(self): + @async_test + async def test_happy_path_multiple_bundles(self): """ Get two bundles with multiple transactions. """ @@ -387,7 +412,7 @@ def test_happy_path_multiple_bundles(self): 'trytes': [self.spam_trytes], }) - response = self.command(transactions = [self.tx_hash, self.tx_hash]) + response = await self.command(transactions = [self.tx_hash, self.tx_hash]) self.maxDiff = None original_bundle = Bundle.from_tryte_strings(self.bundle_trytes) @@ -402,7 +427,8 @@ def test_happy_path_multiple_bundles(self): original_bundle.as_json_compatible(), ) - def test_validator_error(self): + @async_test + async def test_validator_error(self): """ TraverseBundleCommand returns bundle but it is invalid. """ @@ -420,4 +446,4 @@ def test_validator_error(self): }) with self.assertRaises(BadApiResponse): - response = self.command(transactions = [self.tx_hash]) \ No newline at end of file + response = await self.command(transactions = [self.tx_hash]) \ No newline at end of file diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 439a687f..6ac6697b 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -7,14 +7,14 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, BadApiResponse, Iota, TransactionHash -from iota.adapter import MockAdapter +from iota import Address, BadApiResponse, Iota, AsyncIota, TransactionHash +from iota.adapter import MockAdapter, async_return from iota.commands.extended.get_inputs import GetInputsCommand, GetInputsRequestFilter from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetInputsRequestFilterTestCase(BaseFilterTestCase): @@ -441,12 +441,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.get_inputs.GetInputsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -461,7 +461,31 @@ def test_wireup(self): 'You found me!' ) - def test_stop_threshold_met(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.get_inputs.GetInputsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_inputs() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_stop_threshold_met(self): """ ``stop`` provided, balance meets ``threshold``. """ @@ -478,7 +502,7 @@ def test_stop_threshold_met(self): 'iota.crypto.addresses.AddressGenerator.get_addresses', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), stop = 2, threshold = 71, @@ -501,7 +525,8 @@ def test_stop_threshold_met(self): self.assertEqual(input1.balance, 29) self.assertEqual(input1.key_index, 1) - def test_stop_threshold_not_met(self): + @async_test + async def test_stop_threshold_not_met(self): """ ``stop`` provided, balance does not meet ``threshold``. """ @@ -519,13 +544,14 @@ def test_stop_threshold_not_met(self): mock_address_generator, ): with self.assertRaises(BadApiResponse): - self.command( + await self.command( seed = Seed.random(), stop = 2, threshold = 72, ) - def test_stop_threshold_zero(self): + @async_test + async def test_stop_threshold_zero(self): """ ``stop`` provided, ``threshold`` is 0. """ @@ -543,7 +569,7 @@ def test_stop_threshold_zero(self): 'iota.crypto.addresses.AddressGenerator.get_addresses', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), stop = 2, threshold = 0, @@ -560,7 +586,8 @@ def test_stop_threshold_zero(self): self.assertEqual(input0.balance, 1) self.assertEqual(input0.key_index, 1) - def test_stop_no_threshold(self): + @async_test + async def test_stop_no_threshold(self): """ ``stop`` provided, no ``threshold``. """ @@ -577,7 +604,7 @@ def test_stop_no_threshold(self): 'iota.crypto.addresses.AddressGenerator.get_addresses', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), start = 0, stop = 2, @@ -600,7 +627,8 @@ def test_stop_no_threshold(self): self.assertEqual(input1.balance, 29) self.assertEqual(input1.key_index, 1) - def test_no_stop_threshold_met(self): + @async_test + async def test_no_stop_threshold_met(self): """ No ``stop`` provided, balance meets ``threshold``. """ @@ -652,7 +680,7 @@ def mock_address_generator(ag, start, step=1): 'iota.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), threshold = 71, ) @@ -674,7 +702,8 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input1.balance, 29) self.assertEqual(input1.key_index, 1) - def test_no_stop_threshold_not_met(self): + @async_test + async def test_no_stop_threshold_not_met(self): """ No ``stop`` provided, balance does not meet ``threshold``. """ @@ -696,12 +725,13 @@ def mock_address_generator(ag, start, step=1): mock_address_generator, ): with self.assertRaises(BadApiResponse): - self.command( + await self.command( seed = Seed.random(), threshold = 72, ) - def test_no_stop_threshold_zero(self): + @async_test + async def test_no_stop_threshold_zero(self): """ No ``stop`` provided, ``threshold`` is 0. """ @@ -755,7 +785,7 @@ def mock_address_generator(ag, start, step=1): 'iota.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), threshold = 0, ) @@ -771,7 +801,8 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input0.balance, 1) self.assertEqual(input0.key_index, 1) - def test_no_stop_no_threshold(self): + @async_test + async def test_no_stop_no_threshold(self): """ No ``stop`` provided, no ``threshold``. """ @@ -823,7 +854,7 @@ def mock_address_generator(ag, start, step=1): 'iota.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), ) @@ -844,7 +875,8 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input1.balance, 29) self.assertEqual(input1.key_index, 1) - def test_start(self): + @async_test + async def test_start(self): """ Using ``start`` to offset the key range. """ @@ -888,7 +920,7 @@ def mock_address_generator(ag, start, step=1): 'iota.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), start = 1, ) @@ -903,7 +935,8 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input0.balance, 86) self.assertEqual(input0.key_index, 1) - def test_start_stop(self): + @async_test + async def test_start_stop(self): """ Using ``start`` and ``stop`` at once. Checking if correct number of addresses is returned. Must be stop - start @@ -927,7 +960,7 @@ def mock_address_generator(ag, start, step=1): 'iota.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): - response = self.command( + response = await self.command( seed = Seed.random(), start = 1, stop = 3, @@ -949,7 +982,8 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input1.key_index, 2) - def test_security_level_1_no_stop(self): + @async_test + async def test_security_level_1_no_stop(self): """ Testing GetInputsCoommand: - with security_level = 1 (non default) @@ -984,7 +1018,7 @@ def test_security_level_1_no_stop(self): 'balances': [86], }) - response = GetInputsCommand(self.adapter)( + response = await GetInputsCommand(self.adapter)( seed=seed, securityLevel=1, ) @@ -998,7 +1032,8 @@ def test_security_level_1_no_stop(self): self.assertEqual(input0.balance, 86) self.assertEqual(input0.key_index, 0) - def test_security_level_1_with_stop(self): + @async_test + async def test_security_level_1_with_stop(self): """ Testing GetInputsCoommand: - with security_level = 1 (non default) @@ -1028,7 +1063,7 @@ def test_security_level_1_with_stop(self): 'hashes': [], }) - response = GetInputsCommand(self.adapter)( + response = await GetInputsCommand(self.adapter)( seed=seed, securityLevel=1, stop=1, # <<<<< here diff --git a/test/commands/extended/get_latest_inclusion_test.py b/test/commands/extended/get_latest_inclusion_test.py index 4680693d..611f79f1 100644 --- a/test/commands/extended/get_latest_inclusion_test.py +++ b/test/commands/extended/get_latest_inclusion_test.py @@ -7,12 +7,12 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash, TryteString -from iota.adapter import MockAdapter +from iota import Iota, AsyncIota, TransactionHash, TryteString +from iota.adapter import MockAdapter, async_return from iota.commands.extended.get_latest_inclusion import \ GetLatestInclusionCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetLatestInclusionRequestFilterTestCase(BaseFilterTestCase): @@ -204,12 +204,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -224,7 +224,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_latest_inclusion('hashes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Successfully requesting latest inclusion state. """ @@ -239,7 +263,7 @@ def test_happy_path(self): 'states': [True, False], }) - response = self.command(hashes=[self.hash1, self.hash2]) + response = await self.command(hashes=[self.hash1, self.hash2]) self.assertDictEqual( response, diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index bf14b0a1..51bc514c 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -7,13 +7,13 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, Iota -from iota.adapter import MockAdapter +from iota import Address, Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.get_new_addresses import GetNewAddressesCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetNewAddressesRequestFilterTestCase(BaseFilterTestCase): @@ -365,12 +365,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.get_new_addresses.GetNewAddressesCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -385,13 +385,37 @@ def test_wireup(self): 'You found me!' ) - def test_get_addresses_offline(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.get_new_addresses.GetNewAddressesCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_new_addresses('hashes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_get_addresses_offline(self): """ Generate addresses in offline mode (without filtering used addresses). """ response =\ - self.command( + await self.command( count = 2, index = 0, seed = self.seed, @@ -405,12 +429,13 @@ def test_get_addresses_offline(self): # No API requests were made. self.assertListEqual(self.adapter.requests, []) - def test_security_level(self): + @async_test + async def test_security_level(self): """ Generating addresses with a different security level. """ response =\ - self.command( + await self.command( count = 2, index = 0, securityLevel = 1, @@ -437,7 +462,8 @@ def test_security_level(self): }, ) - def test_get_addresses_online_already_spent_from(self): + @async_test + async def test_get_addresses_online_already_spent_from(self): """ Generate address in online mode (filtering used addresses). Test if an address that was already spent from will not be returned. @@ -449,6 +475,11 @@ def test_get_addresses_online_already_spent_from(self): 'states': [True], }) + self.adapter.seed_response('findTransactions', { + 'duration': 1, + 'hashes': [], + }) + self.adapter.seed_response('wereAddressesSpentFrom', { 'states': [False], }) @@ -459,7 +490,7 @@ def test_get_addresses_online_already_spent_from(self): }) response =\ - self.command( + await self.command( # If ``count`` is missing or ``None``, the command will operate # in online mode. # count = None, @@ -472,18 +503,22 @@ def test_get_addresses_online_already_spent_from(self): # it skipped that one. self.assertDictEqual(response, {'addresses': [self.addy_2]}) - self.assertListEqual( + # Due to running WereAddressesSpentFromCommand and FindTransactionsCommand + # with asyncio.gather, we can't infer their execution order. Therefore, + # we need to assert if the contents of the two lists match by value, + # regardless of their order: + # https://docs.python.org/3.5/library/unittest.html#unittest.TestCase.assertCountEqual + self.assertCountEqual( self.adapter.requests, - - # The command issued a `wereAddressesSpentFrom` API request to - # check if the first address was used. Then it called `wereAddressesSpentFrom` - # and `findTransactions` to verify that the second address was - # indeed not used. [ { 'command': 'wereAddressesSpentFrom', 'addresses': [self.addy_1], }, + { + 'command': 'findTransactions', + 'addresses': [self.addy_1], + }, { 'command': 'wereAddressesSpentFrom', 'addresses': [self.addy_2], @@ -495,7 +530,8 @@ def test_get_addresses_online_already_spent_from(self): ], ) - def test_get_addresses_online_has_transaction(self): + @async_test + async def test_get_addresses_online_has_transaction(self): """ Generate address in online mode (filtering used addresses). Test if an address that has a transaction will not be returned. @@ -521,13 +557,13 @@ def test_get_addresses_online_has_transaction(self): 'hashes': [], }) - response = self.command(index=0, seed=self.seed) + response = await self.command(index=0, seed=self.seed) # The command determined that ``self.addy1`` was already used, so # it skipped that one. self.assertDictEqual(response, {'addresses': [self.addy_2]}) - self.assertListEqual( + self.assertCountEqual( self.adapter.requests, [ { @@ -549,12 +585,13 @@ def test_get_addresses_online_has_transaction(self): ], ) - def test_new_address_checksum(self): + @async_test + async def test_new_address_checksum(self): """ Generate address with a checksum. """ response =\ - self.command( + await self.command( checksum = True, count = 1, index = 0, diff --git a/test/commands/extended/get_transaction_objects_test.py b/test/commands/extended/get_transaction_objects_test.py index 552889a4..f283957b 100644 --- a/test/commands/extended/get_transaction_objects_test.py +++ b/test/commands/extended/get_transaction_objects_test.py @@ -4,9 +4,10 @@ from unittest import TestCase -from iota import Iota, MockAdapter, Transaction +from iota import Iota, AsyncIota, MockAdapter, Transaction from iota.commands.extended import GetTransactionObjectsCommand -from test import patch, MagicMock, mock +from iota.adapter import async_return +from test import patch, MagicMock, mock, async_test class GetTransactionObjectsCommandTestCase(TestCase): @@ -69,12 +70,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.get_transaction_objects.GetTransactionObjectsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -89,31 +90,56 @@ def test_wireup(self): 'You found me!' ) - def test_transaction_found(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.get_transaction_objects.GetTransactionObjectsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_transaction_objects('hashes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_transaction_found(self): """ A transaction is found with the inputs. A transaction object is returned """ with mock.patch( 'iota.commands.core.get_trytes.GetTrytesCommand._execute', - mock.Mock(return_value={'trytes': [self.trytes, ]}), + mock.Mock(return_value=async_return({'trytes': [self.trytes, ]})), ): - response = self.command(hashes=[self.transaction_hash]) + response = await self.command(hashes=[self.transaction_hash]) self.assertEqual(len(response['transactions']), 1) transaction = response['transactions'][0] self.assertIsInstance(transaction, Transaction) self.assertEqual(transaction.hash, self.transaction_hash) - def test_no_transactions_fround(self): + @async_test + async def test_no_transactions_fround(self): """ No transaction is found with the inputs. An empty list is returned """ with mock.patch( 'iota.commands.core.get_trytes.GetTrytesCommand._execute', - mock.Mock(return_value={'trytes': []}), + mock.Mock(return_value=async_return({'trytes': []})), ): - response = self.command(hashes=[self.transaction_hash]) + response = await self.command(hashes=[self.transaction_hash]) self.assertDictEqual( response, diff --git a/test/commands/extended/get_transfers_test.py b/test/commands/extended/get_transfers_test.py index 7373daf4..896ec0fb 100644 --- a/test/commands/extended/get_transfers_test.py +++ b/test/commands/extended/get_transfers_test.py @@ -8,14 +8,14 @@ from filters.test import BaseFilterTestCase from six import binary_type -from iota import Address, Bundle, Iota, Tag, Transaction, TryteString -from iota.adapter import MockAdapter +from iota import Address, Bundle, Iota, AsyncIota, Tag, Transaction, TryteString +from iota.adapter import MockAdapter, async_return from iota.commands.extended.get_transfers import GetTransfersCommand, \ GetTransfersRequestFilter from iota.crypto.types import Seed from iota.filters import Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class GetTransfersRequestFilterTestCase(BaseFilterTestCase): @@ -339,12 +339,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.get_transfers.GetTransfersCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -358,7 +358,32 @@ def test_wireup(self): response, 'You found me!' ) - def test_full_scan(self): + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.get_transfers.GetTransfersCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_transfers() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_full_scan(self): """ Scanning the Tangle for all transfers. """ @@ -437,9 +462,9 @@ def create_generator(ag, start, step=1): ]) mock_get_bundles =\ - mock.Mock(return_value={ + mock.Mock(return_value=async_return({ 'bundles': [bundle], - }) + })) with mock.patch( 'iota.crypto.addresses.AddressGenerator.create_iterator', @@ -449,7 +474,7 @@ def create_generator(ag, start, step=1): 'iota.commands.extended.get_bundles.GetBundlesCommand._execute', mock_get_bundles, ): - response = self.command(seed=Seed.random()) + response = await self.command(seed=Seed.random()) self.assertDictEqual( response, @@ -459,7 +484,8 @@ def create_generator(ag, start, step=1): }, ) - def test_no_transactions(self): + @async_test + async def test_no_transactions(self): """ There are no transactions for the specified seed. """ @@ -491,11 +517,12 @@ def create_generator(ag, start, step=1): 'iota.crypto.addresses.AddressGenerator.create_iterator', create_generator, ): - response = self.command(seed=Seed.random()) + response = await self.command(seed=Seed.random()) self.assertDictEqual(response, {'bundles': []}) - def test_start(self): + @async_test + async def test_start(self): """ Scanning the Tangle for all transfers, with start index. """ @@ -568,9 +595,9 @@ def create_generator(ag, start, step=1): ) ]) - mock_get_bundles = mock.Mock(return_value={ + mock_get_bundles = mock.Mock(return_value=async_return({ 'bundles': [bundle], - }) + })) with mock.patch( 'iota.crypto.addresses.AddressGenerator.create_iterator', @@ -580,7 +607,7 @@ def create_generator(ag, start, step=1): 'iota.commands.extended.get_bundles.GetBundlesCommand._execute', mock_get_bundles, ): - response = self.command(seed=Seed.random(), start=1) + response = await self.command(seed=Seed.random(), start=1) self.assertDictEqual( response, @@ -590,7 +617,8 @@ def create_generator(ag, start, step=1): }, ) - def test_stop(self): + @async_test + async def test_stop(self): """ Scanning the Tangle for all transfers, with stop index. """ @@ -646,9 +674,9 @@ def create_generator(ag, start, step=1): ) ]) - mock_get_bundles = mock.Mock(return_value={ + mock_get_bundles = mock.Mock(return_value=async_return({ 'bundles': [bundle], - }) + })) with mock.patch( 'iota.crypto.addresses.AddressGenerator.create_iterator', @@ -658,7 +686,7 @@ def create_generator(ag, start, step=1): 'iota.commands.extended.get_bundles.GetBundlesCommand._execute', mock_get_bundles, ): - response = self.command(seed=Seed.random(), stop=1) + response = await self.command(seed=Seed.random(), stop=1) self.assertDictEqual( response, @@ -668,7 +696,8 @@ def create_generator(ag, start, step=1): }, ) - def test_get_inclusion_states(self): + @async_test + async def test_get_inclusion_states(self): """ Fetching inclusion states with transactions. """ @@ -748,15 +777,15 @@ def create_generator(ag, start, step=1): transaction = Transaction.from_tryte_string(transaction_trytes) - mock_get_bundles = mock.Mock(return_value={ + mock_get_bundles = mock.Mock(return_value=async_return({ 'bundles': [Bundle([transaction])], - }) + })) - mock_get_latest_inclusion = mock.Mock(return_value={ + mock_get_latest_inclusion = mock.Mock(return_value=async_return({ 'states': { transaction.hash: True, }, - }) + })) with mock.patch( 'iota.crypto.addresses.AddressGenerator.create_iterator', @@ -770,7 +799,7 @@ def create_generator(ag, start, step=1): 'iota.commands.extended.get_latest_inclusion.GetLatestInclusionCommand._execute', mock_get_latest_inclusion, ): - response = self.command( + response = await self.command( seed = Seed.random(), inclusionStates = True, diff --git a/test/commands/extended/helpers_test.py b/test/commands/extended/helpers_test.py deleted file mode 100644 index 65059ab6..00000000 --- a/test/commands/extended/helpers_test.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, \ - unicode_literals - -from unittest import TestCase - -from iota import Iota, TransactionHash -from iota.adapter import MockAdapter - - -class HelpersTestCase(TestCase): - def setUp(self): - super(HelpersTestCase, self).setUp() - - self.api = api = Iota('mock://') - self.api.adapter = MockAdapter() - - # noinspection SpellCheckingInspection - self.transaction = ( - 'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR' - 'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ' - ) - - def test_positive_is_promotable(self): - """ - Transaction is promotable - """ - - self.api.adapter.seed_response('checkConsistency', { - 'state': True, - }) - - self.assertTrue(self.api.helpers.is_promotable(tail=self.transaction)) - - def test_negative_is_promotable(self): - """ - Transaction is not promotable - """ - - self.api.adapter.seed_response('checkConsistency', { - 'state': False, - 'info': 'Inconsistent state', - }) - - self.assertFalse(self.api.helpers.is_promotable(tail=self.transaction)) diff --git a/test/commands/extended/is_promotable_test.py b/test/commands/extended/is_promotable_test.py index cc1506a3..f0c70953 100644 --- a/test/commands/extended/is_promotable_test.py +++ b/test/commands/extended/is_promotable_test.py @@ -8,13 +8,13 @@ from filters.test import BaseFilterTestCase from iota import Iota, TransactionHash, TryteString, TransactionTrytes, \ - Transaction -from iota.adapter import MockAdapter + Transaction, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.is_promotable import IsPromotableCommand, \ get_current_ms, is_within_depth, MILESTONE_INTERVAL, ONE_WAY_DELAY from iota.filters import Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class IsPromotableRequestFilterTestCase(BaseFilterTestCase): filter_type = IsPromotableCommand(MockAdapter()).get_request_filter @@ -290,12 +290,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.is_promotable.IsPromotableCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -310,7 +310,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.is_promotable.IsPromotableCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.is_promotable('tails') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Successfully checking promotability. """ @@ -324,7 +348,7 @@ def test_happy_path(self): with mock.patch('iota.commands.extended.is_promotable.get_current_ms', mock.MagicMock(return_value=self.valid_now)): - response = self.command(tails=[self.hash1, self.hash2]) + response = await self.command(tails=[self.hash1, self.hash2]) self.assertDictEqual( response, @@ -334,7 +358,8 @@ def test_happy_path(self): } ) - def test_not_consistent(self): + @async_test + async def test_not_consistent(self): """ One of the tails is not consistent. """ @@ -347,7 +372,7 @@ def test_not_consistent(self): # No need for mokcing `getTrytes` becasue we should not # reach that part - response = self.command(tails=[self.hash1, self.hash2]) + response = await self.command(tails=[self.hash1, self.hash2]) self.assertDictEqual( response, @@ -358,7 +383,8 @@ def test_not_consistent(self): } ) - def test_one_timestamp_invalid(self): + @async_test + async def test_one_timestamp_invalid(self): """ Test invalid timestamp in one of the transactions. """ @@ -378,7 +404,7 @@ def test_one_timestamp_invalid(self): # Here we don`t mock get_current_ms. # Tx 1 will have updated, passing timestamp. # Tx 2 has the old one, so should fail. - response = self.command(tails=[self.hash1, self.hash2]) + response = await self.command(tails=[self.hash1, self.hash2]) self.assertDictEqual( response, diff --git a/test/commands/extended/is_reattachable_test.py b/test/commands/extended/is_reattachable_test.py index 2800b069..641f83f3 100644 --- a/test/commands/extended/is_reattachable_test.py +++ b/test/commands/extended/is_reattachable_test.py @@ -8,10 +8,10 @@ from filters.test import BaseFilterTestCase from six import text_type -from iota import Address, Iota -from iota.adapter import MockAdapter +from iota import Address, Iota, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.is_reattachable import IsReattachableCommand -from test import patch, MagicMock +from test import patch, MagicMock, async_test class IsReattachableRequestFilterTestCase(BaseFilterTestCase): @@ -199,12 +199,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.is_reattachable.IsReattachableCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -218,3 +218,26 @@ def test_wireup(self): response, 'You found me!' ) + + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.is_reattachable.IsReattachableCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.is_reattachable('addresses') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) \ No newline at end of file diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index 0a6c46b6..e25c73c8 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -9,14 +9,14 @@ from six import binary_type, iterkeys from iota import Address, BadApiResponse, Iota, ProposedTransaction, Tag, \ - TryteString, Transaction, TransactionHash -from iota.adapter import MockAdapter + TryteString, Transaction, TransactionHash, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.prepare_transfer import PrepareTransferCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import GeneratedAddress, Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class PrepareTransferRequestFilterTestCase(BaseFilterTestCase): @@ -576,12 +576,12 @@ def get_current_timestamp(): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.prepare_transfer.PrepareTransferCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -596,12 +596,36 @@ def test_wireup(self): 'You found me!' ) - def test_pass_inputs_not_needed(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.prepare_transfer.PrepareTransferCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.prepare_transfer('transfers') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_pass_inputs_not_needed(self): """ Preparing a bundle that does not transfer any IOTAs. """ response =\ - self.command( + await self.command( seed = Seed( 'TESTVALUE9DONTUSEINPRODUCTION99999HORPYY' @@ -647,7 +671,8 @@ def test_pass_inputs_not_needed(self): ) - def test_pass_inputs_explicit_no_change(self): + @async_test + async def test_pass_inputs_explicit_no_change(self): """ Preparing a bundle with specified inputs, no change address needed. """ @@ -660,7 +685,7 @@ def test_pass_inputs_explicit_no_change(self): 'FIVFBBYQHFYZYIEEWZL9VPMMKIIYTEZRRHXJXKIKF', }) - response = self.command( + response = await self.command( seed = Seed( 'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' @@ -741,7 +766,8 @@ def test_pass_inputs_explicit_no_change(self): TryteString('999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999OJ9999999999999999999999999NYBKIVD99999999999D99999999WNQNUFDDEVEKCLVLUJCFRRWBHSHXQQKSCWACHBLWXPEBWWEBJWJXQQBFJ9HSSDATPLVLL9SLSRFAVRE9Z999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'), ) - def test_pass_inputs_explicit_with_change(self): + @async_test + async def test_pass_inputs_explicit_with_change(self): """ Preparing a bundle with specified inputs, change address needed. """ @@ -754,7 +780,7 @@ def test_pass_inputs_explicit_with_change(self): 'FIVFBBYQHFYZYIEEWZL9VPMMKIIYTEZRRHXJXKIKF', }) - response = self.command( + response = await self.command( seed = Seed( 'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' @@ -820,7 +846,8 @@ def test_pass_inputs_explicit_with_change(self): TryteString('999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999UA9999999999999999999999999NYBKIVD99999999999C99999999GZTLUWOGA9QLYBHUHB9GVUABQHPIJRWUIUOXFIBGYEJWUA9QUZVAKCFLDVUUZEFIDZIUOWUHSFOQIWJFD999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'), ) - def test_fail_inputs_explicit_insufficient(self): + @async_test + async def test_fail_inputs_explicit_insufficient(self): """ Specified inputs are not sufficient to cover spend amount. """ @@ -834,7 +861,7 @@ def test_fail_inputs_explicit_insufficient(self): }) with self.assertRaises(BadApiResponse): - self.command( + await self.command( seed = Seed( b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' @@ -862,7 +889,8 @@ def test_fail_inputs_explicit_insufficient(self): ], ) - def test_pass_inputs_implicit_no_change(self): + @async_test + async def test_pass_inputs_implicit_no_change(self): """ Preparing a bundle that finds inputs to use automatically, no change address needed. @@ -875,7 +903,7 @@ def test_pass_inputs_implicit_no_change(self): # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` mock_get_inputs =\ mock.Mock( - return_value = { + return_value = async_return({ 'inputs': [ Address( trytes = @@ -899,7 +927,7 @@ def test_pass_inputs_implicit_no_change(self): ], 'totalBalance': 42, - }, + }), ) with mock.patch( @@ -907,7 +935,7 @@ def test_pass_inputs_implicit_no_change(self): mock_get_inputs, ): response =\ - self.command( + await self.command( seed = Seed( 'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' @@ -962,7 +990,8 @@ def test_pass_inputs_implicit_no_change(self): TryteString('999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999OJ9999999999999999999999999NYBKIVD99999999999D99999999WNQNUFDDEVEKCLVLUJCFRRWBHSHXQQKSCWACHBLWXPEBWWEBJWJXQQBFJ9HSSDATPLVLL9SLSRFAVRE9Z999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'), ) - def test_pass_inputs_implicit_with_change(self): + @async_test + async def test_pass_inputs_implicit_with_change(self): """ Preparing a bundle that finds inputs to use automatically, change address needed. @@ -975,7 +1004,7 @@ def test_pass_inputs_implicit_with_change(self): # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` mock_get_inputs =\ mock.Mock( - return_value = { + return_value = async_return({ 'inputs': [ Address( trytes = @@ -989,14 +1018,14 @@ def test_pass_inputs_implicit_with_change(self): ], 'totalBalance': 86, - }, + }), ) with mock.patch( 'iota.commands.extended.get_inputs.GetInputsCommand._execute', mock_get_inputs, ): - response = self.command( + response = await self.command( seed = Seed( 'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' @@ -1051,7 +1080,8 @@ def test_pass_inputs_implicit_with_change(self): TryteString('999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999BI9999999999999999999999999NYBKIVD99999999999C99999999FWVD9JAZQGWBOFXANTLCCUHZTKWDDTBRICCOXGWGDDZSXJXKYAJJSCRWSVWVLXKNGOBUJLASABZRJXKVX999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'), ) - def test_fail_inputs_implicit_insufficient(self): + @async_test + async def test_fail_inputs_implicit_insufficient(self): """ Account's total balance is not enough to cover spend amount. """ @@ -1068,7 +1098,7 @@ def test_fail_inputs_implicit_insufficient(self): mock_get_inputs, ): with self.assertRaises(BadApiResponse): - self.command( + await self.command( seed = Seed( b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' @@ -1085,7 +1115,8 @@ def test_fail_inputs_implicit_insufficient(self): ], ) - def test_pass_change_address_auto_generated(self): + @async_test + async def test_pass_change_address_auto_generated(self): """ Preparing a bundle with an auto-generated change address. """ @@ -1097,7 +1128,7 @@ def test_pass_change_address_auto_generated(self): # - :py:class:`iota.commands.extended.get_new_addresses.GetNewAddressesCommand` mock_get_new_addresses_command =\ mock.Mock( - return_value = { + return_value = async_return({ 'addresses': [ Address( trytes = @@ -1108,7 +1139,7 @@ def test_pass_change_address_auto_generated(self): security_level = 2, ), ], - }, + }), ) self.adapter.seed_response('getBalances', { @@ -1125,7 +1156,7 @@ def test_pass_change_address_auto_generated(self): mock_get_new_addresses_command, ): response = \ - self.command( + await self.command( seed = Seed( b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' @@ -1185,11 +1216,12 @@ def test_pass_change_address_auto_generated(self): TryteString(b'999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999999999999999999999999999999NYBKIVD99999999999C99999999IXYSIGLOJQGEKAIDXIRITVQTDKZ9RYRXVHUJJOCTDJEEPQVLLLPWCZOQBOVZEFFFGZVI9AXQTLIZIRZGB999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999'), ) - def test_pass_message_short(self): + @async_test + async def test_pass_message_short(self): """ Adding a message to a transaction. """ - response = self.command( + response = await self.command( seed = Seed( 'TESTVALUE9DONTUSEINPRODUCTION99999HORPYY' @@ -1219,12 +1251,13 @@ def test_pass_message_short(self): TryteString('HHVFHFHHVFEFHHVFOFHHVFHFHHVFMEHHVFSFHHVFCEHHVFPFHHVFEFHHWFVDHHVFCFHHVFUDFA9999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999TESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVAOWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999YAOTA9UNIT9TESTS99999999999NYBKIVD999999999999999999999DRTUWFTZIDCUOAWLSDGIAWAIOGDYWFZNJHCEXDGAHCPKOIUIICUWGPXTGCRJWZBV9AXBCBRAKLDPRKQW999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999PYOTA9UNIT9TESTS99999999999999999999999999999999999999999999999999999999999999999'), ) - def test_pass_message_long(self): + @async_test + async def test_pass_message_long(self): """ The message is too long to fit into a single transaction. """ response =\ - self.command( + await self.command( seed = Seed( 'TESTVALUE9DONTUSEINPRODUCTION99999HORPYY' @@ -1289,7 +1322,8 @@ def test_pass_message_long(self): TryteString('SGKETGDEEASG9GSGSFEASGZFSGAGSGTFSGSFTGVDSGSFEATGUDSGBGTGTDSGNFSGPFSGVFTGVDTGEETGUDTGHEEASGBGTGTDSGNFSGPFSGRFTGWDFAEASGZETGDESG9GQAEASGZFTGDEEASGTFSGVFSGPFSGSFSGZFEASGPFEASGZFSGVFTGTDSGSFQAEASGXFSGAGTGVDSGAGTGTDTGDESGWFEASGVFSGZFSGSFSGSFTGVDEATGUDTGVDSGSFSG9GTGDESAEASGQEEATGFETGVDSGVFEATGUDTGVDSGSFSG9GTGDEEASGRFSGAGSGYFSGTFSG9GTGDEEASGOFTGDETGVDTGEEEASGAGTGYDTGTDSGNFSG9GTGHETGGETGVDEASGYFTGGESGRFSGVFEATGUDEASGAGTGTDTGWDSGTFSGVFSGSFSGZFSAEASGSETGVDSGAGEASGOFTGWDSGRFSGSFTGVDEATGFETGVDSGAGEASGRFSGSFSGYFSGNFTGVDTGEEIBEASGKETGDEIBEASGKETGDEQAEASGYFSGSFSGWFTGVDSGSFSG9GSGNFSG9GTGVDEAFCTCXCBDQCTCFDVCIBEASGAFEASGZFSGSFSG9GTGHEEASGSFTGUDTGVDTGEEEASGOFSGAGSGYFTGEETGAESGNFTGHEEASGAGTGVDSGPFSGSFTGVDTGUDTGVDSGPFSGSFSG9GSG9GSGAGTGUDTGVDTGEEQAEATG9ESGSFSGZFEASGPFTGDEEASGZFSGAGSGTFSGSFTGVDSGSFEASGBGSGAGSG9GTGHETGVDTGEESAEASG9FTGDEEASGBGSGYFSGNFTG9ESGSFTGAETGEEEASGZESGNFSG9GTGVDTGEETGHESGQFSGAGEASGVFEASGBGTGTDSGAGSGXFSGYFTGHESG9GSGVFEASGZFSGAGTGTDTGUDSGXFSGVFTGYDEASGBGSGSFTGYDSGAGTGVDSGVFSG9GTGZDSGSFSGPFSAEASGAFEASGPFSGNFTGUDEASGSFTGUDTGVDTGEEEATGVDSGNFSGXFSGAGSGWFEATGTDSGAGTGUDSGXFSGAGTGAESGVFSAEASGAFEASGPFSGNFTGUDEASGSFTGUDTGVDTGEEEATGTDSGAGTGUDSGXFSGAGTGAETGEEQAEASG9GSGSFEASGUFSG9GSGNFTGHEQAEATG9ETGVDSGAGEATGHEEASGUFSG9GSGNFTGGEDBEATG9ETGVDSGAGEATGUDSGZFSGSFTGTDTGVDTGEEEASGZESGNFSG9GTGVDTGEETGHESGQFSGAGQAEASGPFEATGVDSGAGEASGPFTGTDSGSFSGZFTGHEEASGXFSGNFSGXFEATGVDTGTDSGNFSGQFSGVFTG9ESGSFTGUDSGXFSGVFSGWFQAEASGPFSGSFTGTDSGAGTGHETGVDSG9GSGAGQAEATGUDSGBGSGNFTGUDEASGTFSGVFSGUFSG9GTGEESAEASGQEEASGZFSGAGSGSFEATGUDTGWDTGBESGSFTGUDTGVDSGPFSGAGSGPFSGNFSG9GSGVFSGSFQAEASGPFEATGVDSGAGEASGPFTGTDSGSFSGZFTGHEEASGXFSGNFSGXFEASGQFTGTDSGAGTGVDSGSFTGUDSGXFEASGVFEASG9GSGSFSGBGSGAGSG9GTGHETGVDSG9GTGDESGZFSGVFEASGRFSGYFTGHEEASGPFSGNFTGUDQAEATGUDSGBGSGNFTGUDSGNFSGSFTGVDEASGTFSGVFSGUFSG9GSGVFEASASASAEASGKETGDEEASG9GSGSFEATGYDSGAGTGVDSGVFTGVDSGSFEASGUFSG9GSGNFTGVDTGEEEASGBGTGTDSGNFSGPFSGRFTGWDSAEASGXESGAGTGVDSGAGSGZFTGWDEATG9ETGVDSGAGEASGPFEASGQFSGYFTGWDSGOFSGVFSG9GSGSFEASGRFTGWDTGAESGVFQAEASGPFEATGVDSGSFTGYDEASGZFSGSFTGUDTGVDSGNFTGYDQAEASGPFTGDEEASG9GSGSFEASGQFSGAGSGPFSGAGTGTDSGVFTGVDSGSFEASGAGEASG9GSGNFEASGPFSGSFTG9ESGSFTGTDSGVFSG9GSGXFSGNFTGYDQAEASGPFTGDEEATGYDSGAGTGVDSGVFTGVDSGSFEASGZFSGSTESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVAOWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999GFOTA9UNIT9TESTS99999999999NYBKIVD99999999999B99999999EKHBGESJFZXE9PY9UVFIPRHGGFKDFKQOQFKQAYISJOWCXIVBSGHOZGT9DZEQPPLTYHKTWBQZOFX9BEAID999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999PYOTA9UNIT9TESTS99999999999999999999999999999999999999999999999999999999999999999'), ) - def test_security_level(self): + @async_test + async def test_security_level(self): """ testing use of security_level when inputs are given and change address is not given. """ @@ -1316,7 +1350,7 @@ def mock_get_balances_execute(adapter, request): # returns balances of input addresses equal to SEND_VALUE + security_level * 11 addr = request["addresses"][0] security_level = [l for l, a in mock_addresses.items() if str(a) == addr][0] - return dict(balances=[SEND_VALUE + security_level * 11], milestone=None) + return async_return(dict(balances=[SEND_VALUE + security_level * 11], milestone=None)) # testing for several security levels for security_level in SECURITY_LEVELS_TO_TEST: @@ -1337,7 +1371,7 @@ def mock_get_balances_execute(adapter, request): mock_get_balances_execute, ): response = \ - self.command( + await self.command( seed=seed, transfers=[ ProposedTransaction( @@ -1366,7 +1400,8 @@ def mock_get_balances_execute(adapter, request): self.assertEqual(change_tx.value, EXPECTED_CHANGE_VALUE) - def test_security_level_no_inputs(self): + @async_test + async def test_security_level_no_inputs(self): """ testing use of security_level when neither inputs nor change address is given. """ @@ -1390,7 +1425,7 @@ def mock_get_balances_execute(adapter, request): # returns balances of input addresses equal to SEND_VALUE + security_level * 11 addr = request["addresses"][0] security_level = [l for l, a in addresses.items() if str(a) == addr][0] - return dict(balances=[SEND_VALUE + security_level * 11], milestone=None) + return async_return(dict(balances=[SEND_VALUE + security_level * 11], milestone=None)) # testing several security levels for security_level in SECURITY_LEVELS_TO_TEST: @@ -1429,7 +1464,7 @@ def mock_get_balances_execute(adapter, request): mock_get_balances_execute, ): response = \ - self.command( + await self.command( seed=seed, transfers=[ ProposedTransaction( diff --git a/test/commands/extended/promote_transaction_test.py b/test/commands/extended/promote_transaction_test.py index e732ae4f..b8e9c4e2 100644 --- a/test/commands/extended/promote_transaction_test.py +++ b/test/commands/extended/promote_transaction_test.py @@ -8,12 +8,13 @@ from filters.test import BaseFilterTestCase from six import binary_type -from iota import Bundle, Iota, TransactionHash, TransactionTrytes, BadApiResponse -from iota.adapter import MockAdapter +from iota import Bundle, Iota, TransactionHash, TransactionTrytes, \ + BadApiResponse, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.promote_transaction import PromoteTransactionCommand from iota.filters import Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class PromoteTransactionRequestFilterTestCase(BaseFilterTestCase): @@ -312,12 +313,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.promote_transaction.PromoteTransactionCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -332,7 +333,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.promote_transaction.PromoteTransactionCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.promote_transaction('transaction') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Successfully promoting a bundle. """ @@ -345,16 +370,16 @@ def test_happy_path(self): TransactionTrytes(self.trytes1), TransactionTrytes(self.trytes2), ]) - mock_send_transfer = mock.Mock(return_value={ + mock_send_transfer = mock.Mock(return_value=async_return({ 'bundle': result_bundle, - }) + })) with mock.patch( 'iota.commands.extended.send_transfer.SendTransferCommand._execute', mock_send_transfer, ): - response = self.command( + response = await self.command( transaction=self.hash1, depth=3, minWeightMagnitude=16, @@ -368,7 +393,8 @@ def test_happy_path(self): } ) - def test_not_promotable(self): + @async_test + async def test_not_promotable(self): """ Bundle isn't promotable. """ @@ -378,7 +404,7 @@ def test_not_promotable(self): }) with self.assertRaises(BadApiResponse): - response = self.command( + response = await self.command( transaction=self.hash1, depth=3, minWeightMagnitude=16, diff --git a/test/commands/extended/replay_bundle_test.py b/test/commands/extended/replay_bundle_test.py index 858e038b..07f5765e 100644 --- a/test/commands/extended/replay_bundle_test.py +++ b/test/commands/extended/replay_bundle_test.py @@ -9,12 +9,12 @@ from six import binary_type from iota import Address, Bundle, BundleHash, Fragment, Iota, Nonce, Tag, \ - Transaction, TransactionHash -from iota.adapter import MockAdapter + Transaction, TransactionHash, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.replay_bundle import ReplayBundleCommand from iota.filters import Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class ReplayBundleRequestFilterTestCase(BaseFilterTestCase): @@ -304,12 +304,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.replay_bundle.ReplayBundleCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -324,7 +324,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.replay_bundle.ReplayBundleCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.replay_bundle('transaction') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Successfully replaying a bundle. """ @@ -602,9 +626,9 @@ def test_happy_path(self): ]) mock_get_bundles =\ - mock.Mock(return_value={ + mock.Mock(return_value=async_return({ 'bundles': [bundle], - }) + })) send_trytes_response = { 'trytes': bundle.as_tryte_strings(), @@ -619,7 +643,7 @@ def mock_send_trytes(_,request): - https://github.com/iotaledger/iota.py/issues/74 """ self.assertEqual(request['trytes'], send_trytes_response['trytes']) - return send_trytes_response + return async_return(send_trytes_response) with mock.patch( 'iota.commands.extended.get_bundles.GetBundlesCommand._execute', @@ -629,7 +653,7 @@ def mock_send_trytes(_,request): 'iota.commands.extended.send_trytes.SendTrytesCommand._execute', mock_send_trytes, ): - response = self.command( + response = await self.command( depth = 100, minWeightMagnitude = 18, transaction = bundle[0].hash, diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index 8a277c0e..c9ab2fde 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -9,14 +9,14 @@ from six import binary_type from iota import Address, Bundle, Iota, ProposedTransaction, TransactionHash, \ - TransactionTrytes, TryteString -from iota.adapter import MockAdapter + TransactionTrytes, TryteString, AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.send_transfer import SendTransferCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes from test import mock -from test import patch, MagicMock +from test import patch, MagicMock, async_test class SendTransferRequestFilterTestCase(BaseFilterTestCase): @@ -672,12 +672,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.send_transfer.SendTransferCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -692,7 +692,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.send_transfer.SendTransferCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.send_transfer('transfers') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Sending a transfer successfully. """ @@ -743,14 +767,14 @@ def test_happy_path(self): ) mock_prepare_transfer =\ - mock.Mock(return_value={ + mock.Mock(return_value=async_return({ 'trytes': [transaction1], - }) + })) mock_send_trytes =\ - mock.Mock(return_value={ + mock.Mock(return_value=async_return({ 'trytes': [transaction1], - }) + })) with mock.patch( 'iota.commands.extended.prepare_transfer.PrepareTransferCommand._execute', @@ -760,7 +784,7 @@ def test_happy_path(self): 'iota.commands.extended.send_trytes.SendTrytesCommand._execute', mock_send_trytes, ): - response = self.command( + response = await self.command( depth = 100, minWeightMagnitude = 18, seed = Seed.random(), diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index 70301806..12989eed 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -8,11 +8,12 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type -from iota import Iota, TransactionTrytes, TryteString, TransactionHash -from iota.adapter import MockAdapter +from iota import Iota, TransactionTrytes, TryteString, TransactionHash, \ + AsyncIota +from iota.adapter import MockAdapter, async_return from iota.commands.extended.send_trytes import SendTrytesCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test class SendTrytesRequestFilterTestCase(BaseFilterTestCase): @@ -376,7 +377,7 @@ def test_wireup(self): The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.send_trytes.SendTrytesCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -391,7 +392,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.send_trytes.SendTrytesCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.send_trytes('trytes') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Successful invocation of ``sendTrytes``. """ @@ -415,7 +440,7 @@ def test_happy_path(self): TransactionTrytes(self.trytes2), ] - response = self.command( + response = await self.command( trytes = trytes, depth = 100, minWeightMagnitude = 18, diff --git a/test/commands/extended/traverse_bundle_test.py b/test/commands/extended/traverse_bundle_test.py index b869b367..383b225e 100644 --- a/test/commands/extended/traverse_bundle_test.py +++ b/test/commands/extended/traverse_bundle_test.py @@ -8,11 +8,11 @@ from filters.test import BaseFilterTestCase from iota import Address, BadApiResponse, Bundle, BundleHash, Fragment, Hash, \ - Iota, Tag, Transaction, TransactionHash, TransactionTrytes, Nonce -from iota.adapter import MockAdapter + Iota, AsyncIota, Tag, Transaction, TransactionHash, TransactionTrytes, Nonce +from iota.adapter import MockAdapter, async_return from iota.commands.extended.traverse_bundle import TraverseBundleCommand from iota.filters import Trytes -from test import patch, MagicMock +from test import patch, MagicMock, async_test # Same tests as for GetBundlesRequestFilter (it is the same filter) class TraverseBundleRequestFilterTestCase(BaseFilterTestCase): @@ -129,12 +129,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.commands.extended.traverse_bundle.TraverseBundleCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = Iota(self.adapter) @@ -149,7 +149,31 @@ def test_wireup(self): 'You found me!' ) - def test_single_transaction(self): + @async_test + async def test_wireup(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.commands.extended.traverse_bundle.TraverseBundleCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.traverse_bundle('tail') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_single_transaction(self): """ Getting a bundle that contains a single transaction. """ @@ -204,7 +228,7 @@ def test_single_transaction(self): 'trytes': [transaction.as_tryte_string()], }) - response = self.command(transaction=transaction.hash) + response = await self.command(transaction=transaction.hash) bundle = response['bundles'][0] # type: Bundle self.assertEqual(len(bundle), 1) @@ -215,7 +239,8 @@ def test_single_transaction(self): transaction.as_json_compatible(), ) - def test_multiple_transactions(self): + @async_test + async def test_multiple_transactions(self): """ Getting a bundle that contains multiple transactions. """ @@ -363,7 +388,7 @@ def test_multiple_transactions(self): ], }) - response = self.command( + response = await self.command( transaction = TransactionHash( b'TOYJPHKMLQNDVLDHDILARUJCCIUMQBLUSWPCTIVA' @@ -376,7 +401,8 @@ def test_multiple_transactions(self): bundle.as_json_compatible(), ) - def test_non_tail_transaction(self): + @async_test + async def test_non_tail_transaction(self): """ Trying to get a bundle for a non-tail transaction. @@ -429,7 +455,7 @@ def test_non_tail_transaction(self): }) with self.assertRaises(BadApiResponse): - self.command( + await self.command( transaction = TransactionHash( b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' @@ -437,14 +463,15 @@ def test_non_tail_transaction(self): ), ) - def test_missing_transaction(self): + @async_test + async def test_missing_transaction(self): """ Unable to find the requested transaction. """ self.adapter.seed_response('getTrytes', {'trytes': []}) with self.assertRaises(BadApiResponse): - self.command( + await self.command( transaction = TransactionHash( b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' @@ -452,7 +479,8 @@ def test_missing_transaction(self): ), ) - def test_missing_transaction_zero_trytes(self): + @async_test + async def test_missing_transaction_zero_trytes(self): """ Unable to find the requested transaction. getTrytes returned only zeros, no tx was found. @@ -461,7 +489,7 @@ def test_missing_transaction_zero_trytes(self): self.adapter.seed_response('getTrytes', {'trytes': [zero_trytes]}) with self.assertRaises(BadApiResponse): - self.command( + await self.command( transaction = TransactionHash( b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' diff --git a/test/commands/extended/utils_test.py b/test/commands/extended/utils_test.py index 4ba6bfe2..480c924f 100644 --- a/test/commands/extended/utils_test.py +++ b/test/commands/extended/utils_test.py @@ -6,7 +6,7 @@ from iota.commands.extended.utils import iter_used_addresses from iota import MockAdapter from iota.crypto.types import Seed -from test import mock +from test import mock, async_test class IterUsedAddressesTestCase(TestCase): @@ -35,11 +35,13 @@ def seed_unused_address(self): 'states': [False], }) - def get_all_used_addresses(self, start=0): - return [address for address, _ + async def get_all_used_addresses(self, start=0): + # `iter_used_addresses` is an async generator, so we have to use `async for` + return [address async for address, _ in iter_used_addresses(self.adapter, self.seed, start)] - def test_first_address_is_not_used(self): + @async_test + async def test_first_address_is_not_used(self): """ The very first address is not used. No address is returned. """ @@ -50,7 +52,7 @@ def test_first_address_is_not_used(self): 'iota.crypto.addresses.AddressGenerator.create_iterator', self.mock_address_generator, ): - self.assertEqual([], self.get_all_used_addresses()) + self.assertEqual([], await self.get_all_used_addresses()) self.assertListEqual( self.adapter.requests, @@ -66,7 +68,8 @@ def test_first_address_is_not_used(self): ] ) - def test_transactions_are_considered_used(self): + @async_test + async def test_transactions_are_considered_used(self): """ An address with a transaction is considered used. """ @@ -82,7 +85,7 @@ def test_transactions_are_considered_used(self): 'iota.crypto.addresses.AddressGenerator.create_iterator', self.mock_address_generator, ): - self.assertEqual([self.address0], self.get_all_used_addresses()) + self.assertEqual([self.address0], await self.get_all_used_addresses()) self.assertListEqual( self.adapter.requests, @@ -102,7 +105,8 @@ def test_transactions_are_considered_used(self): ] ) - def test_spent_from_is_considered_used(self): + @async_test + async def test_spent_from_is_considered_used(self): """ An address that was spent from is considered used. """ @@ -121,7 +125,7 @@ def test_spent_from_is_considered_used(self): 'iota.crypto.addresses.AddressGenerator.create_iterator', self.mock_address_generator, ): - self.assertEqual([self.address0], self.get_all_used_addresses()) + self.assertEqual([self.address0], await self.get_all_used_addresses()) self.assertListEqual( self.adapter.requests, @@ -145,7 +149,8 @@ def test_spent_from_is_considered_used(self): ] ) - def test_start_parameter_is_given(self): + @async_test + async def test_start_parameter_is_given(self): """ The correct address is returned if a start parameter is given. """ @@ -162,7 +167,7 @@ def test_start_parameter_is_given(self): self.mock_address_generator, ): self.assertEqual([self.address1], - self.get_all_used_addresses(start=1)) + await self.get_all_used_addresses(start=1)) self.assertListEqual( self.adapter.requests, @@ -182,7 +187,8 @@ def test_start_parameter_is_given(self): ] ) - def test_multiple_addresses_return(self): + @async_test + async def test_multiple_addresses_return(self): """ A larger test that combines multiple cases and more than one address should be returned. @@ -212,7 +218,7 @@ def test_multiple_addresses_return(self): self.mock_address_generator, ): self.assertEqual([self.address0, self.address1], - self.get_all_used_addresses()) + await self.get_all_used_addresses()) self.assertListEqual( self.adapter.requests, @@ -239,3 +245,5 @@ def test_multiple_addresses_return(self): }, ] ) + +# TODO: add tests for `get_bundles_from_transaction_hashes` \ No newline at end of file diff --git a/test/multisig/commands/create_multisig_address_test.py b/test/multisig/commands/create_multisig_address_test.py index be7f3adc..954dce64 100644 --- a/test/multisig/commands/create_multisig_address_test.py +++ b/test/multisig/commands/create_multisig_address_test.py @@ -9,13 +9,13 @@ from six import binary_type from iota import TryteString -from iota.adapter import MockAdapter +from iota.adapter import MockAdapter, async_return from iota.crypto.types import Digest from iota.filters import Trytes -from iota.multisig import MultisigIota +from iota.multisig import MultisigIota, AsyncMultisigIota from iota.multisig.commands import CreateMultisigAddressCommand from iota.multisig.types import MultisigAddress -from test import patch, MagicMock +from test import patch, MagicMock, async_test class CreateMultisigAddressCommandTestCase(TestCase): @@ -49,12 +49,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.multisig.commands.create_multisig_address.CreateMultisigAddressCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = MultisigIota(self.adapter) @@ -69,11 +69,35 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.multisig.commands.create_multisig_address.CreateMultisigAddressCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncMultisigIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.create_multisig_address('digests') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Generating a multisig address. """ - result = self.command(digests=[self.digest_1, self.digest_2]) + result = await self.command(digests=[self.digest_1, self.digest_2]) # noinspection SpellCheckingInspection self.assertDictEqual( diff --git a/test/multisig/commands/get_digests_test.py b/test/multisig/commands/get_digests_test.py index daa88c20..f201ef7c 100644 --- a/test/multisig/commands/get_digests_test.py +++ b/test/multisig/commands/get_digests_test.py @@ -9,14 +9,14 @@ from six import binary_type from iota import Hash, TryteString -from iota.adapter import MockAdapter +from iota.adapter import MockAdapter, async_return from iota.crypto import FRAGMENT_LENGTH from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Digest, PrivateKey, Seed from iota.filters import Trytes -from iota.multisig import MultisigIota +from iota.multisig import MultisigIota, AsyncMultisigIota from iota.multisig.commands import GetDigestsCommand -from test import mock, patch, MagicMock +from test import mock, patch, MagicMock, async_test class GetDigestsCommandTestCase(TestCase): @@ -36,12 +36,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.multisig.commands.get_digests.GetDigestsCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = MultisigIota(self.adapter) @@ -56,13 +56,37 @@ def test_wireup(self): 'You found me!' ) - def test_generate_single_digest(self): + @async_test + async def test_wireup(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.multisig.commands.get_digests.GetDigestsCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncMultisigIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_digests() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_generate_single_digest(self): """ Generating a single digest. """ seed = Seed.random() - mock_get_private_keys = mock.Mock(return_value={'keys': [self.key1]}) + mock_get_private_keys = mock.Mock(return_value=async_return({'keys': [self.key1]})) with mock.patch( 'iota.multisig.commands.get_private_keys.GetPrivateKeysCommand._execute', @@ -72,7 +96,7 @@ def test_generate_single_digest(self): with mock.patch.object(self.key1, 'get_digest') as mock_get_digest_1: # type: mock.MagicMock mock_get_digest_1.return_value = self.digest1 - result = self.command(seed=seed, index=0, count=1, securityLevel=1) + result = await self.command(seed=seed, index=0, count=1, securityLevel=1) self.assertDictEqual(result, {'digests': [self.digest1]}) @@ -83,14 +107,15 @@ def test_generate_single_digest(self): 'seed': seed, }) - def test_generate_multiple_digests(self): + @async_test + async def test_generate_multiple_digests(self): """ Generating multiple digests. """ seed = Seed.random() mock_get_private_keys =\ - mock.Mock(return_value={'keys': [self.key1, self.key2]}) + mock.Mock(return_value=async_return({'keys': [self.key1, self.key2]})) with mock.patch( 'iota.multisig.commands.get_private_keys.GetPrivateKeysCommand._execute', @@ -104,7 +129,7 @@ def test_generate_multiple_digests(self): with mock.patch.object(self.key2, 'get_digest') as mock_get_digest_2: # type: mock.MagicMock mock_get_digest_2.return_value = self.digest2 - result = self.command(seed=seed, index=0, count=2, securityLevel=1) + result = await self.command(seed=seed, index=0, count=2, securityLevel=1) self.assertDictEqual(result, {'digests': [self.digest1, self.digest2]}) diff --git a/test/multisig/commands/get_private_keys_test.py b/test/multisig/commands/get_private_keys_test.py index a6dc7d54..8c541e2f 100644 --- a/test/multisig/commands/get_private_keys_test.py +++ b/test/multisig/commands/get_private_keys_test.py @@ -9,14 +9,14 @@ from six import binary_type from iota import TryteString -from iota.adapter import MockAdapter +from iota.adapter import MockAdapter, async_return from iota.crypto import FRAGMENT_LENGTH from iota.crypto.addresses import AddressGenerator from iota.crypto.types import PrivateKey, Seed from iota.filters import Trytes -from iota.multisig import MultisigIota +from iota.multisig import MultisigIota, AsyncMultisigIota from iota.multisig.commands import GetPrivateKeysCommand -from test import mock, patch, MagicMock +from test import mock, patch, MagicMock, async_test class GetPrivateKeysCommandTestCase(TestCase): @@ -40,12 +40,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.multisig.commands.get_private_keys.GetPrivateKeysCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = MultisigIota(self.adapter) @@ -60,7 +60,31 @@ def test_wireup(self): 'You found me!' ) - def test_generate_single_key(self): + @async_test + async def test_wireup_async(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.multisig.commands.get_private_keys.GetPrivateKeysCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncMultisigIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.get_private_keys() + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_generate_single_key(self): """ Generating a single key. """ @@ -68,7 +92,7 @@ def test_generate_single_key(self): mock_get_keys = mock.Mock(return_value=keys) with mock.patch('iota.crypto.signing.KeyGenerator.get_keys', mock_get_keys): - result = self.command(seed=Seed.random(), securityLevel=2) + result = await self.command(seed=Seed.random(), securityLevel=2) self.assertDictEqual(result, {'keys': keys}) mock_get_keys.assert_called_once_with( @@ -77,7 +101,8 @@ def test_generate_single_key(self): start = 0, ) - def test_generate_multiple_keys(self): + @async_test + async def test_generate_multiple_keys(self): """ Generating multiple keys. """ @@ -86,7 +111,7 @@ def test_generate_multiple_keys(self): mock_get_keys = mock.Mock(return_value=keys) with mock.patch('iota.crypto.signing.KeyGenerator.get_keys', mock_get_keys): result =\ - self.command( + await self.command( count = 2, index = 0, securityLevel = 1, diff --git a/test/multisig/commands/prepare_multisig_transfer_test.py b/test/multisig/commands/prepare_multisig_transfer_test.py index bbf4ac6b..46dd373c 100644 --- a/test/multisig/commands/prepare_multisig_transfer_test.py +++ b/test/multisig/commands/prepare_multisig_transfer_test.py @@ -8,13 +8,13 @@ from filters.test import BaseFilterTestCase from iota import Address, Bundle, Fragment, ProposedTransaction -from iota.adapter import MockAdapter +from iota.adapter import MockAdapter, async_return from iota.commands.core import GetBalancesCommand from iota.crypto.types import Digest -from iota.multisig import MultisigIota +from iota.multisig import MultisigIota, AsyncMultisigIota from iota.multisig.commands import PrepareMultisigTransferCommand from iota.multisig.types import MultisigAddress -from test import patch, MagicMock +from test import patch, MagicMock, async_test class PrepareMultisigTransferRequestFilterTestCase(BaseFilterTestCase): @@ -527,12 +527,12 @@ def setUp(self): def test_wireup(self): """ - Verify that the command is wired up correctly. + Verify that the command is wired up correctly. (sync) The API method indeed calls the appropiate command. """ with patch('iota.multisig.commands.prepare_multisig_transfer.PrepareMultisigTransferCommand.__call__', - MagicMock(return_value='You found me!') + MagicMock(return_value=async_return('You found me!')) ) as mocked_command: api = MultisigIota(self.adapter) @@ -547,7 +547,31 @@ def test_wireup(self): 'You found me!' ) - def test_happy_path(self): + @async_test + async def test_wireup(self): + """ + Verify that the command is wired up correctly. (async) + + The API method indeed calls the appropiate command. + """ + with patch('iota.multisig.commands.prepare_multisig_transfer.PrepareMultisigTransferCommand.__call__', + MagicMock(return_value=async_return('You found me!')) + ) as mocked_command: + + api = AsyncMultisigIota(self.adapter) + + # Don't need to call with proper args here. + response = await api.prepare_multisig_transfer('transfer', 'multisig_input') + + self.assertTrue(mocked_command.called) + + self.assertEqual( + response, + 'You found me!' + ) + + @async_test + async def test_happy_path(self): """ Preparing a bundle with a multisig input. """ @@ -563,7 +587,7 @@ def test_happy_path(self): ) pmt_result =\ - self.command( + await self.command( transfers = [ ProposedTransaction( address = Address(self.trytes_1), @@ -624,7 +648,8 @@ def test_happy_path(self): self.assertEqual(txn_5.value, 0) self.assertEqual(txn_5.signature_message_fragment, Fragment(b'')) - def test_unspent_inputs_with_change_address(self): + @async_test + async def test_unspent_inputs_with_change_address(self): """ The bundle has unspent inputs, so it uses the provided change address. @@ -639,7 +664,7 @@ def test_unspent_inputs_with_change_address(self): ) pmt_result =\ - self.command( + await self.command( transfers = [ ProposedTransaction( address = Address(self.trytes_1), @@ -694,7 +719,8 @@ def test_unspent_inputs_with_change_address(self): self.assertEqual(txn_6.address, self.trytes_3) self.assertEqual(txn_6.value, 59) - def test_error_zero_iotas_transferred(self): + @async_test + async def test_error_zero_iotas_transferred(self): """ The bundle doesn't spend any IOTAs. @@ -705,7 +731,7 @@ def test_error_zero_iotas_transferred(self): using :py:meth:`iota.api.Iota.prepare_transfer` instead. """ with self.assertRaises(ValueError): - self.command( + await self.command( transfers = [ ProposedTransaction( address = Address(self.trytes_1), @@ -720,7 +746,8 @@ def test_error_zero_iotas_transferred(self): ), ) - def test_error_insufficient_inputs(self): + @async_test + async def test_error_insufficient_inputs(self): """ The multisig input does not contain sufficient IOTAs to cover the spends. @@ -735,7 +762,7 @@ def test_error_insufficient_inputs(self): ) with self.assertRaises(ValueError): - self.command( + await self.command( transfers = [ ProposedTransaction( address = Address(self.trytes_1), @@ -750,7 +777,8 @@ def test_error_insufficient_inputs(self): ), ) - def test_error_unspent_inputs_no_change_address(self): + @async_test + async def test_error_unspent_inputs_no_change_address(self): """ The bundle has unspent inputs, but no change address was specified. @@ -773,7 +801,7 @@ def test_error_unspent_inputs_no_change_address(self): ) with self.assertRaises(ValueError): - self.command( + await self.command( transfers = [ ProposedTransaction( address = Address(self.trytes_1), diff --git a/tox.ini b/tox.ini index 3220f263..821cac87 100644 --- a/tox.ini +++ b/tox.ini @@ -4,10 +4,11 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35, py36, py37 +envlist = py36, py37 [testenv] commands = nosetests deps = + aiounittest mock nose