Skip to content
This repository has been archived by the owner on Jan 13, 2023. It is now read-only.

Refactor traverse_bundle into Extended Api Command #256

Merged
merged 1 commit into from
Nov 5, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions docs/api.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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.
27 changes: 27 additions & 0 deletions iota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
1 change: 1 addition & 0 deletions iota/commands/extended/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@
from .replay_bundle import *
from .send_transfer import *
from .send_trytes import *
from .traverse_bundle import *
72 changes: 6 additions & 66 deletions iota/commands/extended/get_bundles.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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():
Expand All @@ -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__({
Expand Down
108 changes: 108 additions & 0 deletions iota/commands/extended/traverse_bundle.py
Original file line number Diff line number Diff line change
@@ -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),
})
Loading