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

Commit 8e0a18a

Browse files
committed
add check_consistency api
1 parent ac6167d commit 8e0a18a

File tree

4 files changed

+314
-0
lines changed

4 files changed

+314
-0
lines changed

iota/api.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,34 @@ def broadcast_transactions(self, trytes):
195195
"""
196196
return core.BroadcastTransactionsCommand(self.adapter)(trytes=trytes)
197197

198+
def check_consistency(self, tails):
199+
# type: (Iterable[TransactionHash]) -> dict
200+
"""
201+
Used to ensure tail resolves to a consistent ledger which is necessary to
202+
validate before attempting promotionChecks transaction hashes for
203+
promotability.
204+
205+
This is called with a pending transaction (or more of them) and it will
206+
tell you if it is still possible for this transaction (or all the
207+
transactions simultaneously if you give more than one) to be confirmed, or
208+
not (because it conflicts with another already confirmed transaction).
209+
210+
:param tails:
211+
Transaction hashes. Must be tail transactions.
212+
213+
:return:
214+
Dict containing the following::
215+
{
216+
'state': bool,
217+
218+
'info': str,
219+
This field will only exist set if `state` is False.
220+
}
221+
"""
222+
return core.CheckConsistencyCommand(self.adapter)(
223+
tails = tails,
224+
)
225+
198226
def find_transactions(
199227
self,
200228
bundles = None,

iota/commands/core/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from .add_neighbors import *
1414
from .attach_to_tangle import *
1515
from .broadcast_transactions import *
16+
from .check_consistency import *
1617
from .find_transactions import *
1718
from .get_balances import *
1819
from .get_inclusion_states import *
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# coding=utf-8
2+
from __future__ import absolute_import, division, print_function, \
3+
unicode_literals
4+
5+
import filters as f
6+
from iota import Transaction, TransactionHash
7+
from iota.commands import FilterCommand, RequestFilter
8+
from iota.filters import Trytes
9+
10+
__all__ = [
11+
'CheckConsistencyCommand',
12+
]
13+
14+
15+
class CheckConsistencyCommand(FilterCommand):
16+
"""
17+
Executes ``checkConsistency`` extended API command.
18+
19+
See :py:meth:`iota.api.Iota.check_consistency` for more info.
20+
"""
21+
command = 'checkConsistency'
22+
23+
def get_request_filter(self):
24+
return CheckConsistencyRequestFilter()
25+
26+
def get_response_filter(self):
27+
pass
28+
29+
30+
class CheckConsistencyRequestFilter(RequestFilter):
31+
def __init__(self):
32+
super(CheckConsistencyRequestFilter, self).__init__({
33+
'tails': (
34+
f.Required
35+
| f.Array
36+
| f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash))
37+
),
38+
})
Lines changed: 247 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,247 @@
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 filters as f
8+
from filters.test import BaseFilterTestCase
9+
10+
from iota import Iota, TransactionHash, TryteString
11+
from iota.adapter import MockAdapter
12+
from iota.commands.core.check_consistency import CheckConsistencyCommand
13+
from iota.filters import Trytes
14+
15+
16+
class CheckConsistencyRequestFilterTestCase(BaseFilterTestCase):
17+
filter_type = CheckConsistencyCommand(MockAdapter()).get_request_filter
18+
skip_value_check = True
19+
20+
# noinspection SpellCheckingInspection
21+
def setUp(self):
22+
super(CheckConsistencyRequestFilterTestCase, self).setUp()
23+
24+
self.hash1 = (
25+
'TESTVALUE9DONTUSEINPRODUCTION99999DXSCAD'
26+
'YBVDCTTBLHFYQATFZPYPCBG9FOUKIGMYIGLHM9NEZ'
27+
)
28+
29+
self.hash2 = (
30+
'TESTVALUE9DONTUSEINPRODUCTION99999EMFYSM'
31+
'HWODIAPUTTFDLQRLYIDAUIPJXXEXZZSBVKZEBWGAN'
32+
)
33+
34+
def test_pass_happy_path(self):
35+
"""
36+
Request is valid.
37+
"""
38+
request = {
39+
# Raw trytes are extracted to match the IRI's JSON protocol.
40+
'tails': [self.hash1, self.hash2],
41+
}
42+
43+
filter_ = self._filter(request)
44+
45+
self.assertFilterPasses(filter_)
46+
self.assertDictEqual(filter_.cleaned_data, request)
47+
48+
def test_pass_compatible_types(self):
49+
"""
50+
Request contains values that can be converted to the expected
51+
types.
52+
"""
53+
filter_ = self._filter({
54+
'tails': [
55+
# Any TrytesCompatible value can be used here.
56+
TransactionHash(self.hash1),
57+
bytearray(self.hash2.encode('ascii')),
58+
],
59+
})
60+
61+
self.assertFilterPasses(filter_)
62+
self.assertDictEqual(
63+
filter_.cleaned_data,
64+
65+
{
66+
# Raw trytes are extracted to match the IRI's JSON protocol.
67+
'tails': [self.hash1, self.hash2],
68+
},
69+
)
70+
71+
def test_fail_empty(self):
72+
"""
73+
Request is empty.
74+
"""
75+
self.assertFilterErrors(
76+
{},
77+
78+
{
79+
'tails': [f.FilterMapper.CODE_MISSING_KEY],
80+
},
81+
)
82+
83+
def test_fail_unexpected_parameters(self):
84+
"""
85+
Request contains unexpected parameters.
86+
"""
87+
self.assertFilterErrors(
88+
{
89+
'tails': [TransactionHash(self.hash1)],
90+
'foo': 'bar',
91+
},
92+
93+
{
94+
'foo': [f.FilterMapper.CODE_EXTRA_KEY],
95+
},
96+
)
97+
98+
def test_fail_tails_null(self):
99+
"""
100+
``tails`` is null.
101+
"""
102+
self.assertFilterErrors(
103+
{
104+
'tails': None,
105+
},
106+
107+
{
108+
'tails': [f.Required.CODE_EMPTY],
109+
},
110+
)
111+
112+
def test_fail_tails_wrong_type(self):
113+
"""
114+
``tails`` is not an array.
115+
"""
116+
self.assertFilterErrors(
117+
{
118+
# It's gotta be an array, even if there's only one hash.
119+
'tails': TransactionHash(self.hash1),
120+
},
121+
122+
{
123+
'tails': [f.Type.CODE_WRONG_TYPE],
124+
},
125+
)
126+
127+
def test_fail_tails_empty(self):
128+
"""
129+
``tails`` is an array, but it is empty.
130+
"""
131+
self.assertFilterErrors(
132+
{
133+
'tails': [],
134+
},
135+
136+
{
137+
'tails': [f.Required.CODE_EMPTY],
138+
},
139+
)
140+
141+
def test_fail_tails_contents_invalid(self):
142+
"""
143+
``tails`` is a non-empty array, but it contains invalid values.
144+
"""
145+
self.assertFilterErrors(
146+
{
147+
'tails': [
148+
b'',
149+
True,
150+
None,
151+
b'not valid trytes',
152+
153+
# This is actually valid; I just added it to make sure the
154+
# filter isn't cheating!
155+
TryteString(self.hash1),
156+
157+
2130706433,
158+
b'9' * 82,
159+
],
160+
},
161+
162+
{
163+
'tails.0': [f.Required.CODE_EMPTY],
164+
'tails.1': [f.Type.CODE_WRONG_TYPE],
165+
'tails.2': [f.Required.CODE_EMPTY],
166+
'tails.3': [Trytes.CODE_NOT_TRYTES],
167+
'tails.5': [f.Type.CODE_WRONG_TYPE],
168+
'tails.6': [Trytes.CODE_WRONG_FORMAT],
169+
},
170+
)
171+
172+
173+
class CheckConsistencyCommandTestCase(TestCase):
174+
# noinspection SpellCheckingInspection
175+
def setUp(self):
176+
super(CheckConsistencyCommandTestCase, self).setUp()
177+
178+
self.adapter = MockAdapter()
179+
self.command = CheckConsistencyCommand(self.adapter)
180+
181+
# Define some tryte sequences that we can re-use across tests.
182+
self.milestone =\
183+
TransactionHash(
184+
b'TESTVALUE9DONTUSEINPRODUCTION99999W9KDIH'
185+
b'BALAYAFCADIDU9HCXDKIXEYDNFRAKHN9IEIDZFWGJ'
186+
)
187+
188+
self.hash1 =\
189+
TransactionHash(
190+
b'TESTVALUE9DONTUSEINPRODUCTION99999TBPDM9'
191+
b'ADFAWCKCSFUALFGETFIFG9UHIEFE9AYESEHDUBDDF'
192+
)
193+
194+
self.hash2 =\
195+
TransactionHash(
196+
b'TESTVALUE9DONTUSEINPRODUCTION99999CIGCCF'
197+
b'KIUFZF9EP9YEYGQAIEXDTEAAUGAEWBBASHYCWBHDX'
198+
)
199+
200+
def test_wireup(self):
201+
"""
202+
Verify that the command is wired up correctly.
203+
"""
204+
self.assertIsInstance(
205+
Iota(self.adapter).checkConsistency,
206+
CheckConsistencyCommand,
207+
)
208+
209+
def test_happy_path(self):
210+
"""
211+
Successfully checking consistency.
212+
"""
213+
214+
self.adapter.seed_response('checkConsistency', {
215+
'state': True,
216+
})
217+
218+
response = self.command(tails=[self.hash1, self.hash2])
219+
220+
self.assertDictEqual(
221+
response,
222+
223+
{
224+
'state': True,
225+
}
226+
)
227+
228+
def test_info_with_false_state(self):
229+
"""
230+
`info` field exists when `state` is False.
231+
"""
232+
233+
self.adapter.seed_response('checkConsistency', {
234+
'state': False,
235+
'info': 'Additional information',
236+
})
237+
238+
response = self.command(tails=[self.hash1, self.hash2])
239+
240+
self.assertDictEqual(
241+
response,
242+
243+
{
244+
'state': False,
245+
'info': 'Additional information',
246+
}
247+
)

0 commit comments

Comments
 (0)