From b4d32cfe907eb7a4d08d86683b694ca0b5013b91 Mon Sep 17 00:00:00 2001 From: Ribhu Lahiri Date: Sun, 18 Mar 2018 14:29:31 -0700 Subject: [PATCH 01/31] Fixed typo in python api documentation --- iota/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/api.py b/iota/api.py index 349e535..931ffad 100644 --- a/iota/api.py +++ b/iota/api.py @@ -964,7 +964,7 @@ def is_reattachable(self, addresses): # type: (Iterable[Address]) -> dict """ This API function helps you to determine whether you should replay a - transaction or make a completely new transaction with a different seed. + transaction or make a new one (either with the same input, or a different one). What this function does, is it takes one or more input addresses (i.e. from spent transactions) as input and then checks whether any transactions with a value transferred are confirmed. If yes, it means that this input address has already been successfully used in a different From 3bbbefa3f3cd54fa7ef55d9c3516cac0071c82da Mon Sep 17 00:00:00 2001 From: "r.k" Date: Wed, 28 Mar 2018 15:01:42 +0200 Subject: [PATCH 02/31] Corrected parameter-name 'start' to 'index'. --- docs/addresses.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/addresses.rst b/docs/addresses.rst index 6571e7f..8669c54 100644 --- a/docs/addresses.rst +++ b/docs/addresses.rst @@ -44,17 +44,17 @@ Using the API addresses = gna_result['addresses'] # Generate 1 address, starting with index 42: - gna_result = api.get_new_addresses(start=42) + gna_result = api.get_new_addresses(index=42) addresses = gna_result['addresses'] # Find the first unused address, starting with index 86: - gna_result = api.get_new_addresses(start=86, count=None) + gna_result = api.get_new_addresses(index=86, count=None) addresses = gna_result['addresses'] To generate addresses using the API, invoke its ``get_new_addresses`` method, using the following parameters: -- ``start: int``: The starting index (defaults to 0). This can be used +- ``index: int``: The starting index (defaults to 0). This can be used to skip over addresses that have already been generated. - ``count: Optional[int]``: The number of addresses to generate (defaults to 1). @@ -80,13 +80,13 @@ Using AddressGenerator generator = AddressGenerator(b'SEED9GOES9HERE') # Generate a list of addresses: - addresses = generator.get_addresses(start=0, count=5) + addresses = generator.get_addresses(index=0, count=5) # Generate a list of addresses in reverse order: - addresses = generator.get_addresses(start=42, count=10, step=-1) + addresses = generator.get_addresses(index=42, count=10, step=-1) # Create an iterator, advancing 5 indices each iteration. - iterator = generator.create_iterator(start=86, step=5) + iterator = generator.create_iterator(index=86, step=5) for address in iterator: ... From 11433a6611142d029556c1764e584ab4b8c10b91 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Sat, 31 Mar 2018 18:02:15 +0300 Subject: [PATCH 03/31] security_level optional argument added to get_inputs --- iota/api.py | 8 +++- iota/commands/extended/get_inputs.py | 7 +++- test/commands/extended/get_inputs_test.py | 50 +++++++++++++++++++++++ 3 files changed, 62 insertions(+), 3 deletions(-) diff --git a/iota/api.py b/iota/api.py index 349e535..1ef68ac 100644 --- a/iota/api.py +++ b/iota/api.py @@ -554,7 +554,7 @@ def get_bundles(self, transaction): """ return extended.GetBundlesCommand(self.adapter)(transaction=transaction) - def get_inputs(self, start=0, stop=None, threshold=None): + def get_inputs(self, start=0, stop=None, threshold=None, security_level=None): # type: (int, Optional[int], Optional[int]) -> dict """ Gets all possible inputs of a seed and returns them with the total @@ -595,6 +595,11 @@ def get_inputs(self, start=0, stop=None, threshold=None): If ``threshold`` is ``None`` (default), this method will return **all** inputs in the specified key range. + :param security_level: + Number of iterations to use when generating new addresses (see get_new_addresses). + This value must be between 1 and 3, inclusive. + If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 + :return: Dict with the following structure:: @@ -629,6 +634,7 @@ def get_inputs(self, start=0, stop=None, threshold=None): start = start, stop = stop, threshold = threshold, + securityLevel=security_level ) def get_latest_inclusion(self, hashes): diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index 9897b55..a892e33 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -38,13 +38,14 @@ def _execute(self, request): seed = request['seed'] # type: Seed start = request['start'] # type: int threshold = request['threshold'] # type: Optional[int] + security_level = request['securityLevel'] # type: Optional[int] # Determine the addresses we will be scanning. if stop is None: addresses =\ [addy for addy, _ in iter_used_addresses(self.adapter, seed, start)] else: - addresses = AddressGenerator(seed).get_addresses(start, stop) + addresses = AddressGenerator(seed, security_level).get_addresses(start, stop) if addresses: # Load balances for the addresses that we generated. @@ -111,15 +112,17 @@ def __init__(self): 'stop': f.Type(int) | f.Min(0), 'start': f.Type(int) | f.Min(0) | f.Optional(0), 'threshold': f.Type(int) | f.Min(0), + 'securityLevel': f.Type(int) | f.Min(1) | f.Max(3) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), # These arguments are required. 'seed': f.Required | Trytes(result_type=Seed), }, - allow_missing_keys = { + allow_missing_keys={ 'stop', 'start', 'threshold', + 'securityLevel', } ) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 793f114..d92e025 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -38,6 +38,7 @@ def test_pass_happy_path(self): 'start': 0, 'stop': 10, 'threshold': 100, + "securityLevel": 3, } filter_ = self._filter(request) @@ -59,6 +60,7 @@ def test_pass_compatible_types(self): 'start': 42, 'stop': 86, 'threshold': 99, + "securityLevel": 3, }) self.assertFilterPasses(filter_) @@ -70,6 +72,7 @@ def test_pass_compatible_types(self): 'start': 42, 'stop': 86, 'threshold': 99, + "securityLevel": 3, }, ) @@ -90,6 +93,7 @@ def test_pass_optional_parameters_excluded(self): 'start': 0, 'stop': None, 'threshold': None, + "securityLevel": 2, } ) @@ -351,6 +355,51 @@ def test_fail_threshold_too_small(self): }, ) + def test_fail_security_level_too_small(self): + """ + ``securityLevel`` is < 1. + """ + self.assertFilterErrors( + { + 'securityLevel': 0, + 'seed': Seed(self.seed), + }, + + { + 'securityLevel': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_security_level_too_big(self): + """ + ``securityLevel`` is > 3. + """ + self.assertFilterErrors( + { + 'securityLevel': 4, + 'seed': Seed(self.seed), + }, + + { + 'securityLevel': [f.Max.CODE_TOO_BIG], + }, + ) + + def test_fail_security_level_wrong_type(self): + """ + ``securityLevel`` is not an int. + """ + self.assertFilterErrors( + { + 'securityLevel': '2', + 'seed': Seed(self.seed), + }, + + { + 'securityLevel': [f.Type.CODE_WRONG_TYPE], + }, + ) + class GetInputsCommandTestCase(TestCase): # noinspection SpellCheckingInspection @@ -818,3 +867,4 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input0, self.addy1) self.assertEqual(input0.balance, 86) self.assertEqual(input0.key_index, 1) + From f3e2fb49588465e72eb3379d2f59f30310ab1800 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Mon, 2 Apr 2018 11:01:58 +0300 Subject: [PATCH 04/31] requested change in test_pass_optional_parameters_excluded --- test/commands/extended/get_inputs_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index d92e025..716d54e 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -9,8 +9,8 @@ from iota import Address, BadApiResponse, Iota, TransactionHash from iota.adapter import MockAdapter -from iota.commands.extended.get_inputs import GetInputsCommand, \ - GetInputsRequestFilter +from iota.commands.extended.get_inputs import GetInputsCommand, GetInputsRequestFilter +from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes from test import mock @@ -93,7 +93,7 @@ def test_pass_optional_parameters_excluded(self): 'start': 0, 'stop': None, 'threshold': None, - "securityLevel": 2, + "securityLevel": AddressGenerator.DEFAULT_SECURITY_LEVEL, } ) From 0916be0e36d5e84ac6549c42ed5ac01f42430936 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Mon, 2 Apr 2018 11:20:30 +0300 Subject: [PATCH 05/31] requested change: optional security_level in iter_used_addresses and in get_inputs --- iota/commands/extended/get_inputs.py | 2 +- iota/commands/extended/utils.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index a892e33..bd7042e 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -43,7 +43,7 @@ def _execute(self, request): # Determine the addresses we will be scanning. if stop is None: addresses =\ - [addy for addy, _ in iter_used_addresses(self.adapter, seed, start)] + [addy for addy, _ in iter_used_addresses(self.adapter, seed, start, security_level=security_level)] else: addresses = AddressGenerator(seed, security_level).get_addresses(start, stop) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index 1504407..060afac 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -37,7 +37,7 @@ def find_transaction_objects(adapter, **kwargs): return [] -def iter_used_addresses(adapter, seed, start): +def iter_used_addresses(adapter, seed, start, security_level=None): # type: (BaseAdapter, Seed, int) -> Generator[Tuple[Address, List[TransactionHash]]] """ Scans the Tangle for used addresses. @@ -45,9 +45,12 @@ def iter_used_addresses(adapter, seed, start): This is basically the opposite of invoking ``getNewAddresses`` with ``stop=None``. """ + if security_level is None: + security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL + ft_command = FindTransactionsCommand(adapter) - for addy in AddressGenerator(seed).create_iterator(start): + for addy in AddressGenerator(seed, security_level=security_level).create_iterator(start): ft_response = ft_command(addresses=[addy]) if ft_response['hashes']: From 580793c700090cbd0205c14c62300ff0cdb64f38 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Tue, 3 Apr 2018 12:18:01 +0300 Subject: [PATCH 06/31] security_level test for get_inputs command --- test/commands/extended/get_inputs_test.py | 39 +++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 716d54e..5e7e291 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -868,3 +868,42 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input0.balance, 86) self.assertEqual(input0.key_index, 1) + def test_security_level(self): + """ + Testing if it is working with all three security_levels + """ + def invoke_cmd(seed, securityLevel): + self.adapter.seed_response('getBalances', { + 'balances': [86], + }) + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' + b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' + ), + ], + }) + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + return GetInputsCommand(self.adapter)( + seed=seed, + securityLevel=securityLevel, + ) + + seed = "TESTSEED99999DONT9USE9IT" + addrs ={ + 1: Address(b"XKWESBIIE9KHL9V9QZOWSEZSSWFITGXVYWQUNSUR9XMEDNMLSSZ9OCTGTEZLLYIDWIYSIJFFETZYQPJDX"), + 2: Address(b"DNISWI9BUURXYBPTOKLMOGI9ALCRMWHLQGYBNZAMN9REGWZBKFPX99CRCFCWKPHEVFRBFNREJYOWBGVXX"), + 3: Address(b"UTCDSWGXUXHYJFPECRBURCLNHVHRZTQPPERHGZDXQQTTYEHIMFCEMUSZEQT9HKGK9EVBZEDAORKDRLE9W") + } + for securityLevel in [1, 2, 3]: + response = invoke_cmd(seed, securityLevel) + self.assertEqual(response['totalBalance'], 86) + self.assertEqual(len(response['inputs']), 1) + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + self.assertEqual(input0, addrs[securityLevel]) + self.assertEqual(input0.balance, 86) + self.assertEqual(input0.key_index, 0) From 5e29a86c9efe07c07666686feba0df9c17b93060 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Tue, 3 Apr 2018 12:46:25 +0300 Subject: [PATCH 07/31] security_level for get_inputs_test: improvement to cover both banches with and without stop --- test/commands/extended/get_inputs_test.py | 41 ++++++++++++++++------- 1 file changed, 28 insertions(+), 13 deletions(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 5e7e291..6562774 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -871,11 +871,15 @@ def mock_address_generator(ag, start, step=1): def test_security_level(self): """ Testing if it is working with all three security_levels + and with stop and withoutto cover both branches in the command """ - def invoke_cmd(seed, securityLevel): + def invoke_cmd(seed, stopYN, securityLevel): self.adapter.seed_response('getBalances', { 'balances': [86], }) + # ``getInputs`` uses ``findTransactions`` to identify unused + # addresses. + # noinspection SpellCheckingInspection self.adapter.seed_response('findTransactions', { 'hashes': [ TransactionHash( @@ -887,23 +891,34 @@ def invoke_cmd(seed, securityLevel): self.adapter.seed_response('findTransactions', { 'hashes': [], }) - return GetInputsCommand(self.adapter)( - seed=seed, - securityLevel=securityLevel, - ) + if stopYN: + ret = GetInputsCommand(self.adapter)( + seed=seed, + stop=1, + securityLevel=securityLevel, + ) + else: + ret = GetInputsCommand(self.adapter)( + seed=seed, + securityLevel=securityLevel, + ) + return ret seed = "TESTSEED99999DONT9USE9IT" + # one address with index 0 for each security level for the seed. + # to check with respective outputs from command addrs ={ 1: Address(b"XKWESBIIE9KHL9V9QZOWSEZSSWFITGXVYWQUNSUR9XMEDNMLSSZ9OCTGTEZLLYIDWIYSIJFFETZYQPJDX"), 2: Address(b"DNISWI9BUURXYBPTOKLMOGI9ALCRMWHLQGYBNZAMN9REGWZBKFPX99CRCFCWKPHEVFRBFNREJYOWBGVXX"), 3: Address(b"UTCDSWGXUXHYJFPECRBURCLNHVHRZTQPPERHGZDXQQTTYEHIMFCEMUSZEQT9HKGK9EVBZEDAORKDRLE9W") } for securityLevel in [1, 2, 3]: - response = invoke_cmd(seed, securityLevel) - self.assertEqual(response['totalBalance'], 86) - self.assertEqual(len(response['inputs']), 1) - input0 = response['inputs'][0] - self.assertIsInstance(input0, Address) - self.assertEqual(input0, addrs[securityLevel]) - self.assertEqual(input0.balance, 86) - self.assertEqual(input0.key_index, 0) + for stop in [0, 1]: + response = invoke_cmd(seed, stop, securityLevel) + self.assertEqual(response['totalBalance'], 86) + self.assertEqual(len(response['inputs']), 1) + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + self.assertEqual(input0, addrs[securityLevel]) + self.assertEqual(input0.balance, 86) + self.assertEqual(input0.key_index, 0) From 2d21a7aaad72754488ea12c8ddb46cc38540cc2d Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Tue, 3 Apr 2018 12:47:18 +0300 Subject: [PATCH 08/31] security_level for get_inputs_test: improvement to cover both banches with and without stop --- test/commands/extended/get_inputs_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 6562774..5af796f 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -913,7 +913,7 @@ def invoke_cmd(seed, stopYN, securityLevel): 3: Address(b"UTCDSWGXUXHYJFPECRBURCLNHVHRZTQPPERHGZDXQQTTYEHIMFCEMUSZEQT9HKGK9EVBZEDAORKDRLE9W") } for securityLevel in [1, 2, 3]: - for stop in [0, 1]: + for stop in [True, False]: response = invoke_cmd(seed, stop, securityLevel) self.assertEqual(response['totalBalance'], 86) self.assertEqual(len(response['inputs']), 1) From 019543996c1eff4a1ef36d069097dec6249dec3f Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 10:29:32 +0300 Subject: [PATCH 09/31] type hint in iter_used_addresses --- iota/commands/extended/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index 060afac..b07af60 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -38,7 +38,7 @@ def find_transaction_objects(adapter, **kwargs): def iter_used_addresses(adapter, seed, start, security_level=None): - # type: (BaseAdapter, Seed, int) -> Generator[Tuple[Address, List[TransactionHash]]] + # type: (BaseAdapter, Seed, int, int) -> Generator[Tuple[Address, List[TransactionHash]]] """ Scans the Tangle for used addresses. From 7a2cb574666afe5db97e3630fa0d26c2c21b0fff Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 10:30:20 +0300 Subject: [PATCH 10/31] random seed for get_inputs_test.test_security_level --- test/commands/extended/get_inputs_test.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 5af796f..da775bf 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -870,8 +870,8 @@ def mock_address_generator(ag, start, step=1): def test_security_level(self): """ - Testing if it is working with all three security_levels - and with stop and withoutto cover both branches in the command + Testing GetInputsCoommand with non default security_levels + with and without `stop` parameter to cover both branches in the command """ def invoke_cmd(seed, stopYN, securityLevel): self.adapter.seed_response('getBalances', { @@ -904,15 +904,13 @@ def invoke_cmd(seed, stopYN, securityLevel): ) return ret - seed = "TESTSEED99999DONT9USE9IT" - # one address with index 0 for each security level for the seed. + # one address with index 0 for nondefault security levels for the random seed. # to check with respective outputs from command - addrs ={ - 1: Address(b"XKWESBIIE9KHL9V9QZOWSEZSSWFITGXVYWQUNSUR9XMEDNMLSSZ9OCTGTEZLLYIDWIYSIJFFETZYQPJDX"), - 2: Address(b"DNISWI9BUURXYBPTOKLMOGI9ALCRMWHLQGYBNZAMN9REGWZBKFPX99CRCFCWKPHEVFRBFNREJYOWBGVXX"), - 3: Address(b"UTCDSWGXUXHYJFPECRBURCLNHVHRZTQPPERHGZDXQQTTYEHIMFCEMUSZEQT9HKGK9EVBZEDAORKDRLE9W") - } - for securityLevel in [1, 2, 3]: + seed = Seed.random() + security_levels_to_test = [l for l in [1, 2, 3] if l != AddressGenerator.DEFAULT_SECURITY_LEVEL] # nondefault + addrs = {l: AddressGenerator(seed, l).get_addresses(0)[0] for l in security_levels_to_test} + + for securityLevel in security_levels_to_test: for stop in [True, False]: response = invoke_cmd(seed, stop, securityLevel) self.assertEqual(response['totalBalance'], 86) From 0ec63d1ff127fecf3e894ca04e334e46a8e98f6b Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 10:38:05 +0300 Subject: [PATCH 11/31] random seed for get_inputs_test.test_security_level --- test/commands/extended/get_inputs_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index da775bf..b82522a 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -870,7 +870,7 @@ def mock_address_generator(ag, start, step=1): def test_security_level(self): """ - Testing GetInputsCoommand with non default security_levels + Testing GetInputsCoommand with selected security_levels with and without `stop` parameter to cover both branches in the command """ def invoke_cmd(seed, stopYN, securityLevel): @@ -904,10 +904,10 @@ def invoke_cmd(seed, stopYN, securityLevel): ) return ret - # one address with index 0 for nondefault security levels for the random seed. + # one address with index 0 for selected security levels for the random seed. # to check with respective outputs from command seed = Seed.random() - security_levels_to_test = [l for l in [1, 2, 3] if l != AddressGenerator.DEFAULT_SECURITY_LEVEL] # nondefault + security_levels_to_test = [1, 2] # at least one is non-default addrs = {l: AddressGenerator(seed, l).get_addresses(0)[0] for l in security_levels_to_test} for securityLevel in security_levels_to_test: From 15a96deabeac9ff69f90e7d3f5f3353a0051ae86 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 10:43:23 +0300 Subject: [PATCH 12/31] type hint edit --- iota/commands/extended/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index b07af60..3fd22ca 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -38,7 +38,7 @@ def find_transaction_objects(adapter, **kwargs): def iter_used_addresses(adapter, seed, start, security_level=None): - # type: (BaseAdapter, Seed, int, int) -> Generator[Tuple[Address, List[TransactionHash]]] + # type: (BaseAdapter, Seed, int, Optional[int]) -> Generator[Tuple[Address, List[TransactionHash]]] """ Scans the Tangle for used addresses. From 9fddc9636cc9da892fe8e2c5b2296a21780748fc Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 12:24:05 +0300 Subject: [PATCH 13/31] PrepareTransferCommand added `security_level` param. Test modified accordingly. TODO: add specific test --- iota/api.py | 12 +++++++++--- iota/commands/extended/prepare_transfer.py | 6 ++++++ test/commands/extended/prepare_transfer_test.py | 5 ++++- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/iota/api.py b/iota/api.py index 1ef68ac..3c7cd80 100644 --- a/iota/api.py +++ b/iota/api.py @@ -555,7 +555,7 @@ def get_bundles(self, transaction): return extended.GetBundlesCommand(self.adapter)(transaction=transaction) def get_inputs(self, start=0, stop=None, threshold=None, security_level=None): - # type: (int, Optional[int], Optional[int]) -> dict + # type: (int, Optional[int], Optional[int], Optional[int]) -> dict """ Gets all possible inputs of a seed and returns them with the total balance. @@ -753,8 +753,8 @@ def get_transfers(self, start=0, stop=None, inclusion_states=False): inclusionStates = inclusion_states, ) - def prepare_transfer(self, transfers, inputs=None, change_address=None): - # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address]) -> dict + def prepare_transfer(self, transfers, inputs=None, change_address=None, security_level=None): + # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], Optional[int]) -> dict """ Prepares transactions to be broadcast to the Tangle, by generating the correct bundle, as well as choosing and signing the inputs (for @@ -778,6 +778,11 @@ def prepare_transfer(self, transfers, inputs=None, change_address=None): If not specified, a change address will be generated automatically. + :param security_level: + Number of iterations to use when generating new addresses (see get_new_addresses). + This value must be between 1 and 3, inclusive. + If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 + :return: Dict containing the following values:: @@ -795,6 +800,7 @@ def prepare_transfer(self, transfers, inputs=None, change_address=None): transfers = transfers, inputs = inputs, changeAddress = change_address, + securityLevel = security_level, ) def promote_transaction( diff --git a/iota/commands/extended/prepare_transfer.py b/iota/commands/extended/prepare_transfer.py index cab9c7c..30ad84d 100644 --- a/iota/commands/extended/prepare_transfer.py +++ b/iota/commands/extended/prepare_transfer.py @@ -11,6 +11,7 @@ from iota.commands.core.get_balances import GetBalancesCommand from iota.commands.extended.get_inputs import GetInputsCommand from iota.commands.extended.get_new_addresses import GetNewAddressesCommand +from iota.crypto.addresses import AddressGenerator from iota.crypto.signing import KeyGenerator from iota.crypto.types import Seed from iota.exceptions import with_context @@ -43,6 +44,7 @@ def _execute(self, request): # Optional parameters. change_address = request.get('changeAddress') # type: Optional[Address] proposed_inputs = request.get('inputs') # type: Optional[List[Address]] + security_level = request['securityLevel'] # type: Optional[int] want_to_spend = bundle.balance if want_to_spend > 0: @@ -52,6 +54,7 @@ def _execute(self, request): gi_response = GetInputsCommand(self.adapter)( seed = seed, threshold = want_to_spend, + securityLevel=security_level, ) confirmed_inputs = gi_response['inputs'] @@ -130,6 +133,8 @@ def __init__(self): # Optional parameters. 'changeAddress': Trytes(result_type=Address), + 'securityLevel': f.Type(int) | f.Min(1) | f.Max(3) | f.Optional( + default=AddressGenerator.DEFAULT_SECURITY_LEVEL), # Note that ``inputs`` is allowed to be an empty array. 'inputs': @@ -139,5 +144,6 @@ def __init__(self): allow_missing_keys = { 'changeAddress', 'inputs', + 'securityLevel', }, ) diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index f1de60a..de7b8c0 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -12,6 +12,7 @@ TryteString from iota.adapter import MockAdapter from iota.commands.extended.prepare_transfer import PrepareTransferCommand +from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import GeneratedAddress, Trytes from test import mock @@ -81,7 +82,7 @@ def test_pass_happy_path(self): Address(self.trytes3, key_index=3, security_level=2), Address(self.trytes4, key_index=4, security_level=2), ], - + 'securityLevel': 3 } filter_ = self._filter(request) @@ -123,6 +124,7 @@ def test_pass_compatible_types(self): Address(self.trytes3), Address(self.trytes4), ], + 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL }, ) @@ -146,6 +148,7 @@ def test_pass_optional_parameters_omitted(self): # These parameters are set to their default values. 'changeAddress': None, 'inputs': None, + "securityLevel": AddressGenerator.DEFAULT_SECURITY_LEVEL, }, ) From e97af92372b48d7f4db5bf9979a3a65da287e163 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 21:39:20 +0300 Subject: [PATCH 14/31] fixing a bug with incorrect use of ``stop`` in call to get_addresses. Covered with test_start_stop --- iota/commands/extended/get_inputs.py | 2 +- test/commands/extended/get_inputs_test.py | 46 +++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index bd7042e..f131f9a 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -45,7 +45,7 @@ def _execute(self, request): addresses =\ [addy for addy, _ in iter_used_addresses(self.adapter, seed, start, security_level=security_level)] else: - addresses = AddressGenerator(seed, security_level).get_addresses(start, stop) + addresses = AddressGenerator(seed, security_level).get_addresses(start, stop - start) if addresses: # Load balances for the addresses that we generated. diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index b82522a..0c44488 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -868,6 +868,52 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input0.balance, 86) self.assertEqual(input0.key_index, 1) + def test_start_stop(self): + """ + Using ``start`` and ``stop`` at once. + Checking if correct number of addresses is returned. Must be stop - start + """ + # mocking get_balances to get number of returned balances + # equal to number of addresses + # returns {"balances": [11] } for one address, {"balances": [11, 11]} for two etc + + def mock_get_balances_execute(adapter, request): + return dict(balances=[11] * len(request["addresses"])) + + # To keep the unit test nice and speedy, we will mock the address + # generator. We already have plenty of unit tests for that + # functionality, so we can get away with mocking it here. + # noinspection PyUnusedLocal + + def mock_address_generator(ag, start, step=1): + # returning up to 3 addresses, depending on stop value + for addy in [self.addy0, self.addy1, self.addy2][start::step]: + yield addy + + with mock.patch( + 'iota.crypto.addresses.AddressGenerator.create_iterator', + mock_address_generator, + ): + with mock.patch( + 'iota.commands.core.GetBalancesCommand._execute', + mock_get_balances_execute, + ): + response = self.command( + seed = Seed.random(), + start = 1, + stop = 2, + ) + + self.assertEqual(len(response['inputs']), 1) # 2 - 1 = 1 address expected + self.assertEqual(response['totalBalance'], 11) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + self.assertEqual(input0, self.addy1) + self.assertEqual(input0.balance, 11) + self.assertEqual(input0.key_index, 1) + + def test_security_level(self): """ Testing GetInputsCoommand with selected security_levels From 7654b3743b4478d00373db35195852baf75b1954 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 22:04:13 +0300 Subject: [PATCH 15/31] fixing bug with incorrect use of ``stop`` in get_inputs. Covered with test_start_stop --- test/commands/extended/get_inputs_test.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 0c44488..8e77964 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -901,17 +901,22 @@ def mock_address_generator(ag, start, step=1): response = self.command( seed = Seed.random(), start = 1, - stop = 2, + stop = 3, ) - self.assertEqual(len(response['inputs']), 1) # 2 - 1 = 1 address expected - self.assertEqual(response['totalBalance'], 11) + self.assertEqual(len(response['inputs']), 2) # 3 - 1 = 2 address expected + self.assertEqual(response['totalBalance'], 22) input0 = response['inputs'][0] + input1 = response['inputs'][1] self.assertIsInstance(input0, Address) + self.assertIsInstance(input1, Address) self.assertEqual(input0, self.addy1) + self.assertEqual(input1, self.addy2) self.assertEqual(input0.balance, 11) self.assertEqual(input0.key_index, 1) + self.assertEqual(input1.balance, 11) + self.assertEqual(input1.key_index, 2) def test_security_level(self): From 4b7732e0a39e3f44b48ab6d1c41402e93458e33b Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Wed, 4 Apr 2018 22:05:08 +0300 Subject: [PATCH 16/31] fixing bug with incorrect use of ``stop`` in get_inputs. Covered with test_start_stop --- test/commands/extended/get_inputs_test.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 8e77964..d4c4219 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -904,7 +904,7 @@ def mock_address_generator(ag, start, step=1): stop = 3, ) - self.assertEqual(len(response['inputs']), 2) # 3 - 1 = 2 address expected + self.assertEqual(len(response['inputs']), 2) # 3 - 1 = 2 addresses expected self.assertEqual(response['totalBalance'], 22) input0 = response['inputs'][0] From f655af2ed4d099d7589191195c96d31bfb174c91 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Thu, 5 Apr 2018 15:14:19 +0300 Subject: [PATCH 17/31] added `security_level` to `PrepareTransferCommand` with corresponding tests --- iota/commands/extended/prepare_transfer.py | 5 +- .../extended/prepare_transfer_test.py | 99 ++++++++++++++++++- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/iota/commands/extended/prepare_transfer.py b/iota/commands/extended/prepare_transfer.py index 30ad84d..aa22c75 100644 --- a/iota/commands/extended/prepare_transfer.py +++ b/iota/commands/extended/prepare_transfer.py @@ -102,7 +102,7 @@ def _execute(self, request): if bundle.balance < 0: if not change_address: change_address =\ - GetNewAddressesCommand(self.adapter)(seed=seed)['addresses'][0] + GetNewAddressesCommand(self.adapter)(seed=seed, securityLevel=security_level)['addresses'][0] bundle.send_unspent_inputs_to(change_address) @@ -133,8 +133,7 @@ def __init__(self): # Optional parameters. 'changeAddress': Trytes(result_type=Address), - 'securityLevel': f.Type(int) | f.Min(1) | f.Max(3) | f.Optional( - default=AddressGenerator.DEFAULT_SECURITY_LEVEL), + 'securityLevel': f.Choice([1, 2, 3]) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), # Note that ``inputs`` is allowed to be an empty array. 'inputs': diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index de7b8c0..7434efa 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -9,7 +9,7 @@ from six import binary_type, iterkeys from iota import Address, BadApiResponse, Iota, ProposedTransaction, Tag, \ - TryteString + TryteString, Transaction from iota.adapter import MockAdapter from iota.commands.extended.prepare_transfer import PrepareTransferCommand from iota.crypto.addresses import AddressGenerator @@ -109,6 +109,7 @@ def test_pass_compatible_types(self): # These still have to have the correct type, however. 'transfers': [self.transfer1, self.transfer2], + 'securityLevel': None }) self.assertFilterPasses(filter_) @@ -398,6 +399,28 @@ def test_fail_inputs_contents_invalid(self): }, ) + def test_fail_wrong_security_level(self): + """ + ``security_level`` is not one of integers 1, 2 or 3. + """ + self.assertFilterErrors( + { + # Must be an array, even if there's only one input. + 'inputs': None, + + 'seed': Seed(self.trytes1), + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + "securityLevel": 0, + }, + + { + 'securityLevel': [f.Choice.CODE_INVALID], + }, + ) + # noinspection SpellCheckingInspection class PrepareTransferCommandTestCase(TestCase): @@ -1219,3 +1242,77 @@ def test_pass_message_long(self): response['trytes'][2], TryteString('SGKETGDEEASG9GSGSFEASGZFSGAGSGTFSGSFTGVDSGSFEATGUDSGBGTGTDSGNFSGPFSGVFTGVDTGEETGUDTGHEEASGBGTGTDSGNFSGPFSGRFTGWDFAEASGZETGDESG9GQAEASGZFTGDEEASGTFSGVFSGPFSGSFSGZFEASGPFEASGZFSGVFTGTDSGSFQAEASGXFSGAGTGVDSGAGTGTDTGDESGWFEASGVFSGZFSGSFSGSFTGVDEATGUDTGVDSGSFSG9GTGDESAEASGQEEATGFETGVDSGVFEATGUDTGVDSGSFSG9GTGDEEASGRFSGAGSGYFSGTFSG9GTGDEEASGOFTGDETGVDTGEEEASGAGTGYDTGTDSGNFSG9GTGHETGGETGVDEASGYFTGGESGRFSGVFEATGUDEASGAGTGTDTGWDSGTFSGVFSGSFSGZFSAEASGSETGVDSGAGEASGOFTGWDSGRFSGSFTGVDEATGFETGVDSGAGEASGRFSGSFSGYFSGNFTGVDTGEEIBEASGKETGDEIBEASGKETGDEQAEASGYFSGSFSGWFTGVDSGSFSG9GSGNFSG9GTGVDEAFCTCXCBDQCTCFDVCIBEASGAFEASGZFSGSFSG9GTGHEEASGSFTGUDTGVDTGEEEASGOFSGAGSGYFTGEETGAESGNFTGHEEASGAGTGVDSGPFSGSFTGVDTGUDTGVDSGPFSGSFSG9GSG9GSGAGTGUDTGVDTGEEQAEATG9ESGSFSGZFEASGPFTGDEEASGZFSGAGSGTFSGSFTGVDSGSFEASGBGSGAGSG9GTGHETGVDTGEESAEASG9FTGDEEASGBGSGYFSGNFTG9ESGSFTGAETGEEEASGZESGNFSG9GTGVDTGEETGHESGQFSGAGEASGVFEASGBGTGTDSGAGSGXFSGYFTGHESG9GSGVFEASGZFSGAGTGTDTGUDSGXFSGVFTGYDEASGBGSGSFTGYDSGAGTGVDSGVFSG9GTGZDSGSFSGPFSAEASGAFEASGPFSGNFTGUDEASGSFTGUDTGVDTGEEEATGVDSGNFSGXFSGAGSGWFEATGTDSGAGTGUDSGXFSGAGTGAESGVFSAEASGAFEASGPFSGNFTGUDEASGSFTGUDTGVDTGEEEATGTDSGAGTGUDSGXFSGAGTGAETGEEQAEASG9GSGSFEASGUFSG9GSGNFTGHEQAEATG9ETGVDSGAGEATGHEEASGUFSG9GSGNFTGGEDBEATG9ETGVDSGAGEATGUDSGZFSGSFTGTDTGVDTGEEEASGZESGNFSG9GTGVDTGEETGHESGQFSGAGQAEASGPFEATGVDSGAGEASGPFTGTDSGSFSGZFTGHEEASGXFSGNFSGXFEATGVDTGTDSGNFSGQFSGVFTG9ESGSFTGUDSGXFSGVFSGWFQAEASGPFSGSFTGTDSGAGTGHETGVDSG9GSGAGQAEATGUDSGBGSGNFTGUDEASGTFSGVFSGUFSG9GTGEESAEASGQEEASGZFSGAGSGSFEATGUDTGWDTGBESGSFTGUDTGVDSGPFSGAGSGPFSGNFSG9GSGVFSGSFQAEASGPFEATGVDSGAGEASGPFTGTDSGSFSGZFTGHEEASGXFSGNFSGXFEASGQFTGTDSGAGTGVDSGSFTGUDSGXFEASGVFEASG9GSGSFSGBGSGAGSG9GTGHETGVDSG9GTGDESGZFSGVFEASGRFSGYFTGHEEASGPFSGNFTGUDQAEATGUDSGBGSGNFTGUDSGNFSGSFTGVDEASGTFSGVFSGUFSG9GSGVFEASASASAEASGKETGDEEASG9GSGSFEATGYDSGAGTGVDSGVFTGVDSGSFEASGUFSG9GSGNFTGVDTGEEEASGBGTGTDSGNFSGPFSGRFTGWDSAEASGXESGAGTGVDSGAGSGZFTGWDEATG9ETGVDSGAGEASGPFEASGQFSGYFTGWDSGOFSGVFSG9GSGSFEASGRFTGWDTGAESGVFQAEASGPFEATGVDSGSFTGYDEASGZFSGSFTGUDTGVDSGNFTGYDQAEASGPFTGDEEASG9GSGSFEASGQFSGAGSGPFSGAGTGTDSGVFTGVDSGSFEASGAGEASG9GSGNFEASGPFSGSFTG9ESGSFTGTDSGVFSG9GSGXFSGNFTGYDQAEASGPFTGDEEATGYDSGAGTGVDSGVFTGVDSGSFEASGZFSGSTESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVAOWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999GFOTA9UNIT9TESTS99999999999NYBKIVD99999999999B99999999EKHBGESJFZXE9PY9UVFIPRHGGFKDFKQOQFKQAYISJOWCXIVBSGHOZGT9DZEQPPLTYHKTWBQZOFX9BEAID999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999999PYOTA9UNIT9TESTS99999999999999999999999999999999999999999999999999999999999999999'), ) + + def test_security_level(self): + """ + testing use of security_level when inputs are given and change address is not given. + """ + # will be sending SEND_VALUE. + # balances of input addresses returned by the mock will be equal to SEND_VALUE + security_level * 11 + # expected result of the command depends on security_level + # will be testing for at least two security levels + + SECURITY_LEVELS_TO_TEST = [1, 2] # at least one is non-default. With [1,2,3] it takes much longer + SEND_VALUE = 42 + + # creating fake addresses, one for each security_level. + seed = Seed.random() + mock_addresses = {} + for sl in SECURITY_LEVELS_TO_TEST: + mock_addresses[sl] = Address( + trytes=Address.random(81), + key_index=0, + security_level=sl + ) + + # mock get_balances returns balance, depending on security_level of mock addresses + def mock_get_balances_execute(adapter, request): + # returns balances of input addresses equal to SEND_VALUE + security_level * 11 + addr = request["addresses"][0] + security_level = [l for l, a in mock_addresses.items() if str(a) == addr][0] + return dict(balances=[SEND_VALUE + security_level * 11], milestone=None) + + # testing for several security levels + for security_level in SECURITY_LEVELS_TO_TEST: + + # get_new_addresses uses `find_transactions` internaly. + # The following means all addresses are considered unused + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + self.command.reset() + with mock.patch( + 'iota.commands.core.GetBalancesCommand._execute', + mock_get_balances_execute, + ): + response = \ + self.command( + seed=seed, + transfers=[ + ProposedTransaction( + address= + Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + value = SEND_VALUE, + ), + ], + inputs=[ + mock_addresses[security_level] + ], + securityLevel=security_level + ) + + self.assertEqual(set(iterkeys(response)), {'trytes'}) + + EXPECTED_NUMBER_OF_TX = 2 + security_level # signature requires as many transactions as security_level + EXPECTED_CHANGE_VALUE = security_level * 11 # what has left depends on security_level + + self.assertEqual(len(response['trytes']), EXPECTED_NUMBER_OF_TX) + + change_tx = Transaction.from_tryte_string(response['trytes'][0]) + self.assertEqual(change_tx.value, EXPECTED_CHANGE_VALUE) + + From ee564adfcdc6737bac752c37c14d736deda4f02f Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Thu, 5 Apr 2018 16:18:36 +0300 Subject: [PATCH 18/31] added `security_level` to `PrepareTransferCommand` with corresponding tests --- .../extended/prepare_transfer_test.py | 84 ++++++++++++++++++- 1 file changed, 82 insertions(+), 2 deletions(-) diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index 7434efa..2292dc5 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -9,7 +9,7 @@ from six import binary_type, iterkeys from iota import Address, BadApiResponse, Iota, ProposedTransaction, Tag, \ - TryteString, Transaction + TryteString, Transaction, TransactionHash from iota.adapter import MockAdapter from iota.commands.extended.prepare_transfer import PrepareTransferCommand from iota.crypto.addresses import AddressGenerator @@ -1276,7 +1276,7 @@ def mock_get_balances_execute(adapter, request): for security_level in SECURITY_LEVELS_TO_TEST: # get_new_addresses uses `find_transactions` internaly. - # The following means all addresses are considered unused + # The following means requested address is considered unused self.adapter.seed_response('findTransactions', { 'hashes': [], }) @@ -1316,3 +1316,83 @@ def mock_get_balances_execute(adapter, request): self.assertEqual(change_tx.value, EXPECTED_CHANGE_VALUE) + def test_security_level_no_inputs(self): + """ + testing use of security_level when neither inputs nor change address is given. + """ + # will be sending SEND_VALUE. + # balances of input addresses returned by the mock will be equal to SEND_VALUE + security_level * 11 + # expected result of the command depends on security_level + # will be testing for at least two security levels + + SECURITY_LEVELS_TO_TEST = [1, 2] # at least one is non-default. With [1,2,3] it takes much longer + SEND_VALUE = 42 + + # pre-generating addresses, one for each security_level. + # they will be generated again by GetInputs internally + seed = Seed.random() + addresses = {} + for sl in SECURITY_LEVELS_TO_TEST: + addresses[sl] = AddressGenerator(seed, security_level=sl).get_addresses(0, count=1)[0] + + # mock get_balances returns balance, depending on security_level of mock addresses + def mock_get_balances_execute(adapter, request): + # returns balances of input addresses equal to SEND_VALUE + security_level * 11 + addr = request["addresses"][0] + security_level = [l for l, a in addresses.items() if str(a) == addr][0] + return dict(balances=[SEND_VALUE + security_level * 11], milestone=None) + + # testing several security levels + for security_level in SECURITY_LEVELS_TO_TEST: + + # get_inputs use iter_used_addresses and findTransactions. + # until address without tx found + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' + b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' + ), + ], + }) + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + # get_new_addresses uses `find_transactions` internaly. + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + self.command.reset() + + with mock.patch( + 'iota.commands.core.GetBalancesCommand._execute', + mock_get_balances_execute, + ): + response = \ + self.command( + seed=seed, + transfers=[ + ProposedTransaction( + address= + Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + value=SEND_VALUE, + ), + ], + securityLevel=security_level + ) + + self.assertEqual(set(iterkeys(response)), {'trytes'}) + + EXPECTED_NUMBER_OF_TX = 2 + security_level # signature requires as many transactions as security_level + EXPECTED_CHANGE_VALUE = security_level * 11 # what has left depends on security_level + + self.assertEqual(len(response['trytes']), EXPECTED_NUMBER_OF_TX) + + change_tx = Transaction.from_tryte_string(response['trytes'][0]) + self.assertEqual(change_tx.value, EXPECTED_CHANGE_VALUE) + From 8c14f30fcf91010e9058dfa7a47b7e9653332ed7 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Thu, 5 Apr 2018 18:45:16 +0300 Subject: [PATCH 19/31] added `security_level` to `SendTransferCommand` with input validation tests --- iota/api.py | 7 + iota/commands/extended/send_transfer.py | 6 +- test/commands/extended/send_transfer_test.py | 132 +++++++++++++------ 3 files changed, 102 insertions(+), 43 deletions(-) diff --git a/iota/api.py b/iota/api.py index 3c7cd80..d5cc1c1 100644 --- a/iota/api.py +++ b/iota/api.py @@ -881,6 +881,7 @@ def send_transfer( inputs = None, change_address = None, min_weight_magnitude = None, + security_level = None, ): # type: (int, Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], Optional[int]) -> dict """ @@ -932,6 +933,7 @@ def send_transfer( inputs = inputs, changeAddress = change_address, minWeightMagnitude = min_weight_magnitude, + securityLevel = security_level, ) def send_trytes(self, trytes, depth, min_weight_magnitude=None): @@ -952,6 +954,11 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=None): If not provided, a default value will be used. + :param security_level: + Number of iterations to use when generating new addresses (see get_new_addresses). + This value must be between 1 and 3, inclusive. + If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 + :return: Dict containing the following values:: diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index 99a9f39..666cecd 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -10,6 +10,7 @@ from iota.commands.extended.prepare_transfer import PrepareTransferCommand from iota.commands.extended.send_trytes import SendTrytesCommand from iota.crypto.types import Seed +from iota.crypto.addresses import AddressGenerator from iota.filters import Trytes __all__ = [ @@ -39,12 +40,14 @@ def _execute(self, request): seed = request['seed'] # type: Seed transfers = request['transfers'] # type: List[ProposedTransaction] reference = request['reference'] # type: Optional[TransactionHash] + security_level = request['securityLevel'] # type: Optional[int] pt_response = PrepareTransferCommand(self.adapter)( changeAddress = change_address, inputs = inputs, seed = seed, transfers = transfers, + securityLevel = security_level, ) st_response = SendTrytesCommand(self.adapter)( @@ -79,7 +82,7 @@ def __init__(self): # Optional parameters. 'changeAddress': Trytes(result_type=Address), - + 'securityLevel': f.Choice([1, 2, 3]) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), # Note that ``inputs`` is allowed to be an empty array. 'inputs': @@ -92,5 +95,6 @@ def __init__(self): 'changeAddress', 'inputs', 'reference', + 'securityLevel', }, ) diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index 3853485..970110f 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -12,6 +12,7 @@ TransactionTrytes, TryteString, TransactionHash from iota.adapter import MockAdapter from iota.commands.extended.send_transfer import SendTransferCommand +from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes from test import mock @@ -89,6 +90,7 @@ def test_pass_happy_path(self): ], 'reference': TransactionHash(self.trytes1), + 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL, } filter_ = self._filter(request) @@ -120,6 +122,7 @@ def test_pass_compatible_types(self): 'depth': 100, 'minWeightMagnitude': 18, + 'securityLevel': None, }) self.assertFilterPasses(filter_) @@ -142,6 +145,7 @@ def test_pass_compatible_types(self): self.transfer1, self.transfer2 ], + 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL } ) @@ -177,6 +181,7 @@ def test_pass_optional_parameters_omitted(self): self.transfer1, self.transfer2 ], + 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL, } ) @@ -613,6 +618,49 @@ def test_fail_reference_not_trytes(self): }, ) + def test_fail_wrong_security_level(self): + """ + ``security_level`` is not one of integers 1, 2 or 3. + """ + self.assertFilterErrors( + { + 'depth': 100, + 'minWeightMagnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + + # Maybe he's not that smart; maybe he's like a worker bee who + # only knows how to push buttons or something. + 'securityLevel': 0, + }, + + { + 'securityLevel': [f.Choice.CODE_INVALID], + }, + ) + + def test_fail_wrong_security_level_type(self): + """ + ``security_level`` is not one of integers 1, 2 or 3. + """ + self.assertFilterErrors( + { + 'depth': 100, + 'minWeightMagnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + + # Maybe he's not that smart; maybe he's like a worker bee who + # only knows how to push buttons or something. + 'securityLevel': "2", + }, + + { + 'securityLevel': [f.Choice.CODE_INVALID], + }, + ) + + class SendTransferCommandTestCase(TestCase): def setUp(self): @@ -637,48 +685,48 @@ def test_happy_path(self): # noinspection SpellCheckingInspection transaction1 =\ TransactionTrytes( - b'GYPRVHBEZOOFXSHQBLCYW9ICTCISLHDBNMMVYD9JJHQMPQCTIQAQTJNNNJ9IDXLRCC' - b'OYOXYPCLR9PBEY9ORZIEPPDNTI9CQWYZUOTAVBXPSBOFEQAPFLWXSWUIUSJMSJIIIZ' - b'WIKIRH9GCOEVZFKNXEVCUCIIWZQCQEUVRZOCMEL9AMGXJNMLJCIA9UWGRPPHCEOPTS' - b'VPKPPPCMQXYBHMSODTWUOABPKWFFFQJHCBVYXLHEWPD9YUDFTGNCYAKQKVEZYRBQRB' - b'XIAUX9SVEDUKGMTWQIYXRGSWYRK9SRONVGTW9YGHSZRIXWGPCCUCDRMAXBPDFVHSRY' - b'WHGB9DQSQFQKSNICGPIPTRZINYRXQAFSWSEWIFRMSBMGTNYPRWFSOIIWWT9IDSELM9' - b'JUOOWFNCCSHUSMGNROBFJX9JQ9XT9PKEGQYQAWAFPRVRRVQPUQBHLSNTEFCDKBWRCD' - b'X9EYOBB9KPMTLNNQLADBDLZPRVBCKVCYQEOLARJYAGTBFR9QLPKZBOYWZQOVKCVYRG' - b'YI9ZEFIQRKYXLJBZJDBJDJVQZCGYQMROVHNDBLGNLQODPUXFNTADDVYNZJUVPGB9LV' - b'PJIYLAPBOEHPMRWUIAJXVQOEM9ROEYUOTNLXVVQEYRQWDTQGDLEYFIYNDPRAIXOZEB' - b'CS9P99AZTQQLKEILEVXMSHBIDHLXKUOMMNFKPYHONKEYDCHMUNTTNRYVMMEYHPGASP' - b'ZXASKRUPWQSHDMU9VPS99ZZ9SJJYFUJFFMFORBYDILBXCAVJDPDFHTTTIYOVGLRDYR' - b'TKHXJORJVYRPTDH9ZCPZ9ZADXZFRSFPIQKWLBRNTWJHXTOAUOL9FVGTUMMPYGYICJD' - b'XMOESEVDJWLMCVTJLPIEKBE9JTHDQWV9MRMEWFLPWGJFLUXI9BXPSVWCMUWLZSEWHB' - b'DZKXOLYNOZAPOYLQVZAQMOHGTTQEUAOVKVRRGAHNGPUEKHFVPVCOYSJAWHZU9DRROH' - b'BETBAFTATVAUGOEGCAYUXACLSSHHVYDHMDGJP9AUCLWLNTFEVGQGHQXSKEMVOVSKQE' - b'EWHWZUDTYOBGCURRZSJZLFVQQAAYQO9TRLFFN9HTDQXBSPPJYXMNGLLBHOMNVXNOWE' - b'IDMJVCLLDFHBDONQJCJVLBLCSMDOUQCKKCQJMGTSTHBXPXAMLMSXRIPUBMBAWBFNLH' - b'LUJTRJLDERLZFUBUSMF999XNHLEEXEENQJNOFFPNPQ9PQICHSATPLZVMVIWLRTKYPI' - b'XNFGYWOJSQDAXGFHKZPFLPXQEHCYEAGTIWIJEZTAVLNUMAFWGGLXMBNUQTOFCNLJTC' - b'DMWVVZGVBSEBCPFSM99FLOIDTCLUGPSEDLOKZUAEVBLWNMODGZBWOVQT9DPFOTSKRA' - b'BQAVOQ9RXWBMAKFYNDCZOJGTCIDMQSQQSODKDXTPFLNOKSIZEOY9HFUTLQRXQMEPGO' - b'XQGLLPNSXAUCYPGZMNWMQWSWCKAQYKXJTWINSGPPZG9HLDLEAWUWEVCTVRCBDFOXKU' - b'ROXH9HXXAXVPEJFRSLOGRVGYZASTEBAQNXJJROCYRTDPYFUIQJVDHAKEG9YACV9HCP' - b'JUEUKOYFNWDXCCJBIFQKYOXGRDHVTHEQUMHO999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999999999999999999999999999999999999' - b'999999999999RKWEEVD99A99999999A99999999NFDPEEZCWVYLKZGSLCQNOFUSENI' - b'XRHWWTZFBXMPSQHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PGTKORV9IKTJZQ' - b'UBQAWTKBKZ9NEZHBFIMCLV9TTNJNQZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999' - b'999TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJNQZUIJDFPTTCTKBJRHAITVSK' - b'UCUEMD9M9SQJ999999999999999999999999999999999999999999999999999999' - b'999999999999999999999999999999999' - ) + b'GYPRVHBEZOOFXSHQBLCYW9ICTCISLHDBNMMVYD9JJHQMPQCTIQAQTJNNNJ9IDXLRCC' + b'OYOXYPCLR9PBEY9ORZIEPPDNTI9CQWYZUOTAVBXPSBOFEQAPFLWXSWUIUSJMSJIIIZ' + b'WIKIRH9GCOEVZFKNXEVCUCIIWZQCQEUVRZOCMEL9AMGXJNMLJCIA9UWGRPPHCEOPTS' + b'VPKPPPCMQXYBHMSODTWUOABPKWFFFQJHCBVYXLHEWPD9YUDFTGNCYAKQKVEZYRBQRB' + b'XIAUX9SVEDUKGMTWQIYXRGSWYRK9SRONVGTW9YGHSZRIXWGPCCUCDRMAXBPDFVHSRY' + b'WHGB9DQSQFQKSNICGPIPTRZINYRXQAFSWSEWIFRMSBMGTNYPRWFSOIIWWT9IDSELM9' + b'JUOOWFNCCSHUSMGNROBFJX9JQ9XT9PKEGQYQAWAFPRVRRVQPUQBHLSNTEFCDKBWRCD' + b'X9EYOBB9KPMTLNNQLADBDLZPRVBCKVCYQEOLARJYAGTBFR9QLPKZBOYWZQOVKCVYRG' + b'YI9ZEFIQRKYXLJBZJDBJDJVQZCGYQMROVHNDBLGNLQODPUXFNTADDVYNZJUVPGB9LV' + b'PJIYLAPBOEHPMRWUIAJXVQOEM9ROEYUOTNLXVVQEYRQWDTQGDLEYFIYNDPRAIXOZEB' + b'CS9P99AZTQQLKEILEVXMSHBIDHLXKUOMMNFKPYHONKEYDCHMUNTTNRYVMMEYHPGASP' + b'ZXASKRUPWQSHDMU9VPS99ZZ9SJJYFUJFFMFORBYDILBXCAVJDPDFHTTTIYOVGLRDYR' + b'TKHXJORJVYRPTDH9ZCPZ9ZADXZFRSFPIQKWLBRNTWJHXTOAUOL9FVGTUMMPYGYICJD' + b'XMOESEVDJWLMCVTJLPIEKBE9JTHDQWV9MRMEWFLPWGJFLUXI9BXPSVWCMUWLZSEWHB' + b'DZKXOLYNOZAPOYLQVZAQMOHGTTQEUAOVKVRRGAHNGPUEKHFVPVCOYSJAWHZU9DRROH' + b'BETBAFTATVAUGOEGCAYUXACLSSHHVYDHMDGJP9AUCLWLNTFEVGQGHQXSKEMVOVSKQE' + b'EWHWZUDTYOBGCURRZSJZLFVQQAAYQO9TRLFFN9HTDQXBSPPJYXMNGLLBHOMNVXNOWE' + b'IDMJVCLLDFHBDONQJCJVLBLCSMDOUQCKKCQJMGTSTHBXPXAMLMSXRIPUBMBAWBFNLH' + b'LUJTRJLDERLZFUBUSMF999XNHLEEXEENQJNOFFPNPQ9PQICHSATPLZVMVIWLRTKYPI' + b'XNFGYWOJSQDAXGFHKZPFLPXQEHCYEAGTIWIJEZTAVLNUMAFWGGLXMBNUQTOFCNLJTC' + b'DMWVVZGVBSEBCPFSM99FLOIDTCLUGPSEDLOKZUAEVBLWNMODGZBWOVQT9DPFOTSKRA' + b'BQAVOQ9RXWBMAKFYNDCZOJGTCIDMQSQQSODKDXTPFLNOKSIZEOY9HFUTLQRXQMEPGO' + b'XQGLLPNSXAUCYPGZMNWMQWSWCKAQYKXJTWINSGPPZG9HLDLEAWUWEVCTVRCBDFOXKU' + b'ROXH9HXXAXVPEJFRSLOGRVGYZASTEBAQNXJJROCYRTDPYFUIQJVDHAKEG9YACV9HCP' + b'JUEUKOYFNWDXCCJBIFQKYOXGRDHVTHEQUMHO999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999RKWEEVD99A99999999A99999999NFDPEEZCWVYLKZGSLCQNOFUSENI' + b'XRHWWTZFBXMPSQHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PGTKORV9IKTJZQ' + b'UBQAWTKBKZ9NEZHBFIMCLV9TTNJNQZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999' + b'999TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJNQZUIJDFPTTCTKBJRHAITVSK' + b'UCUEMD9M9SQJ999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999' + ) mock_prepare_transfer =\ mock.Mock(return_value={ From aa3ce134cac4487b0944557efd098add002a2ee2 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Mon, 16 Apr 2018 10:45:54 +0300 Subject: [PATCH 20/31] prepare_transfer security_level inout filter on Type/Min/Max checking instead of Choice --- iota/commands/extended/prepare_transfer.py | 2 +- .../extended/prepare_transfer_test.py | 46 ++++++++++++++++--- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/iota/commands/extended/prepare_transfer.py b/iota/commands/extended/prepare_transfer.py index aa22c75..bac5017 100644 --- a/iota/commands/extended/prepare_transfer.py +++ b/iota/commands/extended/prepare_transfer.py @@ -133,7 +133,7 @@ def __init__(self): # Optional parameters. 'changeAddress': Trytes(result_type=Address), - 'securityLevel': f.Choice([1, 2, 3]) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), + 'securityLevel': f.Type(int) | f.Min(1) | f.Max(3) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), # Note that ``inputs`` is allowed to be an empty array. 'inputs': diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index 2292dc5..91f3544 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -399,25 +399,57 @@ def test_fail_inputs_contents_invalid(self): }, ) - def test_fail_wrong_security_level(self): + def test_fail_security_level_too_small(self): """ - ``security_level`` is not one of integers 1, 2 or 3. + ``securityLevel`` is < 1. """ self.assertFilterErrors( { - # Must be an array, even if there's only one input. - 'inputs': None, + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + 'securityLevel': 0, + 'seed': Seed(self.trytes1), + }, - 'seed': Seed(self.trytes1), + { + 'securityLevel': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_security_level_too_big(self): + """ + ``securityLevel`` is > 3. + """ + self.assertFilterErrors( + { + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + 'securityLevel': 4, + 'seed': Seed(self.trytes1), + }, + { + 'securityLevel': [f.Max.CODE_TOO_BIG], + }, + ) + + def test_fail_security_level_wrong_type(self): + """ + ``securityLevel`` is not an int. + """ + self.assertFilterErrors( + { 'transfers': [ ProposedTransaction(address=Address(self.trytes2), value=42), ], - "securityLevel": 0, + 'securityLevel': '2', + 'seed': Seed(self.trytes1), }, { - 'securityLevel': [f.Choice.CODE_INVALID], + 'securityLevel': [f.Type.CODE_WRONG_TYPE], }, ) From aec033cb2e6b437adc386fd34dd8a5ace7c1e094 Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Mon, 16 Apr 2018 11:10:02 +0300 Subject: [PATCH 21/31] change mocking private method to seeding response in `test_start_stop` --- test/commands/extended/get_inputs_test.py | 31 ++++++++++------------- 1 file changed, 13 insertions(+), 18 deletions(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index d4c4219..628b229 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -873,12 +873,6 @@ def test_start_stop(self): Using ``start`` and ``stop`` at once. Checking if correct number of addresses is returned. Must be stop - start """ - # mocking get_balances to get number of returned balances - # equal to number of addresses - # returns {"balances": [11] } for one address, {"balances": [11, 11]} for two etc - - def mock_get_balances_execute(adapter, request): - return dict(balances=[11] * len(request["addresses"])) # To keep the unit test nice and speedy, we will mock the address # generator. We already have plenty of unit tests for that @@ -890,31 +884,32 @@ def mock_address_generator(ag, start, step=1): for addy in [self.addy0, self.addy1, self.addy2][start::step]: yield addy + self.adapter.seed_response('getBalances', { + 'balances': [11, 11], + }) + with mock.patch( 'iota.crypto.addresses.AddressGenerator.create_iterator', mock_address_generator, ): - with mock.patch( - 'iota.commands.core.GetBalancesCommand._execute', - mock_get_balances_execute, - ): - response = self.command( - seed = Seed.random(), - start = 1, - stop = 3, - ) + response = self.command( + seed = Seed.random(), + start = 1, + stop = 3, + ) self.assertEqual(len(response['inputs']), 2) # 3 - 1 = 2 addresses expected self.assertEqual(response['totalBalance'], 22) input0 = response['inputs'][0] - input1 = response['inputs'][1] self.assertIsInstance(input0, Address) - self.assertIsInstance(input1, Address) self.assertEqual(input0, self.addy1) - self.assertEqual(input1, self.addy2) self.assertEqual(input0.balance, 11) self.assertEqual(input0.key_index, 1) + + input1 = response['inputs'][1] + self.assertIsInstance(input1, Address) + self.assertEqual(input1, self.addy2) self.assertEqual(input1.balance, 11) self.assertEqual(input1.key_index, 2) From 7e270814d74fb772cd10676a23890fd77d511d7f Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Mon, 16 Apr 2018 11:15:01 +0300 Subject: [PATCH 22/31] wrong comment from docstring removed --- iota/api.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/iota/api.py b/iota/api.py index d5cc1c1..9f66335 100644 --- a/iota/api.py +++ b/iota/api.py @@ -954,11 +954,6 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=None): If not provided, a default value will be used. - :param security_level: - Number of iterations to use when generating new addresses (see get_new_addresses). - This value must be between 1 and 3, inclusive. - If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 - :return: Dict containing the following values:: From 92b3f624d34d309463995f2a6c01aa2908971f5c Mon Sep 17 00:00:00 2001 From: lunfardo314 Date: Mon, 16 Apr 2018 11:43:10 +0300 Subject: [PATCH 23/31] `test_security_level` replaced by two to make it simpler and more readable --- test/commands/extended/get_inputs_test.py | 133 ++++++++++++++-------- 1 file changed, 85 insertions(+), 48 deletions(-) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 628b229..de62d9f 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -914,55 +914,92 @@ def mock_address_generator(ag, start, step=1): self.assertEqual(input1.key_index, 2) - def test_security_level(self): - """ - Testing GetInputsCoommand with selected security_levels - with and without `stop` parameter to cover both branches in the command - """ - def invoke_cmd(seed, stopYN, securityLevel): - self.adapter.seed_response('getBalances', { - 'balances': [86], - }) - # ``getInputs`` uses ``findTransactions`` to identify unused - # addresses. - # noinspection SpellCheckingInspection - self.adapter.seed_response('findTransactions', { - 'hashes': [ - TransactionHash( - b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' - b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' - ), - ], - }) - self.adapter.seed_response('findTransactions', { - 'hashes': [], - }) - if stopYN: - ret = GetInputsCommand(self.adapter)( - seed=seed, - stop=1, - securityLevel=securityLevel, - ) - else: - ret = GetInputsCommand(self.adapter)( - seed=seed, - securityLevel=securityLevel, - ) - return ret + def test_security_level_1_no_stop(self): + """ + Testing GetInputsCoommand: + - with security_level = 1 (non default) + - without `stop` parameter + """ # one address with index 0 for selected security levels for the random seed. # to check with respective outputs from command seed = Seed.random() - security_levels_to_test = [1, 2] # at least one is non-default - addrs = {l: AddressGenerator(seed, l).get_addresses(0)[0] for l in security_levels_to_test} - - for securityLevel in security_levels_to_test: - for stop in [True, False]: - response = invoke_cmd(seed, stop, securityLevel) - self.assertEqual(response['totalBalance'], 86) - self.assertEqual(len(response['inputs']), 1) - input0 = response['inputs'][0] - self.assertIsInstance(input0, Address) - self.assertEqual(input0, addrs[securityLevel]) - self.assertEqual(input0.balance, 86) - self.assertEqual(input0.key_index, 0) + address = AddressGenerator(seed, security_level=1).get_addresses(0)[0] + + self.adapter.seed_response('getBalances', { + 'balances': [86], + }) + # ``getInputs`` uses ``findTransactions`` to identify unused + # addresses. + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' + b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' + ), + ], + }) + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + response = GetInputsCommand(self.adapter)( + seed=seed, + securityLevel=1, + ) + + self.assertEqual(response['totalBalance'], 86) + self.assertEqual(len(response['inputs']), 1) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + self.assertEqual(input0, address) + self.assertEqual(input0.balance, 86) + self.assertEqual(input0.key_index, 0) + + def test_security_level_1_with_stop(self): + """ + Testing GetInputsCoommand: + - with security_level = 1 (non default) + - with `stop` parameter + """ + + # one address with index 0 for selected security levels for the random seed. + # to check with respective outputs from command + seed = Seed.random() + address = AddressGenerator(seed, security_level=1).get_addresses(0)[0] + + self.adapter.seed_response('getBalances', { + 'balances': [86], + }) + # ``getInputs`` uses ``findTransactions`` to identify unused + # addresses. + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' + b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' + ), + ], + }) + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + response = GetInputsCommand(self.adapter)( + seed=seed, + securityLevel=1, + stop=1, # <<<<< here + ) + + self.assertEqual(response['totalBalance'], 86) + self.assertEqual(len(response['inputs']), 1) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + self.assertEqual(input0, address) + self.assertEqual(input0.balance, 86) + self.assertEqual(input0.key_index, 0) + From a3ca34de31698b2c5ae740afba327bfcfdb9a44f Mon Sep 17 00:00:00 2001 From: mike Date: Fri, 11 May 2018 16:23:24 +0300 Subject: [PATCH 24/31] Fixes invalid receive address in examples --- examples/send_transfer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/send_transfer.py b/examples/send_transfer.py index 011c553..91b92b9 100644 --- a/examples/send_transfer.py +++ b/examples/send_transfer.py @@ -5,7 +5,7 @@ from iota import * SEED1 = b"THESEEDOFTHEWALLETSENDINGGOESHERE999999999999999999999999999999999999999999999999" -ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2 = b"RECEIVINGWALLETADDRESSGOESHERE9WITHCHECKSUMANDSECURITYLEVEL2999999999999999999999999999999" +ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2 = b"RECEIVINGWALLETADDRESSGOESHERE9WITHCHECKSUMANDSECURITYLEVELB999999999999999999999999999999" # Create the API instance. api =\ From 356b05ca6d0a818bf4d0187f8c08738d98008f8e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 May 2018 08:45:55 +0100 Subject: [PATCH 25/31] Fixed missing import. --- iota/commands/extended/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index 3fd22ca..a3a2807 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, Iterable, List, Tuple +from typing import Generator, Iterable, List, Optional, Tuple from iota import Address, Bundle, Transaction, \ TransactionHash From 09df7f3860a261648e161587dc4cfa78b5dd203f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 9 Jun 2018 09:18:08 +1200 Subject: [PATCH 26/31] Added missing import for type hint. --- iota/commands/extended/send_trytes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index b30f4d4..1b43d84 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import List +from typing import List, Optional import filters as f from iota import TransactionTrytes, TryteString, TransactionHash From 7733db9c5cf26f3e9856f2814229668157806d13 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 9 Jun 2018 09:19:29 +1200 Subject: [PATCH 27/31] Added missing docstring entry for param. --- iota/api.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iota/api.py b/iota/api.py index 8831c1b..7c2ad69 100644 --- a/iota/api.py +++ b/iota/api.py @@ -912,6 +912,11 @@ def send_transfer( If not provided, a default value will be used. + :param security_level: + Number of iterations to use when generating new addresses (see get_new_addresses). + This value must be between 1 and 3, inclusive. + If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 + :return: Dict containing the following values:: From edbebf4d918a586d84b1b11395e17f198340d881 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 9 Jun 2018 09:36:49 +1200 Subject: [PATCH 28/31] [#176] Added `SecurityLevel` filter macro. --- iota/commands/extended/get_inputs.py | 6 ++++-- iota/commands/extended/get_new_addresses.py | 12 ++++-------- iota/commands/extended/prepare_transfer.py | 6 +++--- iota/commands/extended/send_transfer.py | 6 +++--- iota/filters.py | 15 +++++++++++++++ test/commands/extended/send_transfer_test.py | 12 ++++++------ 6 files changed, 35 insertions(+), 22 deletions(-) diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index f131f9a..aaeb5d5 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -5,6 +5,7 @@ from typing import Optional import filters as f + from iota import BadApiResponse from iota.commands import FilterCommand, RequestFilter from iota.commands.core.get_balances import GetBalancesCommand @@ -12,7 +13,7 @@ from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.exceptions import with_context -from iota.filters import Trytes +from iota.filters import SecurityLevel, Trytes __all__ = [ 'GetInputsCommand', @@ -112,7 +113,8 @@ def __init__(self): 'stop': f.Type(int) | f.Min(0), 'start': f.Type(int) | f.Min(0) | f.Optional(0), 'threshold': f.Type(int) | f.Min(0), - 'securityLevel': f.Type(int) | f.Min(1) | f.Max(3) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), + + 'securityLevel': SecurityLevel, # These arguments are required. 'seed': f.Required | Trytes(result_type=Seed), diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index 7acf1a8..552561d 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -6,12 +6,12 @@ import filters as f -from iota import Address, AddressChecksum +from iota import Address from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed -from iota.filters import Trytes +from iota.filters import SecurityLevel, Trytes __all__ = [ 'GetNewAddressesCommand', @@ -40,7 +40,7 @@ def _execute(self, request): seed = request['seed'] # type: Seed return { - 'addresses': + 'addresses': self._find_addresses(seed, index, count, security_level, checksum), } @@ -85,11 +85,7 @@ def __init__(self): 'count': f.Type(int) | f.Min(1), 'index': f.Type(int) | f.Min(0) | f.Optional(default=0), - 'securityLevel': - f.Type(int) - | f.Min(1) - | f.Max(self.MAX_SECURITY_LEVEL) - | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), + 'securityLevel': SecurityLevel, 'seed': f.Required | Trytes(result_type=Seed), }, diff --git a/iota/commands/extended/prepare_transfer.py b/iota/commands/extended/prepare_transfer.py index bac5017..390e218 100644 --- a/iota/commands/extended/prepare_transfer.py +++ b/iota/commands/extended/prepare_transfer.py @@ -5,17 +5,17 @@ from typing import List, Optional import filters as f + from iota import Address, BadApiResponse, ProposedBundle, \ ProposedTransaction from iota.commands import FilterCommand, RequestFilter from iota.commands.core.get_balances import GetBalancesCommand from iota.commands.extended.get_inputs import GetInputsCommand from iota.commands.extended.get_new_addresses import GetNewAddressesCommand -from iota.crypto.addresses import AddressGenerator from iota.crypto.signing import KeyGenerator from iota.crypto.types import Seed from iota.exceptions import with_context -from iota.filters import GeneratedAddress, Trytes +from iota.filters import GeneratedAddress, SecurityLevel, Trytes __all__ = [ 'PrepareTransferCommand', @@ -133,7 +133,7 @@ def __init__(self): # Optional parameters. 'changeAddress': Trytes(result_type=Address), - 'securityLevel': f.Type(int) | f.Min(1) | f.Max(3) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), + 'securityLevel': SecurityLevel, # Note that ``inputs`` is allowed to be an empty array. 'inputs': diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index 666cecd..b456c8a 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -5,13 +5,13 @@ from typing import List, Optional import filters as f + from iota import Address, Bundle, ProposedTransaction, TransactionHash from iota.commands import FilterCommand, RequestFilter from iota.commands.extended.prepare_transfer import PrepareTransferCommand from iota.commands.extended.send_trytes import SendTrytesCommand from iota.crypto.types import Seed -from iota.crypto.addresses import AddressGenerator -from iota.filters import Trytes +from iota.filters import SecurityLevel, Trytes __all__ = [ 'SendTransferCommand', @@ -82,7 +82,7 @@ def __init__(self): # Optional parameters. 'changeAddress': Trytes(result_type=Address), - 'securityLevel': f.Choice([1, 2, 3]) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), + 'securityLevel': SecurityLevel, # Note that ``inputs`` is allowed to be an empty array. 'inputs': diff --git a/iota/filters.py b/iota/filters.py index e5bae6a..1c1ffdb 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -5,9 +5,11 @@ from typing import Text import filters as f +from filters.macros import filter_macro from six import binary_type, moves as compat, text_type from iota import Address, TryteString, TrytesCompatible +from iota.crypto.addresses import AddressGenerator class GeneratedAddress(f.BaseFilter): @@ -70,6 +72,19 @@ def _apply(self, value): return value +@filter_macro +def SecurityLevel(): + """ + Generates a filter chain for validating a security level. + """ + return ( + f.Type(int) | + f.Min(1) | + f.Max(3) | + f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL) + ) + + class Trytes(f.BaseFilter): """ Validates a sequence as a sequence of trytes. diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index 970110f..aaedac8 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -6,10 +6,10 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type +from six import binary_type -from iota import Address, Bundle, Iota, ProposedTransaction, \ - TransactionTrytes, TryteString, TransactionHash +from iota import Address, Bundle, Iota, ProposedTransaction, TransactionHash, \ + TransactionTrytes, TryteString from iota.adapter import MockAdapter from iota.commands.extended.send_transfer import SendTransferCommand from iota.crypto.addresses import AddressGenerator @@ -175,13 +175,13 @@ def test_pass_optional_parameters_omitted(self): 'depth': 100, 'minWeightMagnitude': 13, + 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL, 'seed': Seed(self.trytes2), 'transfers': [ self.transfer1, self.transfer2 ], - 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL, } ) @@ -635,7 +635,7 @@ def test_fail_wrong_security_level(self): }, { - 'securityLevel': [f.Choice.CODE_INVALID], + 'securityLevel': [f.Min.CODE_TOO_SMALL], }, ) @@ -656,7 +656,7 @@ def test_fail_wrong_security_level_type(self): }, { - 'securityLevel': [f.Choice.CODE_INVALID], + 'securityLevel': [f.Type.CODE_WRONG_TYPE], }, ) From 1a242740a9836fa3c48420d225bf10dc498af7b6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 10 Jun 2018 08:52:02 +1200 Subject: [PATCH 29/31] Bumping version number. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 13ae30b..444dfc9 100644 --- a/setup.py +++ b/setup.py @@ -43,7 +43,7 @@ name = 'PyOTA', description = 'IOTA API library for Python', url = 'https://github.com/iotaledger/iota.lib.py', - version = '2.0.5', + version = '2.0.6', long_description = long_description, From 96a966f3bc1c87c650171e11141306f804e9d701 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 10 Jun 2018 09:08:12 +1200 Subject: [PATCH 30/31] Fixed type hints. --- iota/commands/extended/get_inputs.py | 2 +- iota/commands/extended/prepare_transfer.py | 2 +- iota/commands/extended/send_transfer.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index aaeb5d5..77d62c1 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -39,7 +39,7 @@ def _execute(self, request): seed = request['seed'] # type: Seed start = request['start'] # type: int threshold = request['threshold'] # type: Optional[int] - security_level = request['securityLevel'] # type: Optional[int] + security_level = request['securityLevel'] # int # Determine the addresses we will be scanning. if stop is None: diff --git a/iota/commands/extended/prepare_transfer.py b/iota/commands/extended/prepare_transfer.py index 390e218..cd3cb90 100644 --- a/iota/commands/extended/prepare_transfer.py +++ b/iota/commands/extended/prepare_transfer.py @@ -44,7 +44,7 @@ def _execute(self, request): # Optional parameters. change_address = request.get('changeAddress') # type: Optional[Address] proposed_inputs = request.get('inputs') # type: Optional[List[Address]] - security_level = request['securityLevel'] # type: Optional[int] + security_level = request['securityLevel'] # type: int want_to_spend = bundle.balance if want_to_spend > 0: diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index b456c8a..7c5060d 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -40,7 +40,7 @@ def _execute(self, request): seed = request['seed'] # type: Seed transfers = request['transfers'] # type: List[ProposedTransaction] reference = request['reference'] # type: Optional[TransactionHash] - security_level = request['securityLevel'] # type: Optional[int] + security_level = request['securityLevel'] # int pt_response = PrepareTransferCommand(self.adapter)( changeAddress = change_address, From 67741dc1e82e60b84f59069efae3f2d0ca3cacb4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 10 Jun 2018 09:15:37 +1200 Subject: [PATCH 31/31] Progressive PEP-8 migration. --- iota/adapter/__init__.py | 927 ++++++++++++++++++++------------------- 1 file changed, 468 insertions(+), 459 deletions(-) diff --git a/iota/adapter/__init__.py b/iota/adapter/__init__.py index 63f8e2e..d5e77e9 100644 --- a/iota/adapter/__init__.py +++ b/iota/adapter/__init__.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import json from abc import ABCMeta, abstractmethod as abstract_method @@ -10,28 +10,27 @@ from socket import getdefaulttimeout as get_default_timeout from typing import Container, Dict, List, Optional, Text, Tuple, Union -from requests import Response, codes, request, auth +from requests import Response, auth, codes, request from six import PY2, binary_type, iteritems, moves as compat, text_type, \ - with_metaclass + with_metaclass from iota.exceptions import with_context from iota.json import JsonEncoder __all__ = [ - 'API_VERSION', - 'AdapterSpec', - 'BadApiResponse', - 'InvalidUri', + 'API_VERSION', + 'AdapterSpec', + 'BadApiResponse', + 'InvalidUri', ] if PY2: - # Fix an error when importing this package using the ``imp`` library - # (note: ``imp`` is deprecated since Python 3.4 in favor of - # ``importlib``). - # https://docs.python.org/3/library/imp.html - # https://travis-ci.org/iotaledger/iota.lib.py/jobs/191974244 - __all__ = map(binary_type, __all__) - + # Fix an error when importing this package using the ``imp`` library + # (note: ``imp`` is deprecated since Python 3.4 in favor of + # ``importlib``). + # https://docs.python.org/3/library/imp.html + # https://travis-ci.org/iotaledger/iota.lib.py/jobs/191974244 + __all__ = map(binary_type, __all__) API_VERSION = '1' """ @@ -39,503 +38,513 @@ https://github.com/iotaledger/iota.lib.py/issues/84 """ - # Custom types for type hints and docstrings. AdapterSpec = Union[Text, 'BaseAdapter'] # Load SplitResult for IDE type hinting and autocompletion. if PY2: - # noinspection PyCompatibility,PyUnresolvedReferences - from urlparse import SplitResult + # noinspection PyCompatibility,PyUnresolvedReferences + from urlparse import SplitResult else: - # noinspection PyCompatibility,PyUnresolvedReferences - from urllib.parse import SplitResult + # noinspection PyCompatibility,PyUnresolvedReferences + from urllib.parse import SplitResult class BadApiResponse(ValueError): - """ - Indicates that a non-success response was received from the node. - """ - pass + """ + Indicates that a non-success response was received from the node. + """ + pass class InvalidUri(ValueError): - """ - Indicates that an invalid URI was provided to `resolve_adapter`. - """ - pass + """ + Indicates that an invalid URI was provided to `resolve_adapter`. + """ + pass -adapter_registry = {} # type: Dict[Text, AdapterMeta] +adapter_registry = {} # type: Dict[Text, AdapterMeta] """ Keeps track of available adapters and their supported protocols. """ def resolve_adapter(uri): - # type: (AdapterSpec) -> BaseAdapter - """ - Given a URI, returns a properly-configured adapter instance. - """ - if isinstance(uri, BaseAdapter): - return uri - - parsed = compat.urllib_parse.urlsplit(uri) # type: SplitResult - - if not parsed.scheme: - raise with_context( - exc = InvalidUri( - 'URI must begin with "://" (e.g., "udp://").', - ), - - context = { - 'parsed': parsed, - 'uri': uri, - }, - ) - - try: - adapter_type = adapter_registry[parsed.scheme] - except KeyError: - raise with_context( - exc = InvalidUri('Unrecognized protocol {protocol!r}.'.format( - protocol = parsed.scheme, - )), - - context = { - 'parsed': parsed, - 'uri': uri, - }, - ) - - return adapter_type.configure(parsed) - - -class AdapterMeta(ABCMeta): - """ - Automatically registers new adapter classes in ``adapter_registry``. - """ - # noinspection PyShadowingBuiltins - def __init__(cls, what, bases=None, dict=None): - super(AdapterMeta, cls).__init__(what, bases, dict) - - if not is_abstract(cls): - for protocol in getattr(cls, 'supported_protocols', ()): - # Note that we will not overwrite existing registered adapters. - adapter_registry.setdefault(protocol, cls) - - def configure(cls, parsed): - # type: (Union[Text, SplitResult]) -> HttpAdapter + # type: (AdapterSpec) -> BaseAdapter """ - Creates a new instance using the specified URI. - - :param parsed: - Result of :py:func:`urllib.parse.urlsplit`. + Given a URI, returns a properly-configured adapter instance. """ - return cls(parsed) + if isinstance(uri, BaseAdapter): + return uri + parsed = compat.urllib_parse.urlsplit(uri) # type: SplitResult -class BaseAdapter(with_metaclass(AdapterMeta)): - """ - Interface for IOTA API adapters. - - Adapters make it easy to customize the way an StrictIota instance - communicates with a node. - """ - supported_protocols = () # type: Tuple[Text] - """ - Protocols that ``resolve_adapter`` can use to identify this adapter - type. - """ - - def __init__(self): - super(BaseAdapter, self).__init__() - - self._logger = None # type: Logger - - @abstract_method - def get_uri(self): - # type: () -> Text - """ - Returns the URI that this adapter will use. - """ - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) + if not parsed.scheme: + raise with_context( + exc=InvalidUri( + 'URI must begin with "://" (e.g., "udp://").', + ), - @abstract_method - def send_request(self, payload, **kwargs): - # type: (dict, dict) -> dict - """ - Sends an API request to the node. + context={ + 'parsed': parsed, + 'uri': uri, + }, + ) - :param payload: - JSON payload. - - :param kwargs: - Additional keyword arguments for the adapter. - - :return: - Decoded response from the node. - - :raise: - - :py:class:`BadApiResponse` if a non-success response was - received. - """ - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) - - def set_logger(self, logger): - # type: (Logger) -> BaseAdapter - """ - Attaches a logger instance to the adapter. - The adapter will send information about API requests/responses to - the logger. - """ - self._logger = logger - return self + try: + adapter_type = adapter_registry[parsed.scheme] + except KeyError: + raise with_context( + exc=InvalidUri('Unrecognized protocol {protocol!r}.'.format( + protocol=parsed.scheme, + )), - def _log(self, level, message, context=None): - # type: (int, Text, Optional[dict]) -> None - """ - Sends a message to the instance's logger, if configured. - """ - if self._logger: - self._logger.log(level, message, extra={'context': context or {}}) + context={ + 'parsed': parsed, + 'uri': uri, + }, + ) + return adapter_type.configure(parsed) -class HttpAdapter(BaseAdapter): - """ - Sends standard HTTP requests. - """ - supported_protocols = ('http', 'https',) - - DEFAULT_HEADERS = { - 'Content-type': 'application/json', - - # https://github.com/iotaledger/iota.lib.py/issues/84 - 'X-IOTA-API-Version': API_VERSION, - } - """ - Default headers sent with every request. - These can be overridden on a per-request basis, by specifying values - in the ``headers`` kwarg. - """ - - def __init__(self, uri, timeout=None, authentication=None): - # type: (Union[Text, SplitResult], Optional[int]) -> None - super(HttpAdapter, self).__init__() - - self.timeout = timeout - self.authentication = authentication - - if isinstance(uri, text_type): - uri = compat.urllib_parse.urlsplit(uri) # type: SplitResult - - if uri.scheme not in self.supported_protocols: - raise with_context( - exc = InvalidUri('Unsupported protocol {protocol!r}.'.format( - protocol = uri.scheme, - )), - - context = { - 'uri': uri, - }, - ) - - if not uri.hostname: - raise with_context( - exc = InvalidUri( - 'Empty hostname in URI {uri!r}.'.format( - uri = uri.geturl(), - ), - ), - - context = { - 'uri': uri, - }, - ) - try: - # noinspection PyStatementEffect - uri.port - except ValueError: - raise with_context( - exc = InvalidUri( - 'Non-numeric port in URI {uri!r}.'.format( - uri = uri.geturl(), - ), - ), - - context = { - 'uri': uri, - }, - ) - - self.uri = uri - - @property - def node_url(self): - # type: () -> Text +class AdapterMeta(ABCMeta): """ - Returns the node URL. + Automatically registers new adapter classes in ``adapter_registry``. """ - return self.uri.geturl() - def get_uri(self): - # type: () -> Text - return self.uri.geturl() + # noinspection PyShadowingBuiltins + def __init__(cls, what, bases=None, dict=None): + super(AdapterMeta, cls).__init__(what, bases, dict) - def send_request(self, payload, **kwargs): - # type: (dict, dict) -> dict - kwargs.setdefault('headers', {}) - for key, value in iteritems(self.DEFAULT_HEADERS): - kwargs['headers'].setdefault(key, value) + if not is_abstract(cls): + for protocol in getattr(cls, 'supported_protocols', ()): + # Note that we will not overwrite existing registered + # adapters. + adapter_registry.setdefault(protocol, cls) - response = self._send_http_request( - # Use a custom JSON encoder that knows how to convert Tryte values. - payload = JsonEncoder().encode(payload), + def configure(cls, parsed): + # type: (Union[Text, SplitResult]) -> HttpAdapter + """ + Creates a new instance using the specified URI. - url = self.node_url, - **kwargs - ) + :param parsed: + Result of :py:func:`urllib.parse.urlsplit`. + """ + return cls(parsed) - return self._interpret_response(response, payload, {codes['ok']}) - def _send_http_request(self, url, payload, method='post', **kwargs): - # type: (Text, Optional[Text], Text, dict) -> Response +class BaseAdapter(with_metaclass(AdapterMeta)): """ - Sends the actual HTTP request. + Interface for IOTA API adapters. - Split into its own method so that it can be mocked during unit - tests. + Adapters make it easy to customize the way an StrictIota instance + communicates with a node. """ - - default_timeout = self.timeout if self.timeout else get_default_timeout() - kwargs.setdefault('timeout', default_timeout) - if self.authentication: - kwargs.setdefault('auth', auth.HTTPBasicAuth(*self.authentication)) - - self._log( - level = DEBUG, - - message = 'Sending {method} to {url}: {payload!r}'.format( - method = method, - payload = payload, - url = url, - ), - - context = { - 'request_method': method, - 'request_kwargs': kwargs, - 'request_payload': payload, - 'request_url': url, - }, - ) - - response = request(method=method, url=url, data=payload, **kwargs) - - self._log( - level = DEBUG, - - message = 'Receiving {method} from {url}: {response!r}'.format( - method = method, - response = response.content, - url = url, - ), - - context = { - 'request_method': method, - 'request_kwargs': kwargs, - 'request_payload': payload, - 'request_url': url, - - 'response_headers': response.headers, - 'response_content': response.content, - }, - ) - - return response - - def _interpret_response(self, response, payload, expected_status): - # type: (Response, dict, Container[int]) -> dict + supported_protocols = () # type: Tuple[Text] + """ + Protocols that ``resolve_adapter`` can use to identify this adapter + type. """ - Interprets the HTTP response from the node. - :param response: - The response object received from :py:meth:`_send_http_request`. + def __init__(self): + super(BaseAdapter, self).__init__() + + self._logger = None # type: Logger + + @abstract_method + def get_uri(self): + # type: () -> Text + """ + Returns the URI that this adapter will use. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + @abstract_method + def send_request(self, payload, **kwargs): + # type: (dict, dict) -> dict + """ + Sends an API request to the node. + + :param payload: + JSON payload. + + :param kwargs: + Additional keyword arguments for the adapter. + + :return: + Decoded response from the node. + + :raise: + - :py:class:`BadApiResponse` if a non-success response was + received. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + def set_logger(self, logger): + # type: (Logger) -> BaseAdapter + """ + Attaches a logger instance to the adapter. + The adapter will send information about API requests/responses + to the logger. + """ + self._logger = logger + return self + + def _log(self, level, message, context=None): + # type: (int, Text, Optional[dict]) -> None + """ + Sends a message to the instance's logger, if configured. + """ + if self._logger: + self._logger.log(level, message, extra={'context': context or {}}) - :param payload: - The request payload that was sent (used for debugging). - :param expected_status: - The response should match one of these status codes to be - considered valid. +class HttpAdapter(BaseAdapter): + """ + Sends standard HTTP requests. """ - raw_content = response.text - if not raw_content: - raise with_context( - exc = BadApiResponse( - 'Empty {status} response from node.'.format( - status = response.status_code, - ), - ), - - context = { - 'request': payload, - }, - ) + supported_protocols = ('http', 'https',) - try: - decoded = json.loads(raw_content) # type: dict - # :bc: py2k doesn't have JSONDecodeError - except ValueError: - raise with_context( - exc = BadApiResponse( - 'Non-JSON {status} response from node: {raw_content}'.format( - status = response.status_code, - raw_content = raw_content, - ) - ), - - context = { - 'request': payload, - 'raw_response': raw_content, - }, - ) - - if not isinstance(decoded, dict): - raise with_context( - exc = BadApiResponse( - 'Malformed {status} response from node: {decoded!r}'.format( - status = response.status_code, - decoded = decoded, - ), - ), - - context = { - 'request': payload, - 'response': decoded, - }, - ) - - if response.status_code in expected_status: - return decoded - - error = None - try: - if response.status_code == codes['bad_request']: - error = decoded['error'] - elif response.status_code == codes['internal_server_error']: - error = decoded['exception'] - except KeyError: - pass + DEFAULT_HEADERS = { + 'Content-type': 'application/json', - raise with_context( - exc = BadApiResponse( - '{status} response from node: {error}'.format( - error = error or decoded, - status = response.status_code, - ), - ), + # https://github.com/iotaledger/iota.lib.py/issues/84 + 'X-IOTA-API-Version': API_VERSION, + } + """ + Default headers sent with every request. + These can be overridden on a per-request basis, by specifying values + in the ``headers`` kwarg. + """ - context = { - 'request': payload, - 'response': decoded, - }, - ) + def __init__(self, uri, timeout=None, authentication=None): + # type: (Union[Text, SplitResult], Optional[int]) -> None + super(HttpAdapter, self).__init__() + + self.timeout = timeout + self.authentication = authentication + + if isinstance(uri, text_type): + uri = compat.urllib_parse.urlsplit(uri) # type: SplitResult + + if uri.scheme not in self.supported_protocols: + raise with_context( + exc=InvalidUri('Unsupported protocol {protocol!r}.'.format( + protocol=uri.scheme, + )), + + context={ + 'uri': uri, + }, + ) + + if not uri.hostname: + raise with_context( + exc=InvalidUri( + 'Empty hostname in URI {uri!r}.'.format( + uri=uri.geturl(), + ), + ), + + context={ + 'uri': uri, + }, + ) + + try: + # noinspection PyStatementEffect + uri.port + except ValueError: + raise with_context( + exc=InvalidUri( + 'Non-numeric port in URI {uri!r}.'.format( + uri=uri.geturl(), + ), + ), + + context={ + 'uri': uri, + }, + ) + + self.uri = uri + + @property + def node_url(self): + # type: () -> Text + """ + Returns the node URL. + """ + return self.uri.geturl() + + def get_uri(self): + # type: () -> Text + return self.uri.geturl() + + def send_request(self, payload, **kwargs): + # type: (dict, dict) -> dict + kwargs.setdefault('headers', {}) + for key, value in iteritems(self.DEFAULT_HEADERS): + kwargs['headers'].setdefault(key, value) + + response = self._send_http_request( + # Use a custom JSON encoder that knows how to convert Tryte + # values. + payload=JsonEncoder().encode(payload), + + url=self.node_url, + **kwargs + ) + + return self._interpret_response(response, payload, {codes['ok']}) + + def _send_http_request(self, url, payload, method='post', **kwargs): + # type: (Text, Optional[Text], Text, dict) -> Response + """ + Sends the actual HTTP request. + + Split into its own method so that it can be mocked during unit + tests. + """ + kwargs.setdefault( + 'timeout', + self.timeout if self.timeout else get_default_timeout(), + ) + + if self.authentication: + kwargs.setdefault('auth', auth.HTTPBasicAuth(*self.authentication)) + + self._log( + level=DEBUG, + + message='Sending {method} to {url}: {payload!r}'.format( + method=method, + payload=payload, + url=url, + ), + + context={ + 'request_method': method, + 'request_kwargs': kwargs, + 'request_payload': payload, + 'request_url': url, + }, + ) + + response = request(method=method, url=url, data=payload, **kwargs) + + self._log( + level=DEBUG, + + message='Receiving {method} from {url}: {response!r}'.format( + method=method, + response=response.content, + url=url, + ), + + context={ + 'request_method': method, + 'request_kwargs': kwargs, + 'request_payload': payload, + 'request_url': url, + + 'response_headers': response.headers, + 'response_content': response.content, + }, + ) + + return response + + def _interpret_response(self, response, payload, expected_status): + # type: (Response, dict, Container[int]) -> dict + """ + Interprets the HTTP response from the node. + + :param response: + The response object received from + :py:meth:`_send_http_request`. + + :param payload: + The request payload that was sent (used for debugging). + + :param expected_status: + The response should match one of these status codes to be + considered valid. + """ + raw_content = response.text + if not raw_content: + raise with_context( + exc=BadApiResponse( + 'Empty {status} response from node.'.format( + status=response.status_code, + ), + ), + + context={ + 'request': payload, + }, + ) + + try: + decoded = json.loads(raw_content) # type: dict + # :bc: py2k doesn't have JSONDecodeError + except ValueError: + raise with_context( + exc=BadApiResponse( + 'Non-JSON {status} response from node: ' + '{raw_content}'.format( + status=response.status_code, + raw_content=raw_content, + ) + ), + + context={ + 'request': payload, + 'raw_response': raw_content, + }, + ) + + if not isinstance(decoded, dict): + raise with_context( + exc=BadApiResponse( + 'Malformed {status} response from node: {decoded!r}'.format( + status=response.status_code, + decoded=decoded, + ), + ), + + context={ + 'request': payload, + 'response': decoded, + }, + ) + + if response.status_code in expected_status: + return decoded + + error = None + try: + if response.status_code == codes['bad_request']: + error = decoded['error'] + elif response.status_code == codes['internal_server_error']: + error = decoded['exception'] + except KeyError: + pass + + raise with_context( + exc=BadApiResponse( + '{status} response from node: {error}'.format( + error=error or decoded, + status=response.status_code, + ), + ), + + context={ + 'request': payload, + 'response': decoded, + }, + ) class MockAdapter(BaseAdapter): - """ - An mock adapter used for simulating API responses. - - To use this adapter, you must first "seed" the responses that the - adapter should return for each request. The adapter will then return - the appropriate seeded response each time it "sends" a request. - """ - supported_protocols = ('mock',) - - # noinspection PyUnusedLocal - @classmethod - def configure(cls, uri): - return cls() - - def __init__(self): - super(MockAdapter, self).__init__() - - self.responses = {} # type: Dict[Text, deque] - self.requests = [] # type: List[dict] - - def get_uri(self): - return 'mock://' - - def seed_response(self, command, response): - # type: (Text, dict) -> MockAdapter """ - Sets the response that the adapter will return for the specified - command. - - You can seed multiple responses per command; the adapter will put - them into a FIFO queue. When a request comes in, the adapter will - pop the corresponding response off of the queue. - - Example:: - - adapter.seed_response('sayHello', {'message': 'Hi!'}) - adapter.seed_response('sayHello', {'message': 'Hello!'}) - - adapter.send_request({'command': 'sayHello'}) - # {'message': 'Hi!'} + An mock adapter used for simulating API responses. - adapter.send_request({'command': 'sayHello'}) - # {'message': 'Hello!'} + To use this adapter, you must first "seed" the responses that the + adapter should return for each request. The adapter will then return + the appropriate seeded response each time it "sends" a request. """ - if command not in self.responses: - self.responses[command] = deque() - - self.responses[command].append(response) - return self - - def send_request(self, payload, **kwargs): - # type: (dict, dict) -> dict - # Store a snapshot so that we can inspect the request later. - self.requests.append(dict(payload)) - - command = payload['command'] - - try: - response = self.responses[command].popleft() - except KeyError: - raise with_context( - exc = BadApiResponse( - 'No seeded response for {command!r} ' - '(expected one of: {seeds!r}).'.format( - command = command, - seeds = list(sorted(self.responses.keys())), - ), - ), - - context = { - 'request': payload, - }, - ) - except IndexError: - raise with_context( - exc = BadApiResponse( - '{command} called too many times; no seeded responses left.'.format( - command = command, - ), - ), - - context = { - 'request': payload, - }, - ) - - error = response.get('exception') or response.get('error') - if error: - raise with_context(BadApiResponse(error), context={'request': payload}) - - return response + supported_protocols = ('mock',) + + # noinspection PyUnusedLocal + @classmethod + def configure(cls, uri): + return cls() + + def __init__(self): + super(MockAdapter, self).__init__() + + self.responses = {} # type: Dict[Text, deque] + self.requests = [] # type: List[dict] + + def get_uri(self): + return 'mock://' + + def seed_response(self, command, response): + # type: (Text, dict) -> MockAdapter + """ + Sets the response that the adapter will return for the specified + command. + + You can seed multiple responses per command; the adapter will + put them into a FIFO queue. When a request comes in, the + adapter will pop the corresponding response off of the queue. + + Example: + + .. code-block:: python + + adapter.seed_response('sayHello', {'message': 'Hi!'}) + adapter.seed_response('sayHello', {'message': 'Hello!'}) + + adapter.send_request({'command': 'sayHello'}) + # {'message': 'Hi!'} + + adapter.send_request({'command': 'sayHello'}) + # {'message': 'Hello!'} + """ + if command not in self.responses: + self.responses[command] = deque() + + self.responses[command].append(response) + return self + + def send_request(self, payload, **kwargs): + # type: (dict, dict) -> dict + # Store a snapshot so that we can inspect the request later. + self.requests.append(dict(payload)) + + command = payload['command'] + + try: + response = self.responses[command].popleft() + except KeyError: + raise with_context( + exc=BadApiResponse( + 'No seeded response for {command!r} ' + '(expected one of: {seeds!r}).'.format( + command=command, + seeds=list(sorted(self.responses.keys())), + ), + ), + + context={ + 'request': payload, + }, + ) + except IndexError: + raise with_context( + exc=BadApiResponse( + '{command} called too many times; ' + 'no seeded responses left.'.format( + command=command, + ), + ), + + context={ + 'request': payload, + }, + ) + + error = response.get('exception') or response.get('error') + if error: + raise with_context(BadApiResponse(error), + context={'request': payload}) + + return response