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

get_bundles can return multiple bundles #293

Merged
merged 1 commit into from
Jan 27, 2020
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
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],
lzpap marked this conversation as resolved.
Show resolved Hide resolved
},
)

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
todofixthis marked this conversation as resolved.
Show resolved Hide resolved
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])