diff --git a/docs/api.rst b/docs/api.rst index bcb3d18..d73abbd 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -429,3 +429,22 @@ This method returns a ``dict`` with the following items: - ``trytes: List[TransactionTrytes]``: Raw trytes that were published to the Tangle. + +``traverse_bundle`` +------------------- + +Given a tail ``TransactionHash``, returns the bundle(s) associated with it. +Unlike ``get_bundles``, this command does not validate the fetched bundle(s). + +Parameters +~~~~~~~~~~ + +- ``tail_hash: TransactionHash``: Hash of a tail transaction. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``bundles: List[Bundle]``: List of matching bundles. Note that this + value is always a list, even if only one bundle was found. diff --git a/iota/api.py b/iota/api.py index 3e0a55e..567b3db 100644 --- a/iota/api.py +++ b/iota/api.py @@ -1292,3 +1292,30 @@ def is_reattachable(self, addresses): return extended.IsReattachableCommand(self.adapter)( addresses=addresses ) + + 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 ``findTransactions``, and + it ensures we don't collect transactions from replayed bundles. + + :param 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 extended.TraverseBundleCommand(self.adapter)( + transaction=tail_hash + ) diff --git a/iota/commands/extended/__init__.py b/iota/commands/extended/__init__.py index e9e55c0..d1c5c11 100644 --- a/iota/commands/extended/__init__.py +++ b/iota/commands/extended/__init__.py @@ -27,3 +27,4 @@ from .replay_bundle import * from .send_transfer import * from .send_trytes import * +from .traverse_bundle import * diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index bec1efc..ac2575a 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -2,14 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import List, Optional - import filters as f -from iota import BadApiResponse, Bundle, BundleHash, Transaction, \ - TransactionHash, TryteString +from iota import BadApiResponse, TransactionHash from iota.commands import FilterCommand, RequestFilter -from iota.commands.core.get_trytes import GetTrytesCommand +from iota.commands.extended.traverse_bundle import TraverseBundleCommand from iota.exceptions import with_context from iota.filters import Trytes from iota.transaction.validator import BundleValidator @@ -36,7 +33,10 @@ def get_response_filter(self): def _execute(self, request): transaction_hash = request['transaction'] # type: TransactionHash - bundle = Bundle(self._traverse_bundle(transaction_hash)) + bundle = TraverseBundleCommand(self.adapter)( + transaction=transaction_hash + )['bundles'][0] # Currently 1 bundle only + validator = BundleValidator(bundle) if not validator.is_valid(): @@ -58,66 +58,6 @@ def _execute(self, request): 'bundles': [bundle], } - def _traverse_bundle(self, txn_hash, target_bundle_hash=None): - # type: (TransactionHash, Optional[BundleHash]) -> List[Transaction] - """ - Recursively traverse the Tangle, collecting transactions until - we hit a new bundle. - - 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] - - if not trytes: - raise with_context( - exc=BadApiResponse( - 'Bundle transactions not visible ' - '(``exc.context`` has more info).', - ), - - context={ - 'transaction_hash': txn_hash, - 'target_bundle_hash': target_bundle_hash, - }, - ) - - transaction = Transaction.from_tryte_string(trytes[0]) - - if (not target_bundle_hash) and transaction.current_index: - raise with_context( - exc=BadApiResponse( - '``_traverse_bundle`` started with a non-tail transaction ' - '(``exc.context`` has more info).', - ), - - context={ - 'transaction_object': transaction, - 'target_bundle_hash': target_bundle_hash, - }, - ) - - if target_bundle_hash: - if target_bundle_hash != transaction.bundle_hash: - # We've hit a different bundle; we can stop now. - return [] - else: - target_bundle_hash = transaction.bundle_hash - - if transaction.current_index == transaction.last_index == 0: - # Bundle only has one transaction. - return [transaction] - - # Recursively follow the trunk transaction, to fetch the next - # transaction in the bundle. - return [transaction] + self._traverse_bundle( - txn_hash=transaction.trunk_transaction_hash, - target_bundle_hash=target_bundle_hash - ) - - class GetBundlesRequestFilter(RequestFilter): def __init__(self): super(GetBundlesRequestFilter, self).__init__({ diff --git a/iota/commands/extended/traverse_bundle.py b/iota/commands/extended/traverse_bundle.py new file mode 100644 index 0000000..d81196d --- /dev/null +++ b/iota/commands/extended/traverse_bundle.py @@ -0,0 +1,108 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import List, Optional + +import filters as f + +from iota import BadApiResponse, BundleHash, Transaction, \ + TransactionHash, TryteString, Bundle +from iota.commands import FilterCommand, RequestFilter +from iota.commands.core.get_trytes import GetTrytesCommand +from iota.exceptions import with_context +from iota.filters import Trytes + +__all__ = [ + 'TraverseBundleCommand', +] + + +class TraverseBundleCommand(FilterCommand): + """ + Executes ``traverseBundle`` extended API command. + + See :py:meth:`iota.api.Iota.traverse_bundle` for more info. + """ + command = 'traverseBundle' + + def get_request_filter(self): + return TraverseBundleRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + txn_hash = request['transaction'] # type: TransactionHash + + bundle = Bundle(self._traverse_bundle(txn_hash, None)) + + # No bundle validation + + return { + 'bundles' : [bundle] + } + + def _traverse_bundle(self, txn_hash, target_bundle_hash): + """ + Recursively traverse the Tangle, collecting transactions until + we hit a new bundle. + + 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] + + if not trytes: + raise with_context( + exc=BadApiResponse( + 'Bundle transactions not visible ' + '(``exc.context`` has more info).', + ), + + context={ + 'transaction_hash': txn_hash, + 'target_bundle_hash': target_bundle_hash, + }, + ) + + transaction = Transaction.from_tryte_string(trytes[0]) + + if (not target_bundle_hash) and transaction.current_index: + raise with_context( + exc=BadApiResponse( + '``_traverse_bundle`` started with a non-tail transaction ' + '(``exc.context`` has more info).', + ), + + context={ + 'transaction_object': transaction, + 'target_bundle_hash': target_bundle_hash, + }, + ) + + if target_bundle_hash: + if target_bundle_hash != transaction.bundle_hash: + # We've hit a different bundle; we can stop now. + return [] + else: + target_bundle_hash = transaction.bundle_hash + + if transaction.current_index == transaction.last_index == 0: + # Bundle only has one transaction. + return [transaction] + + # Recursively follow the trunk transaction, to fetch the next + # transaction in the bundle. + return [transaction] + self._traverse_bundle( + transaction.trunk_transaction_hash, + target_bundle_hash + ) + +class TraverseBundleRequestFilter(RequestFilter): + def __init__(self): + super(TraverseBundleRequestFilter, self).__init__({ + 'transaction': f.Required | Trytes(TransactionHash), + }) diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py index 633dee9..2a1de50 100644 --- a/test/commands/extended/get_bundles_test.py +++ b/test/commands/extended/get_bundles_test.py @@ -1,439 +1,324 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from unittest import TestCase import filters as f from filters.test import BaseFilterTestCase -from iota import Address, BadApiResponse, Bundle, BundleHash, Fragment, Hash, \ - Iota, Tag, Transaction, TransactionHash, TransactionTrytes, Nonce +from iota import Address, BadApiResponse, Bundle, \ + Iota, TransactionHash, TransactionTrytes from iota.adapter import MockAdapter from iota.commands.extended.get_bundles import GetBundlesCommand from iota.filters import Trytes class GetBundlesRequestFilterTestCase(BaseFilterTestCase): - filter_type = GetBundlesCommand(MockAdapter()).get_request_filter - skip_value_check = True - - def setUp(self): - super(GetBundlesRequestFilterTestCase, self).setUp() - - # noinspection SpellCheckingInspection - self.transaction = ( - 'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR' - 'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ' - ) - - def test_pass_happy_path(self): - """ - Request is valid. - """ - # Raw trytes are extracted to match the IRI's JSON protocol. - request = { - 'transaction': self.transaction, - } - - filter_ = self._filter(request) - - self.assertFilterPasses(filter_) - self.assertDictEqual(filter_.cleaned_data, request) - - def test_pass_compatible_types(self): - """ - Request contains values that can be converted to the expected - types. - """ - filter_ = self._filter({ - # Any TrytesCompatible value will work here. - 'transaction': TransactionHash(self.transaction), - }) - - self.assertFilterPasses(filter_) - self.assertDictEqual( - filter_.cleaned_data, - - { - 'transaction': self.transaction, - }, - ) - - def test_fail_empty(self): - """ - Request is empty. - """ - self.assertFilterErrors( - {}, - - { - 'transaction': [f.FilterMapper.CODE_MISSING_KEY], - }, - ) - - def test_fail_unexpected_parameters(self): - """ - Request contains unexpected parameters. - """ - self.assertFilterErrors( - { - 'transaction': TransactionHash(self.transaction), - - # SAY "WHAT" AGAIN! - 'what': 'augh!', - }, - - { - 'what': [f.FilterMapper.CODE_EXTRA_KEY], - }, - ) - - def test_fail_transaction_wrong_type(self): - """ - ``transaction`` is not a TrytesCompatible value. - """ - self.assertFilterErrors( - { - 'transaction': 42, - }, - - { - 'transaction': [f.Type.CODE_WRONG_TYPE], - }, - ) - - def test_fail_transaction_not_trytes(self): - """ - ``transaction`` contains invalid characters. - """ - self.assertFilterErrors( - { - 'transaction': b'not valid; must contain only uppercase and "9"', - }, - - { - 'transaction': [Trytes.CODE_NOT_TRYTES], - }, - ) - - -# noinspection SpellCheckingInspection + filter_type = GetBundlesCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def setUp(self): + super(GetBundlesRequestFilterTestCase, self).setUp() + + # noinspection SpellCheckingInspection + self.transaction = ( + 'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR' + 'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ' + ) + + def test_pass_happy_path(self): + """ + Request is valid. + """ + # Raw trytes are extracted to match the IRI's JSON protocol. + request = { + 'transaction': self.transaction, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + # Any TrytesCompatible value will work here. + 'transaction': TransactionHash(self.transaction), + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'transaction': self.transaction, + }, + ) + + def test_fail_empty(self): + """ + Request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'transaction': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'transaction': TransactionHash(self.transaction), + + # SAY "WHAT" AGAIN! + 'what': 'augh!', + }, + + { + 'what': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_transaction_wrong_type(self): + """ + ``transaction`` is not a TrytesCompatible value. + """ + self.assertFilterErrors( + { + 'transaction': 42, + }, + + { + 'transaction': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_transaction_not_trytes(self): + """ + ``transaction`` contains invalid characters. + """ + self.assertFilterErrors( + { + 'transaction': b'not valid; must contain only uppercase and "9"', + }, + + { + 'transaction': [Trytes.CODE_NOT_TRYTES], + }, + ) + +# Tests related to TraverseBundleCommand are moved to +# iota/test/commands/extended/traverse_bundle_test.py +# Here we only include one 'happy path' test, and focus on bundle validator +# problems. class GetBundlesCommandTestCase(TestCase): - def setUp(self): - super(GetBundlesCommandTestCase, self).setUp() - - self.adapter = MockAdapter() - self.command = GetBundlesCommand(self.adapter) - - def test_wireup(self): - """ - Verifies that the command is wired up correctly. - """ - self.assertIsInstance( - Iota(self.adapter).getBundles, - GetBundlesCommand, - ) - - def test_single_transaction(self): - """ - Getting a bundle that contains a single transaction. - """ - transaction =\ - Transaction( - current_index = 0, - last_index = 0, - tag = Tag(b''), - timestamp = 1484960990, - value = 0, - attachment_timestamp = 1484960990, - attachment_timestamp_lower_bound = 12, - attachment_timestamp_upper_bound = 0, - - # These values are not relevant for 0-value transactions. - nonce = Nonce(b''), - signature_message_fragment = Fragment(b''), - - # This value is computed automatically, so it has to be real. - hash_ = - TransactionHash( - b'XPJIYZWPF9LBCYZPNBFARDRCSUGJGF9TWZT9K9PX' - b'VYDFPZOZBGXUCKLTJEUCFBEKQQ9VCSQVQDMMJQAY9', - ), + def setUp(self): + super(GetBundlesCommandTestCase, self).setUp() - address = - Address( - b'TESTVALUE9DONTUSEINPRODUCTION99999OCSGVF' - b'IBQA99KGTCPCZ9NHR9VGLGADDDIEGGPCGBDEDDTBC', - ), + self.adapter = MockAdapter() + self.command = GetBundlesCommand(self.adapter) - bundle_hash = - BundleHash( - b'TESTVALUE9DONTUSEINPRODUCTION99999DIOAZD' - b'M9AIUHXGVGBC9EMGI9SBVBAIXCBFJ9EELCPDRAD9U', - ), + # Tail transaction hash + self.tx_hash = TransactionHash( + 'TOYJPHKMLQNDVLDHDILARUJCCIUMQBLUSWPCTIVA' + 'DRXICGYDGSVPXFTILFFGAPICYHGGJ9OHXINFX9999' + ) - branch_transaction_hash = - TransactionHash( - b'TESTVALUE9DONTUSEINPRODUCTION99999BBCEDI' - b'ZHUDWBYDJEXHHAKDOCKEKDFIMB9AMCLFW9NBDEOFV', + self.bundle_trytes = [ + # Order is important if we don't convert to bundle representation. + # Tail transaction should be the first. + TransactionTrytes( + 'NBTCPCFDEACCPCBDVC9DTCQAJ9RBTC9D9DCDQAEAKDCDFD9DSCFAJ9VBCDJDTCQAJ9' + 'ZBMDYBCCKB99999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999SYRABNN9JD9PNDL' + 'IKUNCECUELTOHNLFMVD99999999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + 'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXFSEWUNJOEGNU' + 'I9QOCRFMYSIFAZLJHKZBPQZZYFG9ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9' + '999BGUEHHGAIWWQBCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJL' + 'EDAMYVRGABAWBY9999SYRABNN9JD9PNDLIKUNCECUELTOQZPSBDILVHJQVCEOICFAD' + 'YKZVGMOAXJRQNTCKMHGTAUMPGJJMX9LNF' ), - trunk_transaction_hash = - TransactionHash( - b'TESTVALUE9DONTUSEINPRODUCTION999999ARAYA' - b'MHCB9DCFEIWEWDLBCDN9LCCBQBKGDDAECFIAAGDAS', + TransactionTrytes( + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P' + '9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999XZUIENOTTBKJMDP' + 'RXWGQYG9PWGTHNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + 'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ' + 'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9' + '999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L' + 'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW' + 'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR' ), + ] + + # Add a spam tx. When this is returned, traverse_bundle knows it hit a + # different bundle and should stop. + self.spam_trytes = TransactionTrytes( + 'SPAMSPAMSPAM999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999JECDITWO9999999' + '999999999999ONLFMVD99999999999999999999VVCHSQSRVFKSBONDWB9EAQEMQOY' + 'YRBIZHTBJLYNAVDHZPUZAZ9LYHXWKBEJ9IPR9FAMFLT9EEOHVYWUPRHHSRCILCLWFD' + 'GBYBFFOKMCSAPVD9VGZZRRGBLGMZMXD9RMZQDBLMGN9BATWZGULRBCYQEIKIRBPHC9' + '999KTLTRSYOWBD9HVNP9GCUABARNGMYXUZKXWRPGOPETZLKYYC9Z9EYXIWVARUBMBM' + 'BPXGORN9WPBLY99999ZRBVQWULRFXDNDYZKRKIXPZQT9JJJH9FZU9PVWZJWLXBPODP' + 'EHMKTTAGEPLPHUQCZNLDSHERONOMHJCOI' + ) + + def test_wireup(self): + """ + Verifies that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getBundles, + GetBundlesCommand, + ) + + def test_happy_path(self): + """ + Get a bundle with multiple transactions. + """ + for txn_trytes in self.bundle_trytes: + self.adapter.seed_response('getTrytes', { + 'trytes': [txn_trytes], + }) + + self.adapter.seed_response('getTrytes', { + 'trytes': [self.spam_trytes], + }) + + response = self.command(transaction = self.tx_hash) + + self.maxDiff = None + original_bundle = Bundle.from_tryte_strings(self.bundle_trytes) + self.assertListEqual( + response['bundles'][0].as_json_compatible(), + original_bundle.as_json_compatible(), ) - self.adapter.seed_response('getTrytes', { - 'trytes': [transaction.as_tryte_string()], - }) - - response = self.command(transaction=transaction.hash) - - bundle = response['bundles'][0] # type: Bundle - self.assertEqual(len(bundle), 1) - - self.maxDiff = None - self.assertDictEqual( - bundle[0].as_json_compatible(), - transaction.as_json_compatible(), - ) - - def test_multiple_transactions(self): - """ - Getting a bundle that contains multiple transactions. - """ - bundle = Bundle.from_tryte_strings([ - TransactionTrytes( - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P' - b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999XZUIENOTTBKJMDP' - b'RXWGQYG9PWGTHNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' - b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ' - b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9' - b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L' - b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW' - b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR' - ), - - # Well, it was bound to happen sooner or later... the ASCII - # representation of this tryte sequence contains a very naughty - # phrase. But I don't feel like doing another POW, so... enjoy. - TransactionTrytes( - b'NBTCPCFDEACCPCBDVC9DTCQAJ9RBTC9D9DCDQAEAKDCDFD9DSCFAJ9VBCDJDTCQAJ9' - b'ZBMDYBCCKB99999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999SYRABNN9JD9PNDL' - b'IKUNCECUELTOHNLFMVD99999999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' - b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXFSEWUNJOEGNU' - b'I9QOCRFMYSIFAZLJHKZBPQZZYFG9ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9' - b'999BGUEHHGAIWWQBCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJL' - b'EDAMYVRGABAWBY9999SYRABNN9JD9PNDLIKUNCECUELTOQZPSBDILVHJQVCEOICFAD' - b'YKZVGMOAXJRQNTCKMHGTAUMPGJJMX9LNF' - ), - ]) - - for txn in bundle: - self.adapter.seed_response('getTrytes', { - 'trytes': [txn.as_tryte_string()], - }) - - self.adapter.seed_response('getTrytes', { - 'trytes': [ - 'SPAMSPAMSPAM999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999999999999999999' - '999999999999999999999999999999999999999999999999999JECDITWO9999999' - '999999999999ONLFMVD99999999999999999999VVCHSQSRVFKSBONDWB9EAQEMQOY' - 'YRBIZHTBJLYNAVDHZPUZAZ9LYHXWKBEJ9IPR9FAMFLT9EEOHVYWUPRHHSRCILCLWFD' - 'GBYBFFOKMCSAPVD9VGZZRRGBLGMZMXD9RMZQDBLMGN9BATWZGULRBCYQEIKIRBPHC9' - '999KTLTRSYOWBD9HVNP9GCUABARNGMYXUZKXWRPGOPETZLKYYC9Z9EYXIWVARUBMBM' - 'BPXGORN9WPBLY99999ZRBVQWULRFXDNDYZKRKIXPZQT9JJJH9FZU9PVWZJWLXBPODP' - 'EHMKTTAGEPLPHUQCZNLDSHERONOMHJCOI' - ], - }) - - response = self.command( - transaction = - TransactionHash( - b'TOYJPHKMLQNDVLDHDILARUJCCIUMQBLUSWPCTIVA' - b'DRXICGYDGSVPXFTILFFGAPICYHGGJ9OHXINFX9999' - ), - ) - self.maxDiff = None - self.assertListEqual( - response['bundles'][0].as_json_compatible(), - bundle.as_json_compatible(), - ) - - def test_non_tail_transaction(self): - """ - Trying to get a bundle for a non-tail transaction. - - This is not valid; you have to start with a tail transaction. - """ - self.adapter.seed_response('getTrytes', { - 'trytes': [ - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P' - b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999999999999999999' - b'999999999999HNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' - b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ' - b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9' - b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L' - b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW' - b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR' - ], - }) - - with self.assertRaises(BadApiResponse): - self.command( - transaction = - TransactionHash( - b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' - b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' - ), - ) - - def test_missing_transaction(self): - """ - Unable to find the requested transaction. - """ - self.adapter.seed_response('getTrytes', {'trytes': []}) - - with self.assertRaises(BadApiResponse): - self.command( - transaction = - TransactionHash( - b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' - b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' - ), - ) + def test_validator_error(self): + """ + TraverseBundleCommand returns bundle but it is invalid. + """ + # Make the returned bundle invalid + bundle = Bundle.from_tryte_strings(self.bundle_trytes) + bundle.transactions[0].value = 999 # Unbalanced bundle + + for txn in bundle.transactions: + self.adapter.seed_response('getTrytes', { + 'trytes': [txn.as_tryte_string()], + }) + + self.adapter.seed_response('getTrytes', { + 'trytes': [self.spam_trytes], + }) + + with self.assertRaises(BadApiResponse): + response = self.command(transaction = self.tx_hash) \ No newline at end of file diff --git a/test/commands/extended/traverse_bundle_test.py b/test/commands/extended/traverse_bundle_test.py new file mode 100644 index 0000000..e700c77 --- /dev/null +++ b/test/commands/extended/traverse_bundle_test.py @@ -0,0 +1,440 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +import filters as f +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 +from iota.commands.extended.traverse_bundle import TraverseBundleCommand +from iota.filters import Trytes + + +# Same tests as for GetBundlesRequestFilter (it is the same filter) +class TraverseBundleRequestFilterTestCase(BaseFilterTestCase): + filter_type = TraverseBundleCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def setUp(self): + super(TraverseBundleRequestFilterTestCase, self).setUp() + + # noinspection SpellCheckingInspection + self.transaction = ( + 'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR' + 'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ' + ) + + def test_pass_happy_path(self): + """ + Request is valid. + """ + # Raw trytes are extracted to match the IRI's JSON protocol. + request = { + 'transaction': self.transaction, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + # Any TrytesCompatible value will work here. + 'transaction': TransactionHash(self.transaction), + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'transaction': self.transaction, + }, + ) + + def test_fail_empty(self): + """ + Request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'transaction': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'transaction': TransactionHash(self.transaction), + + # SAY "WHAT" AGAIN! + 'what': 'augh!', + }, + + { + 'what': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_transaction_wrong_type(self): + """ + ``transaction`` is not a TrytesCompatible value. + """ + self.assertFilterErrors( + { + 'transaction': 42, + }, + + { + 'transaction': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_transaction_not_trytes(self): + """ + ``transaction`` contains invalid characters. + """ + self.assertFilterErrors( + { + 'transaction': b'not valid; must contain only uppercase and "9"', + }, + + { + 'transaction': [Trytes.CODE_NOT_TRYTES], + }, + ) + + +# noinspection SpellCheckingInspection +class TraverseBundleCommandTestCase(TestCase): + def setUp(self): + super(TraverseBundleCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = TraverseBundleCommand(self.adapter) + + def test_wireup(self): + """ + Verifies that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).traverseBundle, + TraverseBundleCommand, + ) + + def test_single_transaction(self): + """ + Getting a bundle that contains a single transaction. + """ + transaction =\ + Transaction( + current_index = 0, + last_index = 0, + tag = Tag(b''), + timestamp = 1484960990, + value = 0, + attachment_timestamp = 1484960990, + attachment_timestamp_lower_bound = 12, + attachment_timestamp_upper_bound = 0, + + # These values are not relevant for 0-value transactions. + nonce = Nonce(b''), + signature_message_fragment = Fragment(b''), + + # This value is computed automatically, so it has to be real. + hash_ = + TransactionHash( + b'XPJIYZWPF9LBCYZPNBFARDRCSUGJGF9TWZT9K9PX' + b'VYDFPZOZBGXUCKLTJEUCFBEKQQ9VCSQVQDMMJQAY9', + ), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999OCSGVF' + b'IBQA99KGTCPCZ9NHR9VGLGADDDIEGGPCGBDEDDTBC', + ), + + bundle_hash = + BundleHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999DIOAZD' + b'M9AIUHXGVGBC9EMGI9SBVBAIXCBFJ9EELCPDRAD9U', + ), + + branch_transaction_hash = + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999BBCEDI' + b'ZHUDWBYDJEXHHAKDOCKEKDFIMB9AMCLFW9NBDEOFV', + ), + + trunk_transaction_hash = + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION999999ARAYA' + b'MHCB9DCFEIWEWDLBCDN9LCCBQBKGDDAECFIAAGDAS', + ), + ) + + self.adapter.seed_response('getTrytes', { + 'trytes': [transaction.as_tryte_string()], + }) + + response = self.command(transaction=transaction.hash) + + bundle = response['bundles'][0] # type: Bundle + self.assertEqual(len(bundle), 1) + + self.maxDiff = None + self.assertDictEqual( + bundle[0].as_json_compatible(), + transaction.as_json_compatible(), + ) + + def test_multiple_transactions(self): + """ + Getting a bundle that contains multiple transactions. + """ + bundle = Bundle.from_tryte_strings([ + TransactionTrytes( + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P' + b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999XZUIENOTTBKJMDP' + b'RXWGQYG9PWGTHNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ' + b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9' + b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L' + b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW' + b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR' + ), + + # Well, it was bound to happen sooner or later... the ASCII + # representation of this tryte sequence contains a very naughty + # phrase. But I don't feel like doing another POW, so... enjoy. + TransactionTrytes( + b'NBTCPCFDEACCPCBDVC9DTCQAJ9RBTC9D9DCDQAEAKDCDFD9DSCFAJ9VBCDJDTCQAJ9' + b'ZBMDYBCCKB99999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999SYRABNN9JD9PNDL' + b'IKUNCECUELTOHNLFMVD99999999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXFSEWUNJOEGNU' + b'I9QOCRFMYSIFAZLJHKZBPQZZYFG9ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9' + b'999BGUEHHGAIWWQBCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJL' + b'EDAMYVRGABAWBY9999SYRABNN9JD9PNDLIKUNCECUELTOQZPSBDILVHJQVCEOICFAD' + b'YKZVGMOAXJRQNTCKMHGTAUMPGJJMX9LNF' + ), + ]) + + for txn in bundle: + self.adapter.seed_response('getTrytes', { + 'trytes': [txn.as_tryte_string()], + }) + + self.adapter.seed_response('getTrytes', { + 'trytes': [ + 'SPAMSPAMSPAM999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999JECDITWO9999999' + '999999999999ONLFMVD99999999999999999999VVCHSQSRVFKSBONDWB9EAQEMQOY' + 'YRBIZHTBJLYNAVDHZPUZAZ9LYHXWKBEJ9IPR9FAMFLT9EEOHVYWUPRHHSRCILCLWFD' + 'GBYBFFOKMCSAPVD9VGZZRRGBLGMZMXD9RMZQDBLMGN9BATWZGULRBCYQEIKIRBPHC9' + '999KTLTRSYOWBD9HVNP9GCUABARNGMYXUZKXWRPGOPETZLKYYC9Z9EYXIWVARUBMBM' + 'BPXGORN9WPBLY99999ZRBVQWULRFXDNDYZKRKIXPZQT9JJJH9FZU9PVWZJWLXBPODP' + 'EHMKTTAGEPLPHUQCZNLDSHERONOMHJCOI' + ], + }) + + response = self.command( + transaction = + TransactionHash( + b'TOYJPHKMLQNDVLDHDILARUJCCIUMQBLUSWPCTIVA' + b'DRXICGYDGSVPXFTILFFGAPICYHGGJ9OHXINFX9999' + ), + ) + self.maxDiff = None + self.assertListEqual( + response['bundles'][0].as_json_compatible(), + bundle.as_json_compatible(), + ) + + def test_non_tail_transaction(self): + """ + Trying to get a bundle for a non-tail transaction. + + This is not valid; you have to start with a tail transaction. + """ + self.adapter.seed_response('getTrytes', { + 'trytes': [ + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P' + b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999999999999999999' + b'999999999999HNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ' + b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9' + b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L' + b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW' + b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR' + ], + }) + + with self.assertRaises(BadApiResponse): + self.command( + transaction = + TransactionHash( + b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' + b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' + ), + ) + + def test_missing_transaction(self): + """ + Unable to find the requested transaction. + """ + self.adapter.seed_response('getTrytes', {'trytes': []}) + + with self.assertRaises(BadApiResponse): + self.command( + transaction = + TransactionHash( + b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' + b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' + ), + )