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

Commit 82a0f24

Browse files
committed
get_bundles can return multiple bundles
- accept a list of tail transaction hashes instead of just one. - return multiple bundles in the same order as the input tail hashes - added test coverage - modified calls to GetBundlesCommand() in the lib WARNING: Breaking change!
1 parent 7635bb9 commit 82a0f24

File tree

6 files changed

+142
-51
lines changed

6 files changed

+142
-51
lines changed

iota/api.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -983,14 +983,14 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False, security_
983983
security_level=security_level
984984
)
985985

986-
def get_bundles(self, transaction):
987-
# type: (TransactionHash) -> dict
986+
def get_bundles(self, transactions):
987+
# type: (Iterable[TransactionHash]) -> dict
988988
"""
989989
Returns the bundle(s) associated with the specified transaction
990-
hash.
990+
hashes.
991991
992-
:param TransactionHash transaction:
993-
Transaction hash. Must be a tail transaction.
992+
:param Iterable[TransactionHash] transactions:
993+
Transaction hashes. Must be a tail transaction.
994994
995995
:return:
996996
``dict`` with the following structure::
@@ -1001,15 +1001,15 @@ def get_bundles(self, transaction):
10011001
always a list, even if only one bundle was found.
10021002
}
10031003
1004-
:raise:
1005-
- :py:class:`iota.adapter.BadApiResponse` if any of the
1006-
bundles fails validation.
1004+
:raise :py:class:`iota.adapter.BadApiResponse`:
1005+
- if any of the bundles fails validation.
1006+
- if any of the bundles is not visible on the Tangle.
10071007
10081008
References:
10091009
10101010
- https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle
10111011
"""
1012-
return extended.GetBundlesCommand(self.adapter)(transaction=transaction)
1012+
return extended.GetBundlesCommand(self.adapter)(transactions=transactions)
10131013

10141014
def get_inputs(
10151015
self,

iota/commands/extended/broadcast_bundle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def _execute(self, request):
3636
# and validates it.
3737
# Returns List[List[TransactionTrytes]]
3838
# (outer list has one item in current implementation)
39-
bundle = GetBundlesCommand(self.adapter)(transaction=request['tail_hash'])
39+
bundle = GetBundlesCommand(self.adapter)(transactions=[request['tail_hash']])
4040
BroadcastTransactionsCommand(self.adapter)(trytes=bundle[0])
4141
return {
4242
'trytes': bundle[0],

iota/commands/extended/get_bundles.py

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88
from iota.commands import FilterCommand, RequestFilter
99
from iota.commands.extended.traverse_bundle import TraverseBundleCommand
1010
from iota.exceptions import with_context
11-
from iota.filters import Trytes
1211
from iota.transaction.validator import BundleValidator
12+
from iota.filters import Trytes
1313

1414
__all__ = [
1515
'GetBundlesCommand',
@@ -31,35 +31,41 @@ def get_response_filter(self):
3131
pass
3232

3333
def _execute(self, request):
34-
transaction_hash = request['transaction'] # type: TransactionHash
34+
transaction_hashes = request['transactions'] # type: Iterable[TransactionHash]
35+
36+
bundles = []
37+
38+
# Fetch bundles one-by-one
39+
for tx_hash in transaction_hashes:
40+
bundle = TraverseBundleCommand(self.adapter)(
41+
transaction=tx_hash
42+
)['bundles'][0] # Currently 1 bundle only
3543

36-
bundle = TraverseBundleCommand(self.adapter)(
37-
transaction=transaction_hash
38-
)['bundles'][0] # Currently 1 bundle only
44+
validator = BundleValidator(bundle)
3945

40-
validator = BundleValidator(bundle)
46+
if not validator.is_valid():
47+
raise with_context(
48+
exc=BadApiResponse(
49+
'Bundle failed validation (``exc.context`` has more info).',
50+
),
4151

42-
if not validator.is_valid():
43-
raise with_context(
44-
exc=BadApiResponse(
45-
'Bundle failed validation (``exc.context`` has more info).',
46-
),
52+
context={
53+
'bundle': bundle,
54+
'errors': validator.errors,
55+
},
56+
)
4757

48-
context={
49-
'bundle': bundle,
50-
'errors': validator.errors,
51-
},
52-
)
58+
bundles.append(bundle)
5359

5460
return {
55-
# Always return a list, so that we have the necessary
56-
# structure to return multiple bundles in a future
57-
# iteration.
58-
'bundles': [bundle],
61+
'bundles': bundles,
5962
}
6063

6164
class GetBundlesRequestFilter(RequestFilter):
6265
def __init__(self):
6366
super(GetBundlesRequestFilter, self).__init__({
64-
'transaction': f.Required | Trytes(TransactionHash),
67+
'transactions':
68+
f.Required | f.Array | f.FilterRepeater(
69+
f.Required | Trytes(TransactionHash)
70+
)
6571
})

iota/commands/extended/replay_bundle.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def _execute(self, request):
3434
min_weight_magnitude = request['minWeightMagnitude'] # type: int
3535
transaction = request['transaction'] # type: TransactionHash
3636

37-
gb_response = GetBundlesCommand(self.adapter)(transaction=transaction)
37+
gb_response = GetBundlesCommand(self.adapter)(transactions=[transaction])
3838

3939
# Note that we only replay the first bundle returned by
4040
# ``getBundles``.

iota/commands/extended/utils.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def get_bundles_from_transaction_hashes(
118118

119119
# Find the bundles for each transaction.
120120
for txn in tail_transactions:
121-
gb_response = GetBundlesCommand(adapter)(transaction=txn.hash)
121+
gb_response = GetBundlesCommand(adapter)(transactions=[txn.hash])
122122
txn_bundles = gb_response['bundles'] # type: List[Bundle]
123123

124124
if inclusion_states:

test/commands/extended/get_bundles_test.py

Lines changed: 103 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -23,18 +23,24 @@ def setUp(self):
2323
super(GetBundlesRequestFilterTestCase, self).setUp()
2424

2525
# noinspection SpellCheckingInspection
26-
self.transaction = (
27-
'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR'
28-
'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ'
29-
)
26+
self.transactions = [
27+
(
28+
'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR'
29+
'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ'
30+
),
31+
(
32+
'TESTVALUE9DONTUSEINPRODUCTION99999TAXQBF'
33+
'ZMUQLZ9RXRRXQOUSAMGAPEKTZNERIKSDYGHQA9999'
34+
),
35+
]
3036

3137
def test_pass_happy_path(self):
3238
"""
3339
Request is valid.
3440
"""
3541
# Raw trytes are extracted to match the IRI's JSON protocol.
3642
request = {
37-
'transaction': self.transaction,
43+
'transactions': self.transactions,
3844
}
3945

4046
filter_ = self._filter(request)
@@ -47,17 +53,22 @@ def test_pass_compatible_types(self):
4753
Request contains values that can be converted to the expected
4854
types.
4955
"""
56+
# Convert first to TranscationHash
57+
tx_hashes = []
58+
for tx in self.transactions:
59+
tx_hashes.append(TransactionHash(tx))
60+
5061
filter_ = self._filter({
5162
# Any TrytesCompatible value will work here.
52-
'transaction': TransactionHash(self.transaction),
63+
'transactions': tx_hashes,
5364
})
5465

5566
self.assertFilterPasses(filter_)
5667
self.assertDictEqual(
5768
filter_.cleaned_data,
5869

5970
{
60-
'transaction': self.transaction,
71+
'transactions': self.transactions,
6172
},
6273
)
6374

@@ -69,7 +80,7 @@ def test_fail_empty(self):
6980
{},
7081

7182
{
72-
'transaction': [f.FilterMapper.CODE_MISSING_KEY],
83+
'transactions': [f.FilterMapper.CODE_MISSING_KEY],
7384
},
7485
)
7586

@@ -79,7 +90,7 @@ def test_fail_unexpected_parameters(self):
7990
"""
8091
self.assertFilterErrors(
8192
{
82-
'transaction': TransactionHash(self.transaction),
93+
'transactions': self.transactions,
8394

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

93104
def test_fail_transaction_wrong_type(self):
94105
"""
95-
``transaction`` is not a TrytesCompatible value.
106+
``transactions`` contains no TrytesCompatible value.
96107
"""
97108
self.assertFilterErrors(
98109
{
99-
'transaction': 42,
110+
'transactions': [42],
100111
},
101112

102113
{
103-
'transaction': [f.Type.CODE_WRONG_TYPE],
114+
'transactions.0': [f.Type.CODE_WRONG_TYPE],
104115
},
105116
)
106117

107118
def test_fail_transaction_not_trytes(self):
108119
"""
109-
``transaction`` contains invalid characters.
120+
``transactions`` contains invalid characters.
121+
"""
122+
self.assertFilterErrors(
123+
{
124+
'transactions': [b'not valid; must contain only uppercase and "9"'],
125+
},
126+
127+
{
128+
'transactions.0': [Trytes.CODE_NOT_TRYTES],
129+
},
130+
)
131+
132+
def test_fail_no_list(self):
133+
"""
134+
``transactions`` has one hash rather than a list of hashes.
135+
"""
136+
self.assertFilterErrors(
137+
{
138+
'transactions': self.transactions[0],
139+
},
140+
141+
{
142+
'transactions': [f.Type.CODE_WRONG_TYPE],
143+
},
144+
)
145+
146+
def test_fail_transactions_contents_invalid(self):
147+
"""
148+
``transactions`` is a non-empty array, but it contains invlaid values.
110149
"""
111150
self.assertFilterErrors(
112151
{
113-
'transaction': b'not valid; must contain only uppercase and "9"',
152+
'transactions': [
153+
b'',
154+
True,
155+
None,
156+
b'not valid transaction hash',
157+
158+
# A valid tx hash, this should not produce error
159+
TransactionHash(self.transactions[0]),
160+
161+
65498731,
162+
b'9' * (TransactionHash.LEN +1),
163+
],
114164
},
115165

116166
{
117-
'transaction': [Trytes.CODE_NOT_TRYTES],
167+
'transactions.0': [f.Required.CODE_EMPTY],
168+
'transactions.1': [f.Type.CODE_WRONG_TYPE],
169+
'transactions.2': [f.Required.CODE_EMPTY],
170+
'transactions.3': [Trytes.CODE_NOT_TRYTES],
171+
'transactions.5': [f.Type.CODE_WRONG_TYPE],
172+
'transactions.6': [Trytes.CODE_WRONG_FORMAT],
118173
},
119174
)
120175

@@ -286,7 +341,7 @@ def test_wireup(self):
286341
api = Iota(self.adapter)
287342

288343
# Don't need to call with proper args here.
289-
response = api.get_bundles('transaction')
344+
response = api.get_bundles('transactions')
290345

291346
self.assertTrue(mocked_command.called)
292347

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

311-
response = self.command(transaction = self.tx_hash)
366+
response = self.command(transactions = [self.tx_hash])
367+
368+
self.maxDiff = None
369+
original_bundle = Bundle.from_tryte_strings(self.bundle_trytes)
370+
self.assertListEqual(
371+
response['bundles'][0].as_json_compatible(),
372+
original_bundle.as_json_compatible(),
373+
)
374+
375+
def test_happy_path_multiple_bundles(self):
376+
"""
377+
Get two bundles with multiple transactions.
378+
"""
379+
# We will fetch the same two bundle
380+
for _ in range(2):
381+
for txn_trytes in self.bundle_trytes:
382+
self.adapter.seed_response('getTrytes', {
383+
'trytes': [txn_trytes],
384+
})
385+
386+
self.adapter.seed_response('getTrytes', {
387+
'trytes': [self.spam_trytes],
388+
})
389+
390+
response = self.command(transactions = [self.tx_hash, self.tx_hash])
312391

313392
self.maxDiff = None
314393
original_bundle = Bundle.from_tryte_strings(self.bundle_trytes)
394+
315395
self.assertListEqual(
316396
response['bundles'][0].as_json_compatible(),
317397
original_bundle.as_json_compatible(),
318398
)
319399

400+
self.assertListEqual(
401+
response['bundles'][1].as_json_compatible(),
402+
original_bundle.as_json_compatible(),
403+
)
404+
320405
def test_validator_error(self):
321406
"""
322407
TraverseBundleCommand returns bundle but it is invalid.
@@ -335,4 +420,4 @@ def test_validator_error(self):
335420
})
336421

337422
with self.assertRaises(BadApiResponse):
338-
response = self.command(transaction = self.tx_hash)
423+
response = self.command(transactions = [self.tx_hash])

0 commit comments

Comments
 (0)