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

Commit

Permalink
Merge pull request #293 from lzpap/multiple_bundles
Browse files Browse the repository at this point in the history
`get_bundles` can return multiple bundles
  • Loading branch information
lzpap committed Jan 27, 2020
2 parents bf4915b + 82a0f24 commit 04fb2d0
Show file tree
Hide file tree
Showing 6 changed files with 142 additions and 51 deletions.
18 changes: 9 additions & 9 deletions iota/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -983,14 +983,14 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False, security_
security_level=security_level
)

def get_bundles(self, transaction):
# type: (TransactionHash) -> dict
def get_bundles(self, transactions):
# type: (Iterable[TransactionHash]) -> dict
"""
Returns the bundle(s) associated with the specified transaction
hash.
hashes.
:param TransactionHash transaction:
Transaction hash. Must be a tail transaction.
:param Iterable[TransactionHash] transactions:
Transaction hashes. Must be a tail transaction.
:return:
``dict`` with the following structure::
Expand All @@ -1001,15 +1001,15 @@ def get_bundles(self, transaction):
always a list, even if only one bundle was found.
}
:raise:
- :py:class:`iota.adapter.BadApiResponse` if any of the
bundles fails validation.
:raise :py:class:`iota.adapter.BadApiResponse`:
- if any of the bundles fails validation.
- if any of the bundles is not visible on the Tangle.
References:
- https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle
"""
return extended.GetBundlesCommand(self.adapter)(transaction=transaction)
return extended.GetBundlesCommand(self.adapter)(transactions=transactions)

def get_inputs(
self,
Expand Down
2 changes: 1 addition & 1 deletion iota/commands/extended/broadcast_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ def _execute(self, request):
# and validates it.
# Returns List[List[TransactionTrytes]]
# (outer list has one item in current implementation)
bundle = GetBundlesCommand(self.adapter)(transaction=request['tail_hash'])
bundle = GetBundlesCommand(self.adapter)(transactions=[request['tail_hash']])
BroadcastTransactionsCommand(self.adapter)(trytes=bundle[0])
return {
'trytes': bundle[0],
Expand Down
48 changes: 27 additions & 21 deletions iota/commands/extended/get_bundles.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@
from iota.commands import FilterCommand, RequestFilter
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
from iota.filters import Trytes

__all__ = [
'GetBundlesCommand',
Expand All @@ -31,35 +31,41 @@ def get_response_filter(self):
pass

def _execute(self, request):
transaction_hash = request['transaction'] # type: TransactionHash
transaction_hashes = request['transactions'] # type: Iterable[TransactionHash]

bundles = []

# Fetch bundles one-by-one
for tx_hash in transaction_hashes:
bundle = TraverseBundleCommand(self.adapter)(
transaction=tx_hash
)['bundles'][0] # Currently 1 bundle only

bundle = TraverseBundleCommand(self.adapter)(
transaction=transaction_hash
)['bundles'][0] # Currently 1 bundle only
validator = BundleValidator(bundle)

validator = BundleValidator(bundle)
if not validator.is_valid():
raise with_context(
exc=BadApiResponse(
'Bundle failed validation (``exc.context`` has more info).',
),

if not validator.is_valid():
raise with_context(
exc=BadApiResponse(
'Bundle failed validation (``exc.context`` has more info).',
),
context={
'bundle': bundle,
'errors': validator.errors,
},
)

context={
'bundle': bundle,
'errors': validator.errors,
},
)
bundles.append(bundle)

return {
# Always return a list, so that we have the necessary
# structure to return multiple bundles in a future
# iteration.
'bundles': [bundle],
'bundles': bundles,
}

class GetBundlesRequestFilter(RequestFilter):
def __init__(self):
super(GetBundlesRequestFilter, self).__init__({
'transaction': f.Required | Trytes(TransactionHash),
'transactions':
f.Required | f.Array | f.FilterRepeater(
f.Required | Trytes(TransactionHash)
)
})
2 changes: 1 addition & 1 deletion iota/commands/extended/replay_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ def _execute(self, request):
min_weight_magnitude = request['minWeightMagnitude'] # type: int
transaction = request['transaction'] # type: TransactionHash

gb_response = GetBundlesCommand(self.adapter)(transaction=transaction)
gb_response = GetBundlesCommand(self.adapter)(transactions=[transaction])

# Note that we only replay the first bundle returned by
# ``getBundles``.
Expand Down
2 changes: 1 addition & 1 deletion iota/commands/extended/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ def get_bundles_from_transaction_hashes(

# Find the bundles for each transaction.
for txn in tail_transactions:
gb_response = GetBundlesCommand(adapter)(transaction=txn.hash)
gb_response = GetBundlesCommand(adapter)(transactions=[txn.hash])
txn_bundles = gb_response['bundles'] # type: List[Bundle]

if inclusion_states:
Expand Down
121 changes: 103 additions & 18 deletions test/commands/extended/get_bundles_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,18 +23,24 @@ def setUp(self):
super(GetBundlesRequestFilterTestCase, self).setUp()

# noinspection SpellCheckingInspection
self.transaction = (
'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR'
'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ'
)
self.transactions = [
(
'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR'
'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ'
),
(
'TESTVALUE9DONTUSEINPRODUCTION99999TAXQBF'
'ZMUQLZ9RXRRXQOUSAMGAPEKTZNERIKSDYGHQA9999'
),
]

def test_pass_happy_path(self):
"""
Request is valid.
"""
# Raw trytes are extracted to match the IRI's JSON protocol.
request = {
'transaction': self.transaction,
'transactions': self.transactions,
}

filter_ = self._filter(request)
Expand All @@ -47,17 +53,22 @@ def test_pass_compatible_types(self):
Request contains values that can be converted to the expected
types.
"""
# Convert first to TranscationHash
tx_hashes = []
for tx in self.transactions:
tx_hashes.append(TransactionHash(tx))

filter_ = self._filter({
# Any TrytesCompatible value will work here.
'transaction': TransactionHash(self.transaction),
'transactions': tx_hashes,
})

self.assertFilterPasses(filter_)
self.assertDictEqual(
filter_.cleaned_data,

{
'transaction': self.transaction,
'transactions': self.transactions,
},
)

Expand All @@ -69,7 +80,7 @@ def test_fail_empty(self):
{},

{
'transaction': [f.FilterMapper.CODE_MISSING_KEY],
'transactions': [f.FilterMapper.CODE_MISSING_KEY],
},
)

Expand All @@ -79,7 +90,7 @@ def test_fail_unexpected_parameters(self):
"""
self.assertFilterErrors(
{
'transaction': TransactionHash(self.transaction),
'transactions': self.transactions,

# SAY "WHAT" AGAIN!
'what': 'augh!',
Expand All @@ -92,29 +103,73 @@ def test_fail_unexpected_parameters(self):

def test_fail_transaction_wrong_type(self):
"""
``transaction`` is not a TrytesCompatible value.
``transactions`` contains no TrytesCompatible value.
"""
self.assertFilterErrors(
{
'transaction': 42,
'transactions': [42],
},

{
'transaction': [f.Type.CODE_WRONG_TYPE],
'transactions.0': [f.Type.CODE_WRONG_TYPE],
},
)

def test_fail_transaction_not_trytes(self):
"""
``transaction`` contains invalid characters.
``transactions`` contains invalid characters.
"""
self.assertFilterErrors(
{
'transactions': [b'not valid; must contain only uppercase and "9"'],
},

{
'transactions.0': [Trytes.CODE_NOT_TRYTES],
},
)

def test_fail_no_list(self):
"""
``transactions`` has one hash rather than a list of hashes.
"""
self.assertFilterErrors(
{
'transactions': self.transactions[0],
},

{
'transactions': [f.Type.CODE_WRONG_TYPE],
},
)

def test_fail_transactions_contents_invalid(self):
"""
``transactions`` is a non-empty array, but it contains invlaid values.
"""
self.assertFilterErrors(
{
'transaction': b'not valid; must contain only uppercase and "9"',
'transactions': [
b'',
True,
None,
b'not valid transaction hash',

# A valid tx hash, this should not produce error
TransactionHash(self.transactions[0]),

65498731,
b'9' * (TransactionHash.LEN +1),
],
},

{
'transaction': [Trytes.CODE_NOT_TRYTES],
'transactions.0': [f.Required.CODE_EMPTY],
'transactions.1': [f.Type.CODE_WRONG_TYPE],
'transactions.2': [f.Required.CODE_EMPTY],
'transactions.3': [Trytes.CODE_NOT_TRYTES],
'transactions.5': [f.Type.CODE_WRONG_TYPE],
'transactions.6': [Trytes.CODE_WRONG_FORMAT],
},
)

Expand Down Expand Up @@ -286,7 +341,7 @@ def test_wireup(self):
api = Iota(self.adapter)

# Don't need to call with proper args here.
response = api.get_bundles('transaction')
response = api.get_bundles('transactions')

self.assertTrue(mocked_command.called)

Expand All @@ -308,15 +363,45 @@ def test_happy_path(self):
'trytes': [self.spam_trytes],
})

response = self.command(transaction = self.tx_hash)
response = self.command(transactions = [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(),
)

def test_happy_path_multiple_bundles(self):
"""
Get two bundles with multiple transactions.
"""
# We will fetch the same two bundle
for _ in range(2):
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(transactions = [self.tx_hash, 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.assertListEqual(
response['bundles'][1].as_json_compatible(),
original_bundle.as_json_compatible(),
)

def test_validator_error(self):
"""
TraverseBundleCommand returns bundle but it is invalid.
Expand All @@ -335,4 +420,4 @@ def test_validator_error(self):
})

with self.assertRaises(BadApiResponse):
response = self.command(transaction = self.tx_hash)
response = self.command(transactions = [self.tx_hash])

0 comments on commit 04fb2d0

Please sign in to comment.