class GetTrytesResponseFilter(BaseFilterTestCase): filter_type = GetTrytesCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection def setUp(self): super(GetTrytesResponseFilter, self).setUp() # Define some valid tryte sequences that we can re-use between # tests. self.trytes1 = 'RBTC9D9DCDQAEASBYBCCKBFA' self.trytes2 = \ 'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' def test_pass_transactions(self): """ The response contains data for multiple transactions. """ filter_ = self._filter({ 'trytes': [ # In real life, these values would be a lot longer, but for the # purposes of this test, any sequence of trytes will do. self.trytes1, self.trytes2, ], 'duration': 42, }) self.assertFilterPasses(filter_) self.assertDictEqual( filter_.cleaned_data, { 'trytes': [ TryteString(self.trytes1), TryteString(self.trytes2), ], 'duration': 42, }, ) def test_pass_no_transactions(self): """ The response does not contain any transactions. """ response = { 'trytes': [], 'duration': 42, } filter_ = self._filter(response) self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, response)
def find_transaction_objects(adapter, **kwargs): # type: (BaseAdapter, dict) -> 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 _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 )
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 _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 no tx was found by the node for txn_hash, it returns 9s, # so we check here if it returned all 9s trytes. if not trytes or trytes == [TransactionTrytes('')]: raise with_context( exc=BadApiResponse( 'Could not get trytes of bundle transaction from the Tangle. ' '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)
def get_bundles_from_transaction_hashes( adapter, transaction_hashes, inclusion_states, ): # type: (BaseAdapter, Iterable[TransactionHash], bool) -> List[Bundle] """ Given a set of transaction hashes, returns the corresponding bundles, sorted by tail transaction timestamp. """ transaction_hashes = list(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) all_transactions = list( map( Transaction.from_tryte_string, gt_response['trytes'], )) # type: List[Transaction] for txn in all_transactions: if txn.is_tail: tail_transaction_hashes.add(txn.hash) else: # Capture the bundle ID instead of the transaction hash so # that we can query the node to find the tail transaction # for that bundle. non_tail_bundle_hashes.add(txn.bundle_hash) if non_tail_bundle_hashes: for txn in find_transaction_objects( adapter=adapter, bundles=list(non_tail_bundle_hashes), ): if txn.is_tail: if txn.hash not in tail_transaction_hashes: all_transactions.append(txn) tail_transaction_hashes.add(txn.hash) # Filter out all non-tail transactions. tail_transactions = [ txn for txn in all_transactions if txn.hash in tail_transaction_hashes ] # Attach inclusion states, if requested. if inclusion_states: gli_response = GetLatestInclusionCommand(adapter)( hashes=list(tail_transaction_hashes), ) for txn in tail_transactions: txn.is_confirmed = gli_response['states'].get(txn.hash) # Find the bundles for each transaction. for txn in tail_transactions: gb_response = GetBundlesCommand(adapter)(transaction=txn.hash) txn_bundles = gb_response['bundles'] # type: List[Bundle] if inclusion_states: for bundle in txn_bundles: bundle.is_confirmed = txn.is_confirmed my_bundles.extend(txn_bundles) return list( sorted( my_bundles, key=lambda bundle_: bundle_.tail_transaction.timestamp, ))
class GetTrytesRequestFilterTestCase(BaseFilterTestCase): filter_type = GetTrytesCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection def setUp(self): super(GetTrytesRequestFilterTestCase, self).setUp() # Define some valid tryte sequences that we can re-use between # tests. self.trytes1 = ( 'TESTVALUE9DONTUSEINPRODUCTION99999DLPDTB' 'XBXYOMQ9IWPKCDPNWBENBGHCSZDLRLZZ9VZEOHPLC' ) self.trytes2 = ( 'TESTVALUE9DONTUSEINPRODUCTION99999HEXCAN' 'LFTVWRDZJHDWJGVOOUWBXAHKVWDNNOCICGXXBKAEN' ) def test_pass_happy_path(self): """ The request is valid. """ request = { # Raw trytes are extracted to match the IRI's JSON protocol. 'hashes': [self.trytes1, self.trytes2], } filter_ = self._filter(request) self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ The request contains values that can be converted to the expected types. """ filter_ = self._filter({ # Any sequence that can be converted into an ASCII representation # of a TransactionHash is valid. 'hashes': [ TransactionHash(self.trytes1), bytearray(self.trytes2.encode('ascii')), ], }) self.assertFilterPasses(filter_) self.assertDictEqual( filter_.cleaned_data, { 'hashes': [self.trytes1, self.trytes2], }, ) def test_fail_empty(self): """ The request is empty. """ self.assertFilterErrors( {}, { 'hashes': [f.FilterMapper.CODE_MISSING_KEY], }, ) def test_fail_unexpected_parameters(self): """ The request contains unexpected parameters. """ self.assertFilterErrors( { 'hashes': [TransactionHash(self.trytes1)], # This is why we can't have nice things! 'foo': 'bar', }, { 'foo': [f.FilterMapper.CODE_EXTRA_KEY], }, ) def test_fail_hashes_null(self): """ ``hashes`` is null. """ self.assertFilterErrors( { 'hashes': None, }, { 'hashes': [f.Required.CODE_EMPTY], }, ) def test_fail_hashes_wrong_type(self): """ ``hashes`` is not an array. """ self.assertFilterErrors( { # ``hashes`` must be an array, even if we're only querying # against a single transaction. 'hashes': TransactionHash(self.trytes1), }, { 'hashes': [f.Type.CODE_WRONG_TYPE], }, ) def test_fail_hashes_empty(self): """ ``hashes`` is an array, but it is empty. """ self.assertFilterErrors( { 'hashes': [], }, { 'hashes': [f.Required.CODE_EMPTY], }, ) def test_fail_hashes_contents_invalid(self): """ ``hashes`` is an array, but it contains invalid values. """ self.assertFilterErrors( { 'hashes': [ b'', True, None, b'not valid trytes', # This is actually valid; I just added it to make sure the # filter isn't cheating! TryteString(self.trytes1), 2130706433, b'9' * 82, ], }, { 'hashes.0': [f.Required.CODE_EMPTY], 'hashes.1': [f.Type.CODE_WRONG_TYPE], 'hashes.2': [f.Required.CODE_EMPTY], 'hashes.3': [Trytes.CODE_NOT_TRYTES], 'hashes.5': [f.Type.CODE_WRONG_TYPE], 'hashes.6': [Trytes.CODE_WRONG_FORMAT], }, )
class GetTrytesRequestFilterTestCase(BaseFilterTestCase): filter_type = GetTrytesCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection def setUp(self): super(GetTrytesRequestFilterTestCase, self).setUp() # Define some valid tryte sequences that we can re-use between # tests. self.trytes1 = (b'OAATQS9VQLSXCLDJVJJVYUGONXAXOFMJOZNSYWRZ' b'SWECMXAQQURHQBJNLD9IOFEPGZEPEMPXCIVRX9999') self.trytes2 = (b'ZIJGAJ9AADLRPWNCYNNHUHRRAC9QOUDATEDQUMTN' b'OTABUVRPTSTFQDGZKFYUUIE9ZEBIVCCXXXLKX9999') def test_pass_happy_path(self): """ The request is valid. """ request = { 'hashes': [ TransactionHash(self.trytes1), TransactionHash(self.trytes2), ], } filter_ = self._filter(request) self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ The request contains values that can be converted to the expected types. """ filter_ = self._filter({ 'hashes': [ # Any sequence that can be converted into a TransactionHash is # valid. binary_type(self.trytes1), bytearray(self.trytes2), ], }) self.assertFilterPasses(filter_) self.assertDictEqual( filter_.cleaned_data, { 'hashes': [ TransactionHash(self.trytes1), TransactionHash(self.trytes2), ], }, ) def test_fail_empty(self): """ The request is empty. """ self.assertFilterErrors( {}, { 'hashes': [f.FilterMapper.CODE_MISSING_KEY], }, ) def test_fail_unexpected_parameters(self): """ The request contains unexpected parameters. """ self.assertFilterErrors( { 'hashes': [TransactionHash(self.trytes1)], # This is why we can't have nice things! 'foo': 'bar', }, { 'foo': [f.FilterMapper.CODE_EXTRA_KEY], }, ) def test_fail_hashes_null(self): """ ``hashes`` is null. """ self.assertFilterErrors( { 'hashes': None, }, { 'hashes': [f.Required.CODE_EMPTY], }, ) def test_fail_hashes_wrong_type(self): """ ``hashes`` is not an array. """ self.assertFilterErrors( { # ``hashes`` must be an array, even if we're only querying # against a single transaction. 'hashes': TransactionHash(self.trytes1), }, { 'hashes': [f.Type.CODE_WRONG_TYPE], }, ) def test_fail_hashes_empty(self): """ ``hashes`` is an array, but it is empty. """ self.assertFilterErrors( { 'hashes': [], }, { 'hashes': [f.Required.CODE_EMPTY], }, ) def test_fail_hashes_contents_invalid(self): """ ``hashes`` is an array, but it contains invalid values. """ self.assertFilterErrors( { 'hashes': [ b'', text_type(self.trytes1, 'ascii'), True, None, b'not valid trytes', # This is actually valid; I just added it to make sure the # filter isn't cheating! TryteString(self.trytes1), 2130706433, b'9' * 82, ], }, { 'hashes.0': [f.Required.CODE_EMPTY], 'hashes.1': [f.Type.CODE_WRONG_TYPE], 'hashes.2': [f.Type.CODE_WRONG_TYPE], 'hashes.3': [f.Required.CODE_EMPTY], 'hashes.4': [Trytes.CODE_NOT_TRYTES], 'hashes.6': [f.Type.CODE_WRONG_TYPE], 'hashes.7': [Trytes.CODE_WRONG_FORMAT], }, )
def _execute(self, request): stop = request['stop'] # type: Optional[int] inclusion_states = request['inclusionStates'] # type: bool seed = request['seed'] # type: Seed start = request['start'] # type: int generator = AddressGenerator(seed) ft_command = FindTransactionsCommand(self.adapter) # Determine the addresses we will be scanning, and pull their # transaction hashes. if stop is None: # This is similar to the ``getNewAddresses`` command, except it # is interested in all the addresses that `getNewAddresses` # skips. hashes = [] for addy in generator.create_generator(start): ft_response = ft_command(addresses=[addy]) if ft_response.get('hashes'): hashes += ft_response['hashes'] else: break # Reset the command so that we can call it again. ft_command.reset() else: ft_response =\ ft_command(addresses=generator.get_addresses(start, stop - start)) hashes = ft_response.get('hashes') or [] all_bundles = [] # type: List[Bundle] if hashes: # Sort transactions into tail and non-tail. tail_transaction_hashes = set() non_tail_bundle_hashes = set() gt_response = GetTrytesCommand(self.adapter)(hashes=hashes) all_transactions = list( map( Transaction.from_tryte_string, gt_response['trytes'], )) # type: List[Transaction] for txn in all_transactions: if txn.is_tail: tail_transaction_hashes.add(txn.hash) else: # Capture the bundle ID instead of the transaction hash so that # we can query the node to find the tail transaction for that # bundle. non_tail_bundle_hashes.add(txn.bundle_hash) if non_tail_bundle_hashes: for txn in self._find_transactions( bundles=list(non_tail_bundle_hashes)): if txn.is_tail: if txn.hash not in tail_transaction_hashes: all_transactions.append(txn) tail_transaction_hashes.add(txn.hash) # Filter out all non-tail transactions. tail_transactions = [ txn for txn in all_transactions if txn.hash in tail_transaction_hashes ] # Attach inclusion states, if requested. if inclusion_states: gli_response = GetLatestInclusionCommand(self.adapter)( hashes=list(tail_transaction_hashes), ) for txn in tail_transactions: txn.is_confirmed = gli_response['states'].get(txn.hash) # Find the bundles for each transaction. for txn in tail_transactions: gb_response = GetBundlesCommand( self.adapter)(transaction=txn.hash) txn_bundles = gb_response['bundles'] # type: List[Bundle] if inclusion_states: for bundle in txn_bundles: bundle.is_confirmed = txn.is_confirmed all_bundles.extend(txn_bundles) return { # Sort bundles by tail transaction timestamp. 'bundles': list( sorted( all_bundles, key=lambda bundle_: bundle_.tail_transaction.timestamp, )), }