diff --git a/README.rst b/README.rst index d5077e78..e567bd0d 100644 --- a/README.rst +++ b/README.rst @@ -9,12 +9,6 @@ This is the official Python library for the IOTA Core. It implements both the `official API`_, as well as newly-proposed functionality (such as signing, bundles, utilities and conversion). -.. warning:: - This is pre-release software! - There may be performance and stability issues. - - Please report any issues using the `PyOTA Bug Tracker`_. - Join the Discussion =================== If you want to get involved in the community, need help with getting setup, @@ -23,6 +17,9 @@ Distributed Ledgers and IoT with other people, feel free to join our `Slack`_. You can also ask questions on our `dedicated forum`_. +If you encounter any issues while using PyOTA, please report them using the +`PyOTA Bug Tracker`_. + ============ Dependencies ============ @@ -33,12 +30,7 @@ Installation ============ To install the latest version:: - pip install --pre pyota - -**Important:** PyOTA is currently pre-release software. -There may be performance and stability issues. - -Please report any issues using the `PyOTA Bug Tracker`_. + pip install pyota Installing from Source ====================== diff --git a/setup.py b/setup.py index 9926378b..497d6e33 100644 --- a/setup.py +++ b/setup.py @@ -28,7 +28,7 @@ name = 'PyOTA', description = 'IOTA API library for Python', url = 'https://github.com/iotaledger/iota.lib.py', - version = '1.0.0b7', + version = '1.0.0', packages = find_packages('src'), include_package_data = True, @@ -52,7 +52,7 @@ license = 'MIT', classifiers = [ - 'Development Status :: 4 - Beta', + 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', diff --git a/src/iota/adapter/__init__.py b/src/iota/adapter/__init__.py index b69a2df2..f3444f89 100644 --- a/src/iota/adapter/__init__.py +++ b/src/iota/adapter/__init__.py @@ -4,6 +4,7 @@ import json from abc import ABCMeta, abstractmethod as abstract_method +from collections import deque from inspect import isabstract as is_abstract from socket import getdefaulttimeout as get_default_timeout from typing import Dict, List, Text, Tuple, Union @@ -312,7 +313,7 @@ def configure(cls, uri): def __init__(self): super(MockAdapter, self).__init__() - self.responses = {} # type: Dict[Text, List[dict]] + self.responses = {} # type: Dict[Text, deque] self.requests = [] # type: List[dict] def seed_response(self, command, response): @@ -337,7 +338,7 @@ def seed_response(self, command, response): # {'message': 'Hello!'} """ if command not in self.responses: - self.responses[command] = [] + self.responses[command] = deque() self.responses[command].append(response) return self @@ -350,7 +351,7 @@ def send_request(self, payload, **kwargs): command = payload['command'] try: - response = self.responses[command].pop(0) + response = self.responses[command].popleft() except KeyError: raise with_context( exc = BadApiResponse( diff --git a/src/iota/api.py b/src/iota/api.py index a7b04b20..d37882d5 100644 --- a/src/iota/api.py +++ b/src/iota/api.py @@ -385,55 +385,68 @@ def get_bundles(self, transaction): """ return self.getBundles(transaction=transaction) - def get_inputs(self, start=None, end=None, threshold=None): - # type: (Optional[int], Optional[int], Optional[int]) -> dict + def get_inputs(self, start=0, stop=None, threshold=None): + # type: (int, Optional[int], Optional[int]) -> dict """ Gets all possible inputs of a seed and returns them with the total balance. This is either done deterministically (by generating all addresses - until :py:meth:`find_transactions` returns an empty - result and then doing :py:meth:`get_balances`), or by providing a - key range to search. + until :py:meth:`find_transactions` returns an empty result), or by + providing a key range to search. :param start: Starting key index. + Defaults to 0. - :param end: + :param stop: Stop before this index. Note that this parameter behaves like the ``stop`` attribute in a - :py:class:`slice` object; the end index is _not_ included in the + :py:class:`slice` object; the stop index is *not* included in the result. - If not specified, then this method will not stop until it finds - an unused address. + If ``None`` (default), then this method will not stop until it + finds an unused address. :param threshold: - Determines the minimum threshold for a successful result. + If set, determines the minimum threshold for a successful result: - As soon as this threshold is reached, iteration will stop. - If the command runs out of addresses before the threshold is reached, an exception is raised. + Note that this method does not attempt to "optimize" the result + (e.g., smallest number of inputs, get as close to ``threshold`` + as possible, etc.); it simply accumulates inputs in order until + the threshold is met. + + If ``threshold`` is 0, the first address in the key range with + a non-zero balance will be returned (if it exists). + + If ``threshold`` is ``None`` (default), this method will return + **all** inputs in the specified key range. + :return: Dict with the following structure:: { - 'inputs': [ - { - 'address':
, - 'balance':
, - 'keyIndex`: , - }, - ... - ] - + 'inputs': 'totalBalance': , } + Note that each Address in the result has its ``balance`` + attribute set. + + Example:: + + response = iota.get_inputs(...) + + input0 = response['inputs'][0] # type: Address + input0.balance # 42 + :raise: - :py:class:`iota.adapter.BadApiResponse` if ``threshold`` is not - met. + met. Not applicable if ``threshold`` is ``None``. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs @@ -441,7 +454,7 @@ def get_inputs(self, start=None, end=None, threshold=None): return self.getInputs( seed = self.seed, start = start, - end = end, + stop = stop, threshold = threshold, ) diff --git a/src/iota/commands/extended/get_inputs.py b/src/iota/commands/extended/get_inputs.py index 1181c01b..50db9f80 100644 --- a/src/iota/commands/extended/get_inputs.py +++ b/src/iota/commands/extended/get_inputs.py @@ -34,7 +34,7 @@ def get_response_filter(self): pass def _execute(self, request): - end = request['end'] # type: Optional[int] + stop = request['stop'] # type: Optional[int] seed = request['seed'] # type: Seed start = request['start'] # type: int threshold = request['threshold'] # type: Optional[int] @@ -42,7 +42,7 @@ def _execute(self, request): generator = AddressGenerator(seed) # Determine the addresses we will be scanning. - if end is None: + if stop is None: # This is similar to the ``getNewAddresses`` command, except it # is interested in all the addresses that `getNewAddresses` # skips. @@ -55,7 +55,7 @@ def _execute(self, request): else: break else: - addresses = generator.get_addresses(start, end - start) + addresses = generator.get_addresses(start, stop) # Load balances for the addresses that we generated. gb_response = GetBalancesCommand(self.adapter)(addresses=addresses) @@ -71,12 +71,7 @@ def _execute(self, request): addresses[i].balance = balance if balance: - result['inputs'].append({ - 'address': addresses[i], - 'balance': balance, - 'keyIndex': addresses[i].key_index, - }) - + result['inputs'].append(addresses[i]) result['totalBalance'] += balance if (threshold is not None) and (result['totalBalance'] >= threshold): @@ -113,15 +108,15 @@ class GetInputsRequestFilter(RequestFilter): CODE_INTERVAL_TOO_BIG = 'interval_too_big' templates = { - CODE_INTERVAL_INVALID: '``start`` must be <= ``end``', - CODE_INTERVAL_TOO_BIG: '``end`` - ``start`` must be <= {max_interval}', + CODE_INTERVAL_INVALID: '``start`` must be <= ``stop``', + CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}', } def __init__(self): super(GetInputsRequestFilter, self).__init__( { # These arguments are optional. - 'end': f.Type(int) | f.Min(0), + 'stop': f.Type(int) | f.Min(0), 'start': f.Type(int) | f.Min(0) | f.Optional(0), 'threshold': f.Type(int) | f.Min(0), @@ -130,7 +125,7 @@ def __init__(self): }, allow_missing_keys = { - 'end', + 'stop', 'start', 'threshold', } @@ -143,8 +138,8 @@ def _apply(self, value): if self._has_errors: return filtered - if filtered['end'] is not None: - if filtered['start'] > filtered['end']: + if filtered['stop'] is not None: + if filtered['start'] > filtered['stop']: filtered['start'] = self._invalid_value( value = filtered['start'], reason = self.CODE_INTERVAL_INVALID, @@ -152,18 +147,18 @@ def _apply(self, value): context = { 'start': filtered['start'], - 'end': filtered['end'], + 'stop': filtered['stop'], }, ) - elif (filtered['end'] - filtered['start']) > self.MAX_INTERVAL: - filtered['end'] = self._invalid_value( - value = filtered['end'], + elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL: + filtered['stop'] = self._invalid_value( + value = filtered['stop'], reason = self.CODE_INTERVAL_TOO_BIG, - sub_key = 'end', + sub_key = 'stop', context = { 'start': filtered['start'], - 'end': filtered['end'], + 'stop': filtered['stop'], }, template_vars = { diff --git a/src/iota/commands/extended/get_new_addresses.py b/src/iota/commands/extended/get_new_addresses.py index 3a1b712e..75192ecf 100644 --- a/src/iota/commands/extended/get_new_addresses.py +++ b/src/iota/commands/extended/get_new_addresses.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Optional + import filters as f from iota.commands import FilterCommand, RequestFilter @@ -30,12 +32,9 @@ def get_response_filter(self): pass def _execute(self, request): - # Optional parameters. - count = request.get('count') - index = request.get('index') - - # Required parameters. - seed = request['seed'] + count = request['count'] # type: Optional[int] + index = request['index'] # type: int + seed = request['seed'] # type: Seed generator = AddressGenerator(seed) @@ -57,7 +56,7 @@ def __init__(self): { # ``count`` and ``index`` are optional. 'count': f.Type(int) | f.Min(1), - 'index': f.Type(int) | f.Min(0), + 'index': f.Type(int) | f.Min(0) | f.Optional(default=0), 'seed': f.Required | Trytes(result_type=Seed), }, diff --git a/src/iota/commands/extended/get_transfers.py b/src/iota/commands/extended/get_transfers.py index e388b3d0..4e1b9f0d 100644 --- a/src/iota/commands/extended/get_transfers.py +++ b/src/iota/commands/extended/get_transfers.py @@ -71,40 +71,49 @@ def _execute(self, request): if hashes: # Sort transactions into tail and non-tail. - tails = set() - non_tails = set() + tail_transaction_hashes = set() + non_tail_bundle_hashes = set() gt_response = GetTrytesCommand(self.adapter)(hashes=hashes) - transactions = list(map( + all_transactions = list(map( Transaction.from_tryte_string, gt_response['trytes'], - )) + )) # type: List[Transaction] - for txn in transactions: + for txn in all_transactions: if txn.is_tail: - tails.add(txn.hash) + tail_transaction_hashes.add(txn.hash) else: # Capture the bundle ID instead of the transaction hash so that # we can query the node to find the tail transaction for that # bundle. - non_tails.add(txn.bundle_hash) + non_tail_bundle_hashes.add(txn.bundle_hash) - if non_tails: - for txn in self._find_transactions(bundles=non_tails): + if non_tail_bundle_hashes: + for txn in self._find_transactions(bundles=list(non_tail_bundle_hashes)): if txn.is_tail: - tails.add(txn.hash) + if txn.hash not in tail_transaction_hashes: + all_transactions.append(txn) + tail_transaction_hashes.add(txn.hash) + + # Filter out all non-tail transactions. + tail_transactions = [ + txn + for txn in all_transactions + if txn.hash in tail_transaction_hashes + ] # Attach inclusion states, if requested. if inclusion_states: gli_response = GetLatestInclusionCommand(self.adapter)( - hashes = list(tails), + hashes = list(tail_transaction_hashes), ) - for txn in transactions: + for txn in tail_transactions: txn.is_confirmed = gli_response['states'].get(txn.hash) # Find the bundles for each transaction. - for txn in transactions: + for txn in tail_transactions: gb_response = GetBundlesCommand(self.adapter)(transaction=txn.hash) txn_bundles = gb_response['bundles'] # type: List[Bundle] diff --git a/src/iota/commands/extended/prepare_transfer.py b/src/iota/commands/extended/prepare_transfer.py index 481dabde..38253857 100644 --- a/src/iota/commands/extended/prepare_transfer.py +++ b/src/iota/commands/extended/prepare_transfer.py @@ -54,10 +54,7 @@ def _execute(self, request): threshold = want_to_spend, ) - confirmed_inputs = [ - input_['address'] - for input_ in gi_response['inputs'] - ] + confirmed_inputs = gi_response['inputs'] else: # Inputs provided. Check to make sure we have sufficient # balance. diff --git a/src/iota/commands/extended/send_transfer.py b/src/iota/commands/extended/send_transfer.py index 48ceed17..4eecab28 100644 --- a/src/iota/commands/extended/send_transfer.py +++ b/src/iota/commands/extended/send_transfer.py @@ -34,7 +34,7 @@ def get_response_filter(self): def _execute(self, request): change_address = request['changeAddress'] # type: Optional[Address] depth = request['depth'] # type: int - inputs = request['inputs'] or [] # type: List[Address] + inputs = request['inputs'] # type: Optional[List[Address]] min_weight_magnitude = request['minWeightMagnitude'] # type: int seed = request['seed'] # type: Seed transfers = request['transfers'] # type: List[ProposedTransaction] diff --git a/src/iota/transaction.py b/src/iota/transaction.py index 9ef02296..f67a4035 100644 --- a/src/iota/transaction.py +++ b/src/iota/transaction.py @@ -8,8 +8,8 @@ from typing import Generator, Iterable, Iterator, List, MutableSequence, \ Optional, Sequence, Text, Tuple -from iota import Address, Hash, Tag, TrytesCompatible, TryteString, \ - int_from_trits, trits_from_int +from iota import Address, Hash, Tag, TryteString, TrytesCompatible, \ + TrytesDecodeError, int_from_trits, trits_from_int from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH from iota.crypto.addresses import AddressGenerator from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator, \ @@ -125,7 +125,7 @@ def from_tryte_string(cls, trytes): return cls( hash_ = TransactionHash.from_trits(hash_), - signature_message_fragment = tryte_string[0:2187], + signature_message_fragment = Fragment(tryte_string[0:2187]), address = Address(tryte_string[2187:2268]), value = int_from_trits(tryte_string[2268:2295].as_trits()), tag = Tag(tryte_string[2295:2322]), @@ -523,6 +523,58 @@ def tail_transaction(self): """ return self[0] + def get_messages(self, errors='drop'): + # type: () -> List[Text] + """ + Attempts to decipher encoded messages from the transactions in the + bundle. + + :param errors: + How to handle trytes that can't be converted, or bytes that can't + be decoded using UTF-8: + - 'drop': drop the trytes from the result. + - 'strict': raise an exception. + - 'replace': replace with a placeholder character. + - 'ignore': omit the invalid tryte/byte sequence. + """ + decode_errors = 'strict' if errors == 'drop' else errors + + messages = [] + + i = 0 + while i < len(self): + txn = self[i] + + # Ignore inputs. Note that inputs are split into multiple + # transactions due to how big the signatures are. + if txn.value < 0: + i += AddressGenerator.DIGEST_ITERATIONS + continue + + message_trytes = TryteString(txn.signature_message_fragment) + + # If the message is long enough, it has to be split across + # multiple transactions. + for j in range(i+1, len(self)): + aux_txn = self[j] + if (aux_txn.address == txn.address) and (aux_txn.value == 0): + message_trytes += aux_txn.signature_message_fragment + i += 1 + else: + break + + # Ignore empty messages. + if message_trytes: + try: + messages.append(message_trytes.as_string(decode_errors)) + except (TrytesDecodeError, UnicodeDecodeError): + if errors != 'drop': + raise + + i += 1 + + return messages + def as_tryte_strings(self, head_to_tail=True): # type: (bool) -> List[TransactionTrytes] """ diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py index 3d06fa43..709bf146 100644 --- a/test/commands/extended/get_bundles_test.py +++ b/test/commands/extended/get_bundles_test.py @@ -6,7 +6,8 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota, TransactionHash +from iota import Address, BadApiResponse, Bundle, BundleHash, Fragment, Hash, \ + Iota, Tag, Transaction, TransactionHash, TransactionTrytes from iota.adapter import MockAdapter from iota.commands.extended.get_bundles import GetBundlesCommand from iota.filters import Trytes @@ -116,11 +117,13 @@ def test_fail_transaction_not_trytes(self): ) +# noinspection SpellCheckingInspection class GetBundlesCommandTestCase(TestCase): def setUp(self): super(GetBundlesCommandTestCase, self).setUp() self.adapter = MockAdapter() + self.command = GetBundlesCommand(self.adapter) def test_wireup(self): """ @@ -131,4 +134,303 @@ def test_wireup(self): GetBundlesCommand, ) - # :todo: Unit tests. + def test_single_transaction(self): + """ + Getting a bundle that contains a single transaction. + """ + transaction =\ + Transaction( + current_index = 0, + last_index = 0, + tag = Tag(b''), + timestamp = 1484960990, + value = 0, + + # These values are not relevant for 0-value transactions. + nonce = Hash(b''), + signature_message_fragment = Fragment(b''), + + # This value is computed automatically, so it has to be real. + hash_ = + TransactionHash( + b'TAOICZV9ZSXIZINMNRLOLCWNLL9IDKGVWTJITNGU' + b'HAIKLHZLBZWOQ9HJSODUDISTYGIYPWTYDCFMVRBQN' + ), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999OCSGVF' + b'IBQA99KGTCPCZ9NHR9VGLGADDDIEGGPCGBDEDDTBC' + ), + + bundle_hash = + BundleHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999DIOAZD' + b'M9AIUHXGVGBC9EMGI9SBVBAIXCBFJ9EELCPDRAD9U' + ), + + branch_transaction_hash = + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999BBCEDI' + b'ZHUDWBYDJEXHHAKDOCKEKDFIMB9AMCLFW9NBDEOFV' + ), + + trunk_transaction_hash = + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION999999ARAYA' + b'MHCB9DCFEIWEWDLBCDN9LCCBQBKGDDAECFIAAGDAS' + ), + ) + + self.adapter.seed_response('getTrytes', { + 'trytes': [transaction.as_tryte_string()], + }) + + response = self.command(transaction=transaction.hash) + + bundle = response['bundles'][0] # type: Bundle + self.assertEqual(len(bundle), 1) + + self.maxDiff = None + self.assertDictEqual( + bundle[0].as_json_compatible(), + transaction.as_json_compatible(), + ) + + def test_multiple_transactions(self): + """ + Getting a bundle that contains multiple transactions. + """ + bundle = Bundle.from_tryte_strings([ + TransactionTrytes( + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P' + b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999999999999999999' + b'999999999999HNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ' + b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9' + b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L' + b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW' + b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR' + ), + + # Well, it was bound to happen sooner or later... the ASCII + # representation of this tryte sequence contains a very naughty + # phrase. But I don't feel like doing another POW, so... enjoy. + TransactionTrytes( + b'NBTCPCFDEACCPCBDVC9DTCQAJ9RBTC9D9DCDQAEAKDCDFD9DSCFAJ9VBCDJDTCQAJ9' + b'ZBMDYBCCKB99999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999HNLFMVD99999999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXFSEWUNJOEGNU' + b'I9QOCRFMYSIFAZLJHKZBPQZZYFG9ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9' + b'999BGUEHHGAIWWQBCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJL' + b'EDAMYVRGABAWBY9999SYRABNN9JD9PNDLIKUNCECUELTOQZPSBDILVHJQVCEOICFAD' + b'YKZVGMOAXJRQNTCKMHGTAUMPGJJMX9LNF' + ), + ]) + + for txn in bundle: + self.adapter.seed_response('getTrytes', { + 'trytes': [txn.as_tryte_string()], + }) + + self.adapter.seed_response('getTrytes', { + 'trytes': [ + 'SPAMSPAMSPAM999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999999999999999999' + '999999999999999999999999999999999999999999999999999JECDITWO9999999' + '999999999999ONLFMVD99999999999999999999VVCHSQSRVFKSBONDWB9EAQEMQOY' + 'YRBIZHTBJLYNAVDHZPUZAZ9LYHXWKBEJ9IPR9FAMFLT9EEOHVYWUPRHHSRCILCLWFD' + 'GBYBFFOKMCSAPVD9VGZZRRGBLGMZMXD9RMZQDBLMGN9BATWZGULRBCYQEIKIRBPHC9' + '999KTLTRSYOWBD9HVNP9GCUABARNGMYXUZKXWRPGOPETZLKYYC9Z9EYXIWVARUBMBM' + 'BPXGORN9WPBLY99999ZRBVQWULRFXDNDYZKRKIXPZQT9JJJH9FZU9PVWZJWLXBPODP' + 'EHMKTTAGEPLPHUQCZNLDSHERONOMHJCOI' + ], + }) + + response = self.command( + transaction = + TransactionHash( + b'TOYJPHKMLQNDVLDHDILARUJCCIUMQBLUSWPCTIVA' + b'DRXICGYDGSVPXFTILFFGAPICYHGGJ9OHXINFX9999' + ), + ) + + self.maxDiff = None + self.assertListEqual( + response['bundles'][0].as_json_compatible(), + bundle.as_json_compatible(), + ) + + def test_non_tail_transaction(self): + """ + Trying to get a bundle for a non-tail transaction. + + This is not valid; you have to start with a tail transaction. + """ + self.adapter.seed_response('getTrytes', { + 'trytes': [ + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999WUQXEGBVIECGIWO9IGSYKWWPYCIVUJJGSJPWGIAFJPYSF9NSQOHWAHS9P' + b'9PWQHOBXNNQIF9IRHVQXKPZW999999999999999999999999999999999999999999' + b'999999999999HNLFMVD99A99999999A99999999PDQWLVVDPUU9VIBODGMRIAZPGQX' + b'DOGSEXIHKIBWSLDAWUKZCZMK9Z9YZSPCKBDJSVDPRQLJSTKUMTNVSXBGUEHHGAIWWQ' + b'BCJZHZAQOWZMAIDAFUZBVMUVPWQJLUGGQKNKLMGTWXXNZKUCBJLEDAMYVRGABAWBY9' + b'999MYIYBTGIOQYYZFJBLIAWMPSZEFFTXUZPCDIXSLLQDQSFYGQSQOGSPKCZNLVSZ9L' + b'MCUWVNGEN9EJEW9999XZUIENOTTBKJMDPRXWGQYG9PWGTXUO9AXMP9FLMDRMADLRPW' + b'CZCJBROYCDRJMYU9HDYJM9NDBFUPIZVTR' + ], + }) + + with self.assertRaises(BadApiResponse): + self.command( + transaction = + TransactionHash( + b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' + b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' + ), + ) + + def test_missing_transaction(self): + """ + Unable to find the requested transaction. + """ + self.adapter.seed_response('getTrytes', {'trytes': []}) + + with self.assertRaises(BadApiResponse): + self.command( + transaction = + TransactionHash( + b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' + b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' + ), + ) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 41a17619..27c0a58c 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -6,13 +6,15 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota +from mock import Mock, patch +from six import binary_type, text_type + +from iota import Address, BadApiResponse, Iota, TransactionHash from iota.adapter import MockAdapter from iota.commands.extended.get_inputs import GetInputsCommand, \ GetInputsRequestFilter from iota.crypto.types import Seed from iota.filters import Trytes -from six import binary_type, text_type class GetInputsRequestFilterTestCase(BaseFilterTestCase): @@ -33,7 +35,7 @@ def test_pass_happy_path(self): request = { 'seed': Seed(self.seed), 'start': 0, - 'end': 10, + 'stop': 10, 'threshold': 100, } @@ -54,7 +56,7 @@ def test_pass_compatible_types(self): # These values must still be integers, however. 'start': 42, - 'end': 86, + 'stop': 86, 'threshold': 99, }) @@ -65,7 +67,7 @@ def test_pass_compatible_types(self): { 'seed': Seed(self.seed), 'start': 42, - 'end': 86, + 'stop': 86, 'threshold': 99, }, ) @@ -85,7 +87,7 @@ def test_pass_optional_parameters_excluded(self): { 'seed': Seed(self.seed), 'start': 0, - 'end': None, + 'stop': None, 'threshold': None, } ) @@ -212,65 +214,65 @@ def test_fail_start_too_small(self): }, ) - def test_fail_end_string(self): + def test_fail_stop_string(self): """ - ``end`` is a string. + ``stop`` is a string. """ self.assertFilterErrors( { # Not valid; it must be an int. - 'end': '0', + 'stop': '0', 'seed': Seed(self.seed), }, { - 'end': [f.Type.CODE_WRONG_TYPE], + 'stop': [f.Type.CODE_WRONG_TYPE], }, ) - def test_fail_end_float(self): + def test_fail_stop_float(self): """ - ``end`` is a float. + ``stop`` is a float. """ self.assertFilterErrors( { # Even with an empty fpart, floats are not valid. # It's gotta be an int. - 'end': 8.0, + 'stop': 8.0, 'seed': Seed(self.seed), }, { - 'end': [f.Type.CODE_WRONG_TYPE], + 'stop': [f.Type.CODE_WRONG_TYPE], }, ) - def test_fail_end_too_small(self): + def test_fail_stop_too_small(self): """ - ``end`` is less than 0. + ``stop`` is less than 0. """ self.assertFilterErrors( { - 'end': -1, + 'stop': -1, 'seed': Seed(self.seed), }, { - 'end': [f.Min.CODE_TOO_SMALL], + 'stop': [f.Min.CODE_TOO_SMALL], }, ) - def test_fail_end_occurs_before_start(self): + def test_fail_stop_occurs_before_start(self): """ - ``end`` is less than ``start``. + ``stop`` is less than ``start``. """ self.assertFilterErrors( { 'start': 1, - 'end': 0, + 'stop': 0, 'seed': Seed(self.seed), }, @@ -282,18 +284,18 @@ def test_fail_end_occurs_before_start(self): def test_fail_interval_too_large(self): """ - ``end`` is way more than ``start``. + ``stop`` is way more than ``start``. """ self.assertFilterErrors( { 'start': 0, - 'end': GetInputsRequestFilter.MAX_INTERVAL + 1, + 'stop': GetInputsRequestFilter.MAX_INTERVAL + 1, 'seed': Seed(self.seed), }, { - 'end': [GetInputsRequestFilter.CODE_INTERVAL_TOO_BIG], + 'stop': [GetInputsRequestFilter.CODE_INTERVAL_TOO_BIG], }, ) @@ -350,12 +352,42 @@ def test_fail_threshold_too_small(self): class GetInputsCommandTestCase(TestCase): + # noinspection SpellCheckingInspection def setUp(self): super(GetInputsCommandTestCase, self).setUp() self.adapter = MockAdapter() self.command = GetInputsCommand(self.adapter) + # Define some valid tryte sequences that we can reuse between + # tests. + self.addy0 =\ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999FIODSG' + b'IC9CCIFCNBTBDFIEHHE9RBAEVGK9JECCLCPBIINAX', + + key_index = 0, + ) + + self.addy1 =\ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION999999EPCNH' + b'MBTEH9KDVFMHHESDOBTFFACCGBFGACEDCDDCGICIL', + + key_index = 1, + ) + + self.addy2 =\ + Address( + trytes = + b'TESTVALUE9DONTUSEINPRODUCTION99999YDOHWF' + b'U9PFOFHGKFACCCBGDALGI9ZBEBABFAMBPDSEQ9XHJ', + + key_index = 2, + ) + def test_wireup(self): """ Verify that the command is wired up correctly. @@ -365,30 +397,423 @@ def test_wireup(self): GetInputsCommand, ) - def test_start_and_end_with_threshold(self): + def test_stop_threshold_met(self): + """ + ``stop`` provided, balance meets ``threshold``. + """ + self.adapter.seed_response('getBalances', { + 'balances': [42, 29], + }) + + # 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. + mock_address_generator = Mock(return_value=[self.addy0, self.addy1]) + + with patch( + 'iota.crypto.addresses.AddressGenerator.get_addresses', + mock_address_generator, + ): + response = self.command( + seed = Seed.random(), + stop = 2, + threshold = 71, + ) + + self.assertEqual(response['totalBalance'], 71) + self.assertEqual(len(response['inputs']), 2) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + + self.assertEqual(input0, self.addy0) + self.assertEqual(input0.balance, 42) + self.assertEqual(input0.key_index, 0) + + input1 = response['inputs'][1] + self.assertIsInstance(input1, Address) + + self.assertEqual(input1, self.addy1) + self.assertEqual(input1.balance, 29) + self.assertEqual(input1.key_index, 1) + + def test_stop_threshold_not_met(self): """ - ``start`` and ``end`` values provided, with ``threshold``. + ``stop`` provided, balance does not meet ``threshold``. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.adapter.seed_response('getBalances', { + 'balances': [42, 29], + }) + + # 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. + mock_address_generator = Mock(return_value=[self.addy0, self.addy1]) + + with patch( + 'iota.crypto.addresses.AddressGenerator.get_addresses', + mock_address_generator, + ): + with self.assertRaises(BadApiResponse): + self.command( + seed = Seed.random(), + stop = 2, + threshold = 72, + ) + + def test_stop_threshold_zero(self): + """ + ``stop`` provided, ``threshold`` is 0. + """ + # Note that the first address has a zero balance. + self.adapter.seed_response('getBalances', { + 'balances': [0, 1], + }) + + # 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. + mock_address_generator = Mock(return_value=[self.addy0, self.addy1]) + + with patch( + 'iota.crypto.addresses.AddressGenerator.get_addresses', + mock_address_generator, + ): + response = self.command( + seed = Seed.random(), + stop = 2, + threshold = 0, + ) + + self.assertEqual(response['totalBalance'], 1) + self.assertEqual(len(response['inputs']), 1) - def test_start_and_end_no_threshold(self): + # Address 0 was skipped because it has a zero balance. + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + + self.assertEqual(input0, self.addy1) + self.assertEqual(input0.balance, 1) + self.assertEqual(input0.key_index, 1) + + def test_stop_no_threshold(self): """ - ``start`` and ``end`` values provided, no ``threshold``. + ``stop`` provided, no ``threshold``. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.adapter.seed_response('getBalances', { + 'balances': [42, 29], + }) + + # 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. + mock_address_generator = Mock(return_value=[self.addy0, self.addy1]) + + with patch( + 'iota.crypto.addresses.AddressGenerator.get_addresses', + mock_address_generator, + ): + response = self.command( + seed = Seed.random(), + start = 0, + stop = 2, + ) + + self.assertEqual(response['totalBalance'], 71) + self.assertEqual(len(response['inputs']), 2) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + + self.assertEqual(input0, self.addy0) + self.assertEqual(input0.balance, 42) + self.assertEqual(input0.key_index, 0) + + input1 = response['inputs'][1] + self.assertIsInstance(input1, Address) + + self.assertEqual(input1, self.addy1) + self.assertEqual(input1.balance, 29) + self.assertEqual(input1.key_index, 1) + + def test_no_stop_threshold_met(self): + """ + No ``stop`` provided, balance meets ``threshold``. + """ + self.adapter.seed_response('getBalances', { + 'balances': [42, 29], + }) + + # ``getInputs`` uses ``findTransactions`` to identify unused + # addresses. + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999WBL9KD' + b'EIZDMEDFPEYDIIA9LEMEUCC9MFPBY9TEVCUGSEGGN' + ), + ], + }) + + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' + b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' + ), + ], + }) + + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + # 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): + for addy in [self.addy0, self.addy1, self.addy2][start::step]: + yield addy + + # When ``stop`` is None, the command uses a generator internally. + with patch( + 'iota.crypto.addresses.AddressGenerator.create_generator', + mock_address_generator, + ): + response = self.command( + seed = Seed.random(), + threshold = 71, + ) + + self.assertEqual(response['totalBalance'], 71) + self.assertEqual(len(response['inputs']), 2) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + + self.assertEqual(input0, self.addy0) + self.assertEqual(input0.balance, 42) + self.assertEqual(input0.key_index, 0) + + input1 = response['inputs'][1] + self.assertIsInstance(input1, Address) - def test_no_end_with_threshold(self): + self.assertEqual(input1, self.addy1) + self.assertEqual(input1.balance, 29) + self.assertEqual(input1.key_index, 1) + + def test_no_stop_threshold_not_met(self): """ - No ``end`` value provided, with ``threshold``. + No ``stop`` provided, balance does not meet ``threshold``. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.adapter.seed_response('getBalances', { + 'balances': [42, 29, 0], + }) + + # 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): + for addy in [self.addy0, self.addy1, self.addy2][start::step]: + yield addy + + # When ``stop`` is None, the command uses a generator internally. + with patch( + 'iota.crypto.addresses.AddressGenerator.create_generator', + mock_address_generator, + ): + with self.assertRaises(BadApiResponse): + self.command( + seed = Seed.random(), + threshold = 72, + ) + + def test_no_stop_threshold_zero(self): + """ + No ``stop`` provided, ``threshold`` is 0. + """ + # Note that the first address has a zero balance. + self.adapter.seed_response('getBalances', { + 'balances': [0, 1], + }) + + # ``getInputs`` uses ``findTransactions`` to identify unused + # addresses. + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999WBL9KD' + b'EIZDMEDFPEYDIIA9LEMEUCC9MFPBY9TEVCUGSEGGN' + ), + ], + }) + + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' + b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' + ), + ], + }) + + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) - def test_no_end_no_threshold(self): + # 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): + for addy in [self.addy0, self.addy1, self.addy2][start::step]: + yield addy + + # When ``stop`` is None, the command uses a generator internally. + with patch( + 'iota.crypto.addresses.AddressGenerator.create_generator', + mock_address_generator, + ): + response = self.command( + seed = Seed.random(), + threshold = 0, + ) + + self.assertEqual(response['totalBalance'], 1) + self.assertEqual(len(response['inputs']), 1) + + # Because the first address had a zero balance, it was skipped. + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + + self.assertEqual(input0, self.addy1) + self.assertEqual(input0.balance, 1) + self.assertEqual(input0.key_index, 1) + + def test_no_stop_no_threshold(self): + """ + No ``stop`` provided, no ``threshold``. + """ + self.adapter.seed_response('getBalances', { + 'balances': [42, 29], + }) + + # ``getInputs`` uses ``findTransactions`` to identify unused + # addresses. + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999WBL9KD' + b'EIZDMEDFPEYDIIA9LEMEUCC9MFPBY9TEVCUGSEGGN' + ), + ], + }) + + # noinspection SpellCheckingInspection + self.adapter.seed_response('findTransactions', { + 'hashes': [ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999YFXGOD' + b'GISBJAX9PDJIRDMDV9DCRDCAEG9FN9KECCBDDFZ9H' + ), + ], + }) + + self.adapter.seed_response('findTransactions', { + 'hashes': [], + }) + + # 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): + for addy in [self.addy0, self.addy1, self.addy2][start::step]: + yield addy + + # When ``stop`` is None, the command uses a generator internally. + with patch( + 'iota.crypto.addresses.AddressGenerator.create_generator', + mock_address_generator, + ): + response = self.command( + seed = Seed.random(), + ) + + self.assertEqual(response['totalBalance'], 71) + self.assertEqual(len(response['inputs']), 2) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + + self.assertEqual(input0, self.addy0) + self.assertEqual(input0.balance, 42) + self.assertEqual(input0.key_index, 0) + + input1 = response['inputs'][1] + self.assertIsInstance(input1, Address) + + self.assertEqual(input1, self.addy1) + self.assertEqual(input1.balance, 29) + self.assertEqual(input1.key_index, 1) + + def test_start(self): """ - No ``end`` value provided, no ``threshold``. + Using ``start`` to offset the key range. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + 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': [], + }) + + # 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): + # If ``start`` has the wrong value, return garbage to make the + # test asplode. + for addy in [None, self.addy1, self.addy2][start::step]: + yield addy + + # When ``stop`` is None, the command uses a generator internally. + with patch( + 'iota.crypto.addresses.AddressGenerator.create_generator', + mock_address_generator, + ): + response = self.command( + seed = Seed.random(), + start = 1, + ) + + self.assertEqual(response['totalBalance'], 86) + self.assertEqual(len(response['inputs']), 1) + + input0 = response['inputs'][0] + self.assertIsInstance(input0, Address) + + self.assertEqual(input0, self.addy1) + self.assertEqual(input0.balance, 86) + self.assertEqual(input0.key_index, 1) diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index b7d30ae8..e1131f0f 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -55,7 +55,7 @@ def test_pass_optional_parameters_excluded(self): { 'seed': Seed(self.seed), - 'index': None, + 'index': 0, 'count': None, }, ) diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index de467393..b01b3cf9 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -1372,35 +1372,23 @@ def test_pass_inputs_implicit_no_change(self): # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` mock_get_inputs = Mock(return_value={ 'inputs': [ - { - 'address': - Address( - trytes = - b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' - b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', - - balance = 13, - key_index = 4, - ), - - 'balance': 13, - 'keyIndex': 4, - }, + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', - { - 'address': - Address( - trytes = - b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' - b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + balance = 13, + key_index = 4, + ), - balance = 29, - key_index = 5, - ), + Address( + trytes = + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', - 'balance': 29, - 'keyIndex': 5, - }, + balance = 29, + key_index = 5, + ), ], 'totalBalance': 42, @@ -1832,20 +1820,14 @@ def test_pass_inputs_implicit_with_change(self): # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` mock_get_inputs = Mock(return_value={ 'inputs': [ - { - 'address': - Address( - trytes = - b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' - b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', - balance = 86, - key_index = 4, - ), - - 'balance': 86, - 'keyIndex': 4, - }, + balance = 86, + key_index = 4, + ), ], 'totalBalance': 86, diff --git a/test/transaction_test.py b/test/transaction_test.py index 8238672f..f09567b0 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -6,6 +6,7 @@ from unittest import TestCase from mock import patch +from six import binary_type from iota import Address, Bundle, BundleHash, Fragment, Hash, ProposedBundle, \ ProposedTransaction, Tag, Transaction, TransactionHash, TransactionTrytes, \ @@ -13,7 +14,350 @@ from iota.crypto.addresses import AddressGenerator from iota.crypto.signing import KeyGenerator from iota.transaction import BundleValidator -from six import binary_type + + +class BundleTestCase(TestCase): + # noinspection SpellCheckingInspection + def setUp(self): + super(BundleTestCase, self).setUp() + + self.bundle = Bundle([ + # This transaction does not have a message. + Transaction( + signature_message_fragment = Fragment(b''), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999A9PG9A' + b'XCQANAWGJBTFWEAEQCN9WBZB9BJAIIY9UDLIGFOAA' + ), + + current_index = 0, + last_index = 7, + value = 0, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + + # This transaction has something that can't be decoded as a UTF-8 + # sequence. + Transaction( + signature_message_fragment = + Fragment(b'OHCFVELH9GYEMHCF9GPHBGIEWHZFU'), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999HAA9UA' + b'MHCGKEUGYFUBIARAXBFASGLCHCBEVGTBDCSAEBTBM' + ), + + current_index = 1, + last_index = 7, + value = 10, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + + # This transaction has a message that fits into a single + # fragment. + Transaction( + signature_message_fragment = + Fragment.from_string('Hello, world!'), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999D99HEA' + b'M9XADCPFJDFANCIHR9OBDHTAGGE9TGCI9EO9ZCRBN' + ), + + current_index = 2, + last_index = 7, + value = 20, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + + # This transaction has a message that spans multiple fragments. + Transaction( + signature_message_fragment = + Fragment( + b'J9GAQBCDCDSCEAADCDFDBDXCBDVCQAGAEAGDPCXCSCEANBTCTCDDEACCWCCDIDVC' + b'WCHDEAPCHDEA9DPCGDHDSAJ9GAOBFDSASASAEAQBCDCDSCEAADCDFDBDXCBDVCQA' + b'EAYBEANBTCTCDDEACCWCCDIDVCWCHDQAGAEAGDPCXCSCEAVBCDCDBDEDIDPCKD9D' + b'EABDTCFDJDCDIDGD9DMDSAJ9EAEAGANBCDEAMDCDIDEAWCPCJDTCSASASAEATCFD' + b'QAEAHDWCPCHDEAXCGDSASASAGAJ9GASASASAEAPCBDEAPCBDGDKDTCFDEAUCCDFD' + b'EAMDCDIDIBGAEAXCBDHDTCFDFDIDDDHDTCSCEANBTCTCDDEACCWCCDIDVCWCHDEA' + b'ADPCYCTCGDHDXCRCPC9D9DMDSAEAGAHCTCGDSAEASBEAWCPCJDTCSAGAJ9CCWCTC' + b'EAHDKDCDEAADTCBDEAGDWCXCJDTCFDTCSCEAKDXCHDWCEATCLDDDTCRCHDPCBDRC' + b'MDSAEACCWCTCXCFDEAKDPCXCHDXCBDVCEAWCPCSCEABDCDHDEAQCTCTCBDEAXCBD' + b'EAJDPCXCBDSAJ9GACCWCTCFDTCEAFDTCPC9D9DMDEAXCGDEACDBDTCIBGAEAQCFD' + b'TCPCHDWCTCSCEAZBWCCDIDRCWCVCSAJ9GACCWCTCFDTCEAFDTCPC9D9DMDEAXCGD' + b'EACDBDTCQAGAEARCCDBDUCXCFDADTCSCEANBTCTCDDEACCWCCDIDVCWCHDSAJ9GA' + b'CCCDEAOBJDTCFDMDHDWCXCBDVCIBEACCCDEAHDWCTCEAVCFDTCPCHDEA9CIDTCGD' + b'HDXCCDBDEACDUCEAVBXCUCTCQAEAHDWCTCEADCBDXCJDTCFDGDTCEAPCBDSCEAOB' + b'JDTCFDMDHDWCXCBDVCIBGAJ9GAHCTCGDSAGAJ9LBCDHDWCEACDUCEAHDWCTCEAAD' + b'TCBDEAWCPCSCEAQCTCTCBDEAHDFDPCXCBDTCSCEAUCCDFDEAHDWCXCGDEAADCDAD' + b'TCBDHDEBEAHDWCTCXCFDEA9DXCJDTCGDEAWCPCSCEAQCTCTCBDEAPCJ9EAEADDFD' + b'TCDDPCFDPCHDXCCDBDEAUCCDFDEAXCHDEBEAHDWCTCMDEAWCPCSCEAQCTCTCBDEA' + b'GDTC9DTCRCHDTCSCEAPCHDEAQCXCFDHDWCEAPCGDEAHDWCCDGDTCEAKDWCCDEAKD' + b'CDID9DSCJ9EAEAKDXCHDBDTCGDGDEAHDWCTCEAPCBDGDKDTCFDEBEAQCIDHDEATC' + b'JDTCBDEAGDCDEAHDWCTCMDEAUCCDIDBDSCEAHDWCTCADGDTC9DJDTCGDEAVCPCGD' + b'DDXCBDVCEAPCBDSCEAGDEDIDXCFDADXCBDVCJ9EAEA9DXCZCTCEATCLDRCXCHDTC' + b'SCEARCWCXC9DSCFDTCBDSAJ9GAKBBDSCEAMDCDIDLAFDTCEAFDTCPCSCMDEAHDCD' + b'EAVCXCJDTCEAXCHDEAHDCDEAIDGDIBGAEAIDFDVCTCSCEAVBCDCDBDEDIDPCKD9D' + b'SAJ9GASBEAPCADSAGAJ9GAXBCDKDIBGAJ9GAXBCDKDQAGAEAGDPCXCSCEANBTCTC' + b'DDEACCWCCDIDVCWCHDSAJ9CCWCTCMDEAQCCDHDWCEA9DXCRCZCTCSCEAHDWCTCXC' + b'FDEASCFDMDEA9DXCDDGDSAJ9GACCWCCDIDVCWCEASBEASCCDBDLAHDEAHDWCXCBD' + b'ZCQAGAEAPCSCSCTCSCEANBTCTCDDEACCWCCDIDVCWCHDQAEAGAHDWCPCHDEAMDCD' + b'IDLAFDTCEAVCCDXCBDVCEAHDCDEA9DXCZCTCEAXCHDSAGAJ9GANBCDTCGDBDLAHD' + b'EAADPCHDHDTCFDQAGAEAGDPCXCSCEAZBWCCDIDRCWCVCSAEAGAFCTCEAADIDGDHD' + b'EAZCBDCDKDEAXCHDFAEAXBCDKDFAGAJ9GAXBCDKDIBGAEATCBDEDIDXCFDTCSCEA' + b'NBTCTCDDEACCWCCDIDVCWCHDSAJ9GAHCTCGDFAEAXBCDKDFAGAJ9GAKB9D9DEAFD' + b'XCVCWCHDQAGAEAGDPCXCSCEAHDWCTCEARCCDADDDIDHDTCFDEAPCBDSCEAGDTCHD' + b'HD9DTCSCEAXCBDHDCDEAGDXC9DTCBDRCTCEAPCVCPCXCBDSAJ9EAEACCWCTCEAHD' + b'KDCDEAADTCB' + ), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999A9PG9A' + b'XCQANAWGJBTFWEAEQCN9WBZB9BJAIIY9UDLIGFOAA' + ), + + current_index = 3, + last_index = 7, + value = 30, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + + Transaction( + signature_message_fragment = + Fragment( + b'DEAUCXCSCVCTCHDTCSCSAEACCWCTCEAHDTCBDGDXCCDBDEAKDPCGDEAIDBDQCTCP' + b'CFDPCQC9DTCSAJ9GAHCCDIDLAFDTCEAFDTCPC9D9DMDEABDCDHDEAVCCDXCBDVCE' + b'AHDCDEA9DXCZCTCEAXCHDQAGAEACDQCGDTCFDJDTCSCEANBTCTCDDEACCWCCDIDV' + b'CWCHDSAJ9GACCTC9D9DEAIDGDFAGAJ9GAKB9D9DEAFDXCVCWCHDQAGAEAGDPCXCS' + b'CEANBTCTCDDEACCWCCDIDVCWCHDSAEAGACCWCTCEAKBBDGDKDTCFDEAHDCDEAHDW' + b'CTCEAQBFDTCPCHDEA9CIDTCGDHDXCCDBDSASASAGAJ9GAHCTCGDIBGAJ9GAYBUCE' + b'AVBXCUCTCQAEAHDWCTCEADCBDXCJDTCFDGDTCEAPCBDSCEAOBJDTCFDMDHDWCXCB' + b'DVCSASASAGAEAGDPCXCSCEANBTCTCDDEACCWCCDIDVCWCHDSAJ9GAHCTCGDIBIBG' + b'AJ9GASBGDSASASAGAJ9GAHCTCGDIBFAGAJ9GAPBCDFDHDMDRAHDKDCDQAGAEAGDP' + b'CXCSCEANBTCTCDDEACCWCCDIDVCWCHDQAEAKDXCHDWCEAXCBDUCXCBDXCHDTCEAA' + b'DPCYCTCGDHDMDEAPCBDSCEARCPC9DADSAJ9EAEAEAEAEAEAEAEA' + ), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999A9PG9A' + b'XCQANAWGJBTFWEAEQCN9WBZB9BJAIIY9UDLIGFOAA' + ), + + current_index = 4, + last_index = 7, + value = 0, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + + # Input, Part 1 of 2 + Transaction( + # Make the signature look like a message, so we can verify that + # the Bundle skips it correctly. + signature_message_fragment = + Fragment.from_string('This is a signature, not a message!'), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999WGSBUA' + b'HDVHYHOBHGP9VCGIZHNCAAQFJGE9YHEHEFTDAGXHY' + ), + + current_index = 5, + last_index = 7, + value = -100, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + + # Input, Part 2 of 2 + Transaction( + # Make the signature look like a message, so we can verify that + # the Bundle skips it correctly. + signature_message_fragment = + Fragment.from_string('This is a signature, not a message!'), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999WGSBUA' + b'HDVHYHOBHGP9VCGIZHNCAAQFJGE9YHEHEFTDAGXHY' + ), + + current_index = 6, + last_index = 7, + value = 0, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + + # Change + Transaction( + # It's unusual for a change transaction to have a message, but + # half the fun of unit tests is designing unusual scenarios! + signature_message_fragment = + Fragment.from_string('I can haz change?'), + + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999FFYALH' + b'N9ACYCP99GZBSDK9CECFI9RAIH9BRCCAHAIAWEFAN' + ), + + current_index = 7, + last_index = 7, + value = 40, + + # These values are not relevant to the tests. + branch_transaction_hash = TransactionHash(b''), + bundle_hash = BundleHash(b''), + hash_ = TransactionHash(b''), + nonce = Hash(b''), + tag = Tag(b''), + timestamp = 1485020456, + trunk_transaction_hash = TransactionHash(b''), + ), + ]) + + def test_get_messages_errors_drop(self): + """ + Decoding messages from a bundle, with ``errors='drop'``. + """ + messages = self.bundle.get_messages('drop') + + self.assertEqual(len(messages), 3) + + self.assertEqual(messages[0], 'Hello, world!') + + # noinspection SpellCheckingInspection + self.assertEqual( + messages[1], + + ''' +"Good morning," said Deep Thought at last. +"Er... Good morning, O Deep Thought," said Loonquawl nervously. + "Do you have... er, that is..." +"... an answer for you?" interrupted Deep Thought majestically. "Yes. I have." +The two men shivered with expectancy. Their waiting had not been in vain. +"There really is one?" breathed Phouchg. +"There really is one," confirmed Deep Thought. +"To Everything? To the great Question of Life, the Universe and Everything?" +"Yes." +Both of the men had been trained for this moment; their lives had been a + preparation for it; they had been selected at birth as those who would + witness the answer; but even so they found themselves gasping and squirming + like excited children. +"And you're ready to give it to us?" urged Loonquawl. +"I am." +"Now?" +"Now," said Deep Thought. +They both licked their dry lips. +"Though I don't think," added Deep Thought, "that you're going to like it." +"Doesn't matter," said Phouchg. "We must know it! Now!" +"Now?" enquired Deep Thought. +"Yes! Now!" +"All right," said the computer and settled into silence again. + The two men fidgeted. The tension was unbearable. +"You're really not going to like it," observed Deep Thought. +"Tell us!" +"All right," said Deep Thought. "The Answer to the Great Question..." +"Yes?" +"Of Life, the Universe and Everything..." said Deep Thought. +"Yes??" +"Is..." +"Yes?!" +"Forty-two," said Deep Thought, with infinite majesty and calm. + ''', + ) + + self.assertEqual(messages[2], 'I can haz change?') + + def test_get_messages_errors_strict(self): + """ + Decoding messages from a bundle, with ``errors='strict'``. + """ + with self.assertRaises(UnicodeDecodeError): + self.bundle.get_messages('strict') + + def test_get_messages_errors_ignore(self): + """ + Decoding messages from a bundle, with ``errors='ignore'``. + """ + messages = self.bundle.get_messages('ignore') + + self.assertEqual(len(messages), 4) + + # The only message that is treated differently is the invalid one. + self.assertEqual(messages[0], '祝你好运\x15') + + def test_get_messages_errors_replace(self): + """ + Decoding messages from a bundle, with ``errors='replace'``. + """ + messages = self.bundle.get_messages('replace') + + self.assertEqual(len(messages), 4) + + # The only message that is treated differently is the invalid one. + self.assertEqual(messages[0], '祝你好运�\x15') class BundleValidatorTestCase(TestCase):