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

Commit f014740

Browse files
authored
Merge pull request #234 from pdecol/99-find-transaction-objects
Implement find transaction objects command
2 parents f6d3c5d + e2b90a9 commit f014740

File tree

8 files changed

+261
-31
lines changed

8 files changed

+261
-31
lines changed

README.rst

+2-2
Original file line numberDiff line numberDiff line change
@@ -78,14 +78,14 @@ can also build the documentation locally:
7878

7979
#. Install extra dependencies (you only have to do this once)::
8080

81-
pip install '.[docs-builder]'
81+
pip install .[docs-builder]
8282

8383
.. tip::
8484

8585
To install the CCurl extension and the documentation builder tools
8686
together, use the following command::
8787

88-
pip install '.[ccurl,docs-builder]'
88+
pip install .[ccurl,docs-builder]
8989

9090
#. Switch to the ``docs`` directory::
9191

docs/api.rst

+34
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,40 @@ This method returns a ``dict`` with the following items:
3636
broadcast/stored. Should be the same as the value of the ``trytes``
3737
parameter.
3838

39+
40+
``find_transaction_objects``
41+
----------------------------
42+
43+
A more extensive version of the core API ``find_transactions`` that returns
44+
transaction objects instead of hashes.
45+
46+
Effectively, this is ``find_transactions`` + ``get_trytes`` + converting
47+
the trytes into transaction objects. It accepts the same parameters
48+
as ``find_transactions``
49+
50+
Find the transactions which match the specified input.
51+
All input values are lists, for which a list of return values
52+
(transaction hashes), in the same order, is returned for all
53+
individual elements. Using multiple of these input fields returns the
54+
intersection of the values.
55+
56+
Parameters
57+
~~~~~~~~~~
58+
59+
- ``bundles: Optional[Iterable[BundleHash]]``: List of bundle IDs.
60+
- ``addresses: Optional[Iterable[Address]]``: List of addresses.
61+
- ``tags: Optional[Iterable[Tag]]``: List of tags.
62+
- ``param: Optional[Iterable[TransactionHash]]``: List of approvee
63+
transaction IDs.
64+
65+
Return
66+
~~~~~~
67+
68+
This method returns a ``dict`` with the following items:
69+
70+
- ``transactions: List[Transaction]``: List of Transaction objects that
71+
match the input
72+
3973
``get_account_data``
4074
--------------------
4175

iota/api.py

+44
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,50 @@ def broadcast_and_store(self, trytes):
518518
"""
519519
return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes)
520520

521+
def find_transaction_objects(
522+
self,
523+
bundles=None, # type: Optional[Iterable[BundleHash]]
524+
addresses=None, # type: Optional[Iterable[Address]]
525+
tags=None, # type: Optional[Iterable[Tag]]
526+
approvees=None, # type: Optional[Iterable[TransactionHash]]
527+
):
528+
# type: (...) -> dict
529+
"""
530+
A more extensive version of :py:meth:`find_transactions` that
531+
returns transaction objects instead of hashes.
532+
533+
Effectively, this is ``find_transactions`` + ``get_trytes`` +
534+
converting the trytes into transaction objects.
535+
536+
It accepts the same parameters as :py:meth:`find_transactions`
537+
538+
:param bundles:
539+
List of bundle IDs.
540+
541+
:param addresses:
542+
List of addresses.
543+
544+
:param tags:
545+
List of tags.
546+
547+
:param approvees:
548+
List of approvee transaction IDs.
549+
550+
:return:
551+
Dict with the following structure::
552+
553+
{
554+
'transactions': List[Transaction],
555+
List of Transaction objects that match the input.
556+
}
557+
"""
558+
return extended.FindTransactionObjectsCommand(self.adapter)(
559+
bundles=bundles,
560+
addresses=addresses,
561+
tags=tags,
562+
approvees=approvees,
563+
)
564+
521565
def get_account_data(self, start=0, stop=None, inclusion_states=False, security_level=None):
522566
# type: (int, Optional[int], bool, Optional[int]) -> dict
523567
"""

iota/commands/extended/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
unicode_literals
1313

1414
from .broadcast_and_store import *
15+
from .find_transaction_objects import *
1516
from .get_account_data import *
1617
from .get_bundles import *
1718
from .get_inputs import *
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# coding=utf-8
2+
from __future__ import absolute_import, division, print_function, \
3+
unicode_literals
4+
5+
from typing import Iterable, List, Optional
6+
7+
from iota import Address, BundleHash, Tag, Transaction, TransactionHash
8+
from iota.commands.core import GetTrytesCommand, FindTransactionsCommand
9+
10+
__all__ = [
11+
'FindTransactionObjectsCommand',
12+
]
13+
14+
15+
class FindTransactionObjectsCommand(FindTransactionsCommand):
16+
"""
17+
Executes `FindTransactionObjects` command.
18+
19+
See :py:meth:`iota.api.StrictIota.find_transaction_objects`.
20+
"""
21+
command = 'findTransactionObjects'
22+
23+
def get_response_filter(self):
24+
pass
25+
26+
def _execute(self, request):
27+
bundles = request\
28+
.get('bundles') # type: Optional[Iterable[BundleHash]]
29+
addresses = request\
30+
.get('addresses') # type: Optional[Iterable[Address]]
31+
tags = request\
32+
.get('tags') # type: Optional[Iterable[Tag]]
33+
approvees = request\
34+
.get('approvees') # type: Optional[Iterable[TransactionHash]]
35+
36+
ft_response = FindTransactionsCommand(adapter=self.adapter)(
37+
bundles=bundles,
38+
addresses=addresses,
39+
tags=tags,
40+
approvees=approvees,
41+
)
42+
43+
hashes = ft_response['hashes']
44+
transactions = []
45+
if hashes:
46+
gt_response = GetTrytesCommand(adapter=self.adapter)(hashes=hashes)
47+
48+
transactions = list(map(
49+
Transaction.from_tryte_string,
50+
gt_response.get('trytes') or [],
51+
)) # type: List[Transaction]
52+
53+
return {
54+
'transactions': transactions,
55+
}

iota/commands/extended/is_reattachable.py

+4-5
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,8 @@
88

99
from iota import Address
1010
from iota.commands import FilterCommand, RequestFilter, ResponseFilter
11-
from iota.commands.extended import GetLatestInclusionCommand
12-
from iota.commands.extended.utils import find_transaction_objects
11+
from iota.commands.extended import FindTransactionObjectsCommand, \
12+
GetLatestInclusionCommand
1313
from iota.filters import Trytes
1414

1515
__all__ = [
@@ -33,10 +33,9 @@ def _execute(self, request):
3333
addresses = request['addresses'] # type: List[Address]
3434

3535
# fetch full transaction objects
36-
transactions = find_transaction_objects(
37-
adapter=self.adapter,
36+
transactions = FindTransactionObjectsCommand(adapter=self.adapter)(
3837
addresses=addresses,
39-
)
38+
)['transactions']
4039

4140
# Map and filter transactions which have zero value.
4241
# If multiple transactions for the same address are returned,

iota/commands/extended/utils.py

+3-24
Original file line numberDiff line numberDiff line change
@@ -9,34 +9,14 @@
99
from iota.adapter import BaseAdapter
1010
from iota.commands.core.find_transactions import FindTransactionsCommand
1111
from iota.commands.core.get_trytes import GetTrytesCommand
12+
from iota.commands.extended import FindTransactionObjectsCommand
1213
from iota.commands.extended.get_bundles import GetBundlesCommand
1314
from iota.commands.extended.get_latest_inclusion import \
1415
GetLatestInclusionCommand
1516
from iota.crypto.addresses import AddressGenerator
1617
from iota.crypto.types import Seed
1718

1819

19-
def find_transaction_objects(adapter, **kwargs):
20-
# type: (BaseAdapter, **Iterable) -> List[Transaction]
21-
"""
22-
Finds transactions matching the specified criteria, fetches the
23-
corresponding trytes and converts them into Transaction objects.
24-
"""
25-
ft_response = FindTransactionsCommand(adapter)(**kwargs)
26-
27-
hashes = ft_response['hashes']
28-
29-
if hashes:
30-
gt_response = GetTrytesCommand(adapter)(hashes=hashes)
31-
32-
return list(map(
33-
Transaction.from_tryte_string,
34-
gt_response.get('trytes') or [],
35-
)) # type: List[Transaction]
36-
37-
return []
38-
39-
4020
def iter_used_addresses(
4121
adapter, # type: BaseAdapter
4222
seed, # type: Seed
@@ -103,10 +83,9 @@ def get_bundles_from_transaction_hashes(
10383
non_tail_bundle_hashes.add(txn.bundle_hash)
10484

10585
if non_tail_bundle_hashes:
106-
for txn in find_transaction_objects(
107-
adapter=adapter,
86+
for txn in FindTransactionObjectsCommand(adapter=adapter)(
10887
bundles=list(non_tail_bundle_hashes),
109-
):
88+
)['transactions']:
11089
if txn.is_tail:
11190
if txn.hash not in tail_transaction_hashes:
11291
all_transactions.append(txn)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
# coding=utf-8
2+
from __future__ import absolute_import, division, print_function, \
3+
unicode_literals
4+
5+
from unittest import TestCase
6+
7+
import mock
8+
9+
from iota import Iota, MockAdapter, Transaction
10+
from iota.commands.extended import FindTransactionObjectsCommand
11+
12+
13+
class FindTransactionObjectsCommandTestCase(TestCase):
14+
# noinspection SpellCheckingInspection
15+
def setUp(self):
16+
super(FindTransactionObjectsCommandTestCase, self).setUp()
17+
18+
self.adapter = MockAdapter()
19+
self.command = FindTransactionObjectsCommand(self.adapter)
20+
21+
# Define values that we can reuse across tests.
22+
self.address = 'A' * 81
23+
self.transaction_hash = \
24+
b'BROTOVRCAEMFLRWGPVWDPDTBRAMLHVCHQDEHXLCWH' \
25+
b'KKXLVDFCPIJEUZTPPFMPQQ9KOHAEUAMMVJN99999'
26+
self.trytes = \
27+
b'99999999999999999999999999999999999999999999999999999999999999' \
28+
b'99999999999999999999999999999999999999999999999999999999999999' \
29+
b'99999999999999999999999999999999999999999999999999999999999999' \
30+
b'99999999999999999999999999999999999999999999999999999999999999' \
31+
b'99999999999999999999999999999999999999999999999999999999999999' \
32+
b'99999999999999999999999999999999999999999999999999999999999999' \
33+
b'99999999999999999999999999999999999999999999999999999999999999' \
34+
b'99999999999999999999999999999999999999999999999999999999999999' \
35+
b'99999999999999999999999999999999999999999999999999999999999999' \
36+
b'99999999999999999999999999999999999999999999999999999999999999' \
37+
b'99999999999999999999999999999999999999999999999999999999999999' \
38+
b'99999999999999999999999999999999999999999999999999999999999999' \
39+
b'99999999999999999999999999999999999999999999999999999999999999' \
40+
b'99999999999999999999999999999999999999999999999999999999999999' \
41+
b'99999999999999999999999999999999999999999999999999999999999999' \
42+
b'99999999999999999999999999999999999999999999999999999999999999' \
43+
b'99999999999999999999999999999999999999999999999999999999999999' \
44+
b'99999999999999999999999999999999999999999999999999999999999999' \
45+
b'99999999999999999999999999999999999999999999999999999999999999' \
46+
b'99999999999999999999999999999999999999999999999999999999999999' \
47+
b'99999999999999999999999999999999999999999999999999999999999999' \
48+
b'99999999999999999999999999999999999999999999999999999999999999' \
49+
b'99999999999999999999999999999999999999999999999999999999999999' \
50+
b'99999999999999999999999999999999999999999999999999999999999999' \
51+
b'99999999999999999999999999999999999999999999999999999999999999' \
52+
b'99999999999999999999999999999999999999999999999999999999999999' \
53+
b'99999999999999999999999999999999999999999999999999999999999999' \
54+
b'99999999999999999999999999999999999999999999999999999999999999' \
55+
b'99999999999999999999999999999999999999999999999999999999999999' \
56+
b'99999999999999999999999999999999999999999999999999999999999999' \
57+
b'99999999999999999999999999999999999999999999999999999999999999' \
58+
b'99999999999999999999999999999999999999999999999999999999999999' \
59+
b'99999999999999999999999999999999999999999999999999999999999999' \
60+
b'99999999999999999999999999999999999999999999999999999999999999' \
61+
b'99999999999999999999999999999999999999999999999999999999999999' \
62+
b'99999999999999999AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' \
63+
b'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA99999999999999999999999999' \
64+
b'9QC9999999999999999999999999PQYJHAD99999999999999999999WHIUDFV' \
65+
b'IFXNBJVEHYPLDADIDINGAWMHYIJNPYUDWXCAWL9GSKTUIZLJGGFIXEIYTJEDQZ' \
66+
b'TIYRXHC9PBWBDSOTEJTQTYYSZLVTFLDQMZSGLHKLYVJOLMXIJJRTGS9RYBXLAT' \
67+
b'ZJXBVBCPUGWRUKZJYLBGPKRKWIA9999FPYHMFFWMMKOHTSAPMMATZQLWXJSPMT' \
68+
b'JSRQIPMDCQXFFMXMHCYDKVJCFSRECAVALCOFIYCJLNRZZZ9999999999999999' \
69+
b'999999999999999KITCXNZOF999999999MMMMMMMMMEA9999F9999999999999' \
70+
b'9999999'
71+
72+
def test_wireup(self):
73+
"""
74+
Verify that the command is wired up correctly.
75+
"""
76+
self.assertIsInstance(
77+
Iota(self.adapter).findTransactionObjects,
78+
FindTransactionObjectsCommand,
79+
)
80+
81+
def test_transaction_found(self):
82+
"""
83+
A transaction is found with the inputs. A transaction object is
84+
returned
85+
"""
86+
with mock.patch(
87+
'iota.commands.core.find_transactions.FindTransactionsCommand.'
88+
'_execute',
89+
mock.Mock(return_value={'hashes': [self.transaction_hash, ]}),
90+
):
91+
with mock.patch(
92+
'iota.commands.core.get_trytes.GetTrytesCommand._execute',
93+
mock.Mock(return_value={'trytes': [self.trytes, ]}),
94+
):
95+
response = self.command(addresses=[self.address])
96+
97+
self.assertEqual(len(response['transactions']), 1)
98+
transaction = response['transactions'][0]
99+
self.assertIsInstance(transaction, Transaction)
100+
self.assertEqual(transaction.address, self.address)
101+
102+
def test_no_transactions_fround(self):
103+
"""
104+
No transaction is found with the inputs. An empty list is returned
105+
"""
106+
with mock.patch(
107+
'iota.commands.core.find_transactions.FindTransactionsCommand.'
108+
'_execute',
109+
mock.Mock(return_value={'hashes': []}),
110+
):
111+
response = self.command(addresses=[self.address])
112+
113+
self.assertDictEqual(
114+
response,
115+
{
116+
'transactions': [],
117+
},
118+
)

0 commit comments

Comments
 (0)