From e2b90a9e2f3964d4898a6816f2c09adc1ad74458 Mon Sep 17 00:00:00 2001 From: Philipp de Col Date: Sun, 15 Sep 2019 17:10:54 +0200 Subject: [PATCH] [#99] Implement find transaction objects command --- README.rst | 4 +- docs/api.rst | 34 +++++ iota/api.py | 44 +++++++ iota/commands/extended/__init__.py | 1 + .../extended/find_transaction_objects.py | 55 ++++++++ iota/commands/extended/is_reattachable.py | 9 +- iota/commands/extended/utils.py | 27 +--- .../extended/find_transaction_objects.py | 118 ++++++++++++++++++ 8 files changed, 261 insertions(+), 31 deletions(-) create mode 100644 iota/commands/extended/find_transaction_objects.py create mode 100644 test/commands/extended/find_transaction_objects.py diff --git a/README.rst b/README.rst index 2cb91ecc..6cc93213 100644 --- a/README.rst +++ b/README.rst @@ -80,14 +80,14 @@ can also build the documentation locally: #. Install extra dependencies (you only have to do this once):: - pip install '.[docs-builder]' + pip install .[docs-builder] .. tip:: To install the CCurl extension and the documentation builder tools together, use the following command:: - pip install '.[ccurl,docs-builder]' + pip install .[ccurl,docs-builder] #. Switch to the ``docs`` directory:: diff --git a/docs/api.rst b/docs/api.rst index 1a9c176b..778db6a9 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -36,6 +36,40 @@ This method returns a ``dict`` with the following items: broadcast/stored. Should be the same as the value of the ``trytes`` parameter. + +``find_transaction_objects`` +---------------------------- + +A more extensive version of the core API ``find_transactions`` that returns +transaction objects instead of hashes. + +Effectively, this is ``find_transactions`` + ``get_trytes`` + converting +the trytes into transaction objects. It accepts the same parameters +as ``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. + +Parameters +~~~~~~~~~~ + +- ``bundles: Optional[Iterable[BundleHash]]``: List of bundle IDs. +- ``addresses: Optional[Iterable[Address]]``: List of addresses. +- ``tags: Optional[Iterable[Tag]]``: List of tags. +- ``param: Optional[Iterable[TransactionHash]]``: List of approvee + transaction IDs. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``transactions: List[Transaction]``: List of Transaction objects that + match the input + ``get_account_data`` -------------------- diff --git a/iota/api.py b/iota/api.py index 761f50fe..7a31609a 100644 --- a/iota/api.py +++ b/iota/api.py @@ -518,6 +518,50 @@ def broadcast_and_store(self, trytes): """ return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes) + 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 ``find_transactions`` + ``get_trytes`` + + converting the trytes into transaction objects. + + It accepts the same parameters as :py:meth:`find_transactions` + + :param bundles: + List of bundle IDs. + + :param addresses: + List of addresses. + + :param tags: + List of tags. + + :param approvees: + List of approvee transaction IDs. + + :return: + Dict with the following structure:: + + { + 'transactions': List[Transaction], + List of Transaction objects that match the input. + } + """ + return extended.FindTransactionObjectsCommand(self.adapter)( + bundles=bundles, + addresses=addresses, + tags=tags, + approvees=approvees, + ) + def get_account_data(self, start=0, stop=None, inclusion_states=False, security_level=None): # type: (int, Optional[int], bool, Optional[int]) -> dict """ diff --git a/iota/commands/extended/__init__.py b/iota/commands/extended/__init__.py index 71d766fd..4a267f2b 100644 --- a/iota/commands/extended/__init__.py +++ b/iota/commands/extended/__init__.py @@ -12,6 +12,7 @@ unicode_literals from .broadcast_and_store import * +from .find_transaction_objects import * from .get_account_data import * from .get_bundles import * from .get_inputs import * diff --git a/iota/commands/extended/find_transaction_objects.py b/iota/commands/extended/find_transaction_objects.py new file mode 100644 index 00000000..e0dd0960 --- /dev/null +++ b/iota/commands/extended/find_transaction_objects.py @@ -0,0 +1,55 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Iterable, List, Optional + +from iota import Address, BundleHash, Tag, Transaction, TransactionHash +from iota.commands.core import GetTrytesCommand, FindTransactionsCommand + +__all__ = [ + 'FindTransactionObjectsCommand', +] + + +class FindTransactionObjectsCommand(FindTransactionsCommand): + """ + Executes `FindTransactionObjects` command. + + See :py:meth:`iota.api.StrictIota.find_transaction_objects`. + """ + command = 'findTransactionObjects' + + def get_response_filter(self): + pass + + def _execute(self, request): + bundles = request\ + .get('bundles') # type: Optional[Iterable[BundleHash]] + addresses = request\ + .get('addresses') # type: Optional[Iterable[Address]] + tags = request\ + .get('tags') # type: Optional[Iterable[Tag]] + approvees = request\ + .get('approvees') # type: Optional[Iterable[TransactionHash]] + + ft_response = FindTransactionsCommand(adapter=self.adapter)( + bundles=bundles, + addresses=addresses, + tags=tags, + approvees=approvees, + ) + + hashes = ft_response['hashes'] + transactions = [] + if hashes: + gt_response = GetTrytesCommand(adapter=self.adapter)(hashes=hashes) + + transactions = list(map( + Transaction.from_tryte_string, + gt_response.get('trytes') or [], + )) # type: List[Transaction] + + return { + 'transactions': transactions, + } diff --git a/iota/commands/extended/is_reattachable.py b/iota/commands/extended/is_reattachable.py index 8238dc63..f136a194 100644 --- a/iota/commands/extended/is_reattachable.py +++ b/iota/commands/extended/is_reattachable.py @@ -8,8 +8,8 @@ from iota import Address from iota.commands import FilterCommand, RequestFilter, ResponseFilter -from iota.commands.extended import GetLatestInclusionCommand -from iota.commands.extended.utils import find_transaction_objects +from iota.commands.extended import FindTransactionObjectsCommand, \ + GetLatestInclusionCommand from iota.filters import Trytes __all__ = [ @@ -33,10 +33,9 @@ def _execute(self, request): addresses = request['addresses'] # type: List[Address] # fetch full transaction objects - transactions = find_transaction_objects( - adapter=self.adapter, + transactions = FindTransactionObjectsCommand(adapter=self.adapter)( addresses=addresses, - ) + )['transactions'] # Map and filter transactions which have zero value. # If multiple transactions for the same address are returned, diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index 740ec433..a8d3eb8e 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -9,6 +9,7 @@ from iota.adapter import BaseAdapter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.commands.core.get_trytes import GetTrytesCommand +from iota.commands.extended import FindTransactionObjectsCommand from iota.commands.extended.get_bundles import GetBundlesCommand from iota.commands.extended.get_latest_inclusion import \ GetLatestInclusionCommand @@ -16,27 +17,6 @@ from iota.crypto.types import Seed -def find_transaction_objects(adapter, **kwargs): - # type: (BaseAdapter, **Iterable) -> List[Transaction] - """ - Finds transactions matching the specified criteria, fetches the - corresponding trytes and converts them into Transaction objects. - """ - ft_response = FindTransactionsCommand(adapter)(**kwargs) - - hashes = ft_response['hashes'] - - if hashes: - gt_response = GetTrytesCommand(adapter)(hashes=hashes) - - return list(map( - Transaction.from_tryte_string, - gt_response.get('trytes') or [], - )) # type: List[Transaction] - - return [] - - def iter_used_addresses( adapter, # type: BaseAdapter seed, # type: Seed @@ -103,10 +83,9 @@ def get_bundles_from_transaction_hashes( non_tail_bundle_hashes.add(txn.bundle_hash) if non_tail_bundle_hashes: - for txn in find_transaction_objects( - adapter=adapter, + for txn in FindTransactionObjectsCommand(adapter=adapter)( bundles=list(non_tail_bundle_hashes), - ): + )['transactions']: if txn.is_tail: if txn.hash not in tail_transaction_hashes: all_transactions.append(txn) diff --git a/test/commands/extended/find_transaction_objects.py b/test/commands/extended/find_transaction_objects.py new file mode 100644 index 00000000..a98b97c7 --- /dev/null +++ b/test/commands/extended/find_transaction_objects.py @@ -0,0 +1,118 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +import mock + +from iota import Iota, MockAdapter, Transaction +from iota.commands.extended import FindTransactionObjectsCommand + + +class FindTransactionObjectsCommandTestCase(TestCase): + # noinspection SpellCheckingInspection + def setUp(self): + super(FindTransactionObjectsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = FindTransactionObjectsCommand(self.adapter) + + # Define values that we can reuse across tests. + self.address = 'A' * 81 + self.transaction_hash = \ + b'BROTOVRCAEMFLRWGPVWDPDTBRAMLHVCHQDEHXLCWH' \ + b'KKXLVDFCPIJEUZTPPFMPQQ9KOHAEUAMMVJN99999' + self.trytes = \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999999999999999999999999999999999999999999999999' \ + b'99999999999999999AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \ + b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA99999999999999999999999999' \ + b'9QC9999999999999999999999999PQYJHAD99999999999999999999WHIUDFV' \ + b'IFXNBJVEHYPLDADIDINGAWMHYIJNPYUDWXCAWL9GSKTUIZLJGGFIXEIYTJEDQZ' \ + b'TIYRXHC9PBWBDSOTEJTQTYYSZLVTFLDQMZSGLHKLYVJOLMXIJJRTGS9RYBXLAT' \ + b'ZJXBVBCPUGWRUKZJYLBGPKRKWIA9999FPYHMFFWMMKOHTSAPMMATZQLWXJSPMT' \ + b'JSRQIPMDCQXFFMXMHCYDKVJCFSRECAVALCOFIYCJLNRZZZ9999999999999999' \ + b'999999999999999KITCXNZOF999999999MMMMMMMMMEA9999F9999999999999' \ + b'9999999' + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).findTransactionObjects, + FindTransactionObjectsCommand, + ) + + def test_transaction_found(self): + """ + A transaction is found with the inputs. A transaction object is + returned + """ + with mock.patch( + 'iota.commands.core.find_transactions.FindTransactionsCommand.' + '_execute', + mock.Mock(return_value={'hashes': [self.transaction_hash, ]}), + ): + with mock.patch( + 'iota.commands.core.get_trytes.GetTrytesCommand._execute', + mock.Mock(return_value={'trytes': [self.trytes, ]}), + ): + response = 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): + """ + 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': []}), + ): + response = self.command(addresses=[self.address]) + + self.assertDictEqual( + response, + { + 'transactions': [], + }, + )