From d30c8e510dac0676d91b55646543107f2d751066 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 27 Nov 2016 13:22:35 -0500 Subject: [PATCH 001/239] Attached some metadata to the project. --- .gitignore | 6 ++++++ iota/__init__.py | 4 ++++ setup.cfg | 2 ++ setup.py | 37 +++++++++++++++++++++++++++++++++++++ 4 files changed, 49 insertions(+) create mode 100644 .gitignore create mode 100644 iota/__init__.py create mode 100644 setup.cfg create mode 100644 setup.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..92364c3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,6 @@ +*.pyc + +# :see: https://packaging.python.org/distributing/ +build/* +dist/* +PyOTA.egg-info/* diff --git a/iota/__init__.py b/iota/__init__.py new file mode 100644 index 0000000..57de310 --- /dev/null +++ b/iota/__init__.py @@ -0,0 +1,4 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, unicode_literals + +__version__ = '1.0.0' \ No newline at end of file diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..3480374 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,2 @@ +[bdist_wheel] +universal=1 \ No newline at end of file diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..87b29db --- /dev/null +++ b/setup.py @@ -0,0 +1,37 @@ +#!/usr/bin/env python +# coding=utf-8 +from __future__ import absolute_import, division, print_function, unicode_literals + +from setuptools import setup + +from iota import __version__ + +setup( + name = 'PyOTA', + description = 'IOTA API library for Python', + url = 'https://github.com/iotaledger/iota.lib.py', + version = __version__, + packages = ['iota'], + + data_files = [ + ('', ['LICENSE']), + ], + + license = 'MIT', + + classifiers = [ + 'Development Status :: 3 - Alpha', + 'Intended Audience :: Developers', + 'License :: OSI Approved :: MIT License', + 'Programming Language :: Python :: 2', + 'Programming Language :: Python :: 2.7', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Topic :: Software Development :: Libraries :: Python Modules', + ], + + keywords = 'iota,tangle,iot,internet of things,api,library', + + author = 'Phoenix Zerin', + author_email = 'phx@phx.ph', +) \ No newline at end of file From 0614bfd77ef84fefaa6d04546b89a0f830ecb005 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 27 Nov 2016 13:53:44 -0500 Subject: [PATCH 002/239] Stubbed out unit test integration. --- .gitignore | 14 +++++++++++++- iota/__init__.py | 2 +- setup.cfg | 2 +- setup.py | 2 +- tox.ini | 12 ++++++++++++ 5 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 tox.ini diff --git a/.gitignore b/.gitignore index 92364c3..56ad972 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,18 @@ -*.pyc +# Python metadata +*.py[co] +# Generated distribution files. # :see: https://packaging.python.org/distributing/ build/* dist/* PyOTA.egg-info/* + +# Virtualenvs for unit tests. +# :see: https://tox.readthedocs.io/en/latest/ +.tox/* + +# +# Note: For environment- or IDE-specific metadata (e.g., .DS_Store, .idea, etc. +# you can add these to your own "global" .gitignore file. +# :see: https://help.github.com/articles/ignoring-files/#create-a-global-gitignore +# diff --git a/iota/__init__.py b/iota/__init__.py index 57de310..6fe5e43 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -1,4 +1,4 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, unicode_literals -__version__ = '1.0.0' \ No newline at end of file +__version__ = '1.0.0' diff --git a/setup.cfg b/setup.cfg index 3480374..3c6e79c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,2 +1,2 @@ [bdist_wheel] -universal=1 \ No newline at end of file +universal=1 diff --git a/setup.py b/setup.py index 87b29db..14042c8 100644 --- a/setup.py +++ b/setup.py @@ -34,4 +34,4 @@ author = 'Phoenix Zerin', author_email = 'phx@phx.ph', -) \ No newline at end of file +) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..9453a08 --- /dev/null +++ b/tox.ini @@ -0,0 +1,12 @@ +# Tox (http://tox.testrun.org/) is a tool for running tests +# in multiple virtualenvs. This configuration file will run the +# test suite on all supported python versions. To use it, "pip install tox" +# and then run "tox" from this directory. + +[tox] +envlist = py27, py35 + +[testenv] +commands = nosetests +deps = + nose From 57286aa980fcb8ea918d6044dce572eee47fa7a9 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 27 Nov 2016 13:54:36 -0500 Subject: [PATCH 003/239] Added a little documentation. --- README.md | 42 ++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 870e5ef..2a66e45 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,40 @@ -# iota.lib.py -IOTA Python Library +# PyOTA +This is the official Python library for the IOTA Core. +It implements both the [official API](https://iota.readme.io/), as well as + newly proposed functionality (such as signing, bundles, utilities and + conversion). + +> **Join the Discussion** +> If you want to get involved in the community, need help with getting setup, +> have any issues related with the library or just want to discuss Blockchain, +> Distributed Ledgers and IoT with other people, feel free to join our +> [Slack](http://slack.iotatoken.com/). +> You can also ask questions on our +> [dedicated forum](http://forum.iotatoken.com/). + +# Dependencies +PyOTA requires Python v3.5 or v2.7. + +# Installation +To install the latest stable version: +``` +pip install https://github.com/iotaledger/iota.lib.py/archive/master.zip +``` + +To install the development version: +``` +pip install https://github.com/iotaledger/iota.lib.py/archive/develop.zip +``` + +# Documentation +For the full documentation of this library, please refer to the + [official API](https://iota.readme.io/) + +# Contributing +## Running Unit Tests +To run unit tests for the project: + +``` +pip install tox +tox +``` From 0909a83e97e20c3111cb9f203a878a50550fc2a9 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 27 Nov 2016 15:15:34 -0500 Subject: [PATCH 004/239] Updated documentation for renamed repo. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a66e45..709b41c 100644 --- a/README.md +++ b/README.md @@ -18,12 +18,12 @@ PyOTA requires Python v3.5 or v2.7. # Installation To install the latest stable version: ``` -pip install https://github.com/iotaledger/iota.lib.py/archive/master.zip +pip install https://github.com/iotaledger/pyota/archive/master.zip ``` To install the development version: ``` -pip install https://github.com/iotaledger/iota.lib.py/archive/develop.zip +pip install https://github.com/iotaledger/pyota/archive/develop.zip ``` # Documentation From 5ce19eddd949139605ccc374debd335c838bbdcb Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 28 Nov 2016 18:41:24 -0500 Subject: [PATCH 005/239] PoC for `getNodeInfo`. --- examples/__init__.py | 3 ++ examples/hello_world.py | 61 +++++++++++++++++++++++++++++++++ iota/__init__.py | 12 ++++++- iota/adapter.py | 74 +++++++++++++++++++++++++++++++++++++++++ iota/api.py | 47 ++++++++++++++++++++++++++ setup.py | 9 ++++- 6 files changed, 204 insertions(+), 2 deletions(-) create mode 100644 examples/__init__.py create mode 100644 examples/hello_world.py create mode 100644 iota/adapter.py create mode 100644 iota/api.py diff --git a/examples/__init__.py b/examples/__init__.py new file mode 100644 index 0000000..3f3d02d --- /dev/null +++ b/examples/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/examples/hello_world.py b/examples/hello_world.py new file mode 100644 index 0000000..01d2342 --- /dev/null +++ b/examples/hello_world.py @@ -0,0 +1,61 @@ +# coding=utf-8 +""" +Simple "Hello, world!" example that sends a `getNodeInfo` command to + your friendly neighborhood node. +""" + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from argparse import ArgumentParser +from pprint import pprint +from sys import argv +from typing import Text + +from six import text_type as text + +from iota import BadApiResponse, DEFAULT_PORT, HttpAdapter, IotaApi, \ + __version__ + + +def main(host, port): + # type: (Text, int) -> None + api = IotaApi(HttpAdapter(host, port)) + + try: + node_info = api.get_node_info() + except BadApiResponse as e: + print("Looks like {host}:{port} isn't very talkative today ):") + print(e) + else: + print('Hello {host}:{port}!'.format(host=host, port=port)) + pprint(node_info) + + +if __name__ == '__main__': + parser = ArgumentParser( + description = __doc__, + epilog = 'PyOTA v{version}'.format(version=__version__), + ) + + parser.add_argument( + '--host', + type = text, + default = 'localhost', + + help = + 'Hostname or IP address of the node to connect to ' + '(defaults to localhost).', + ) + + parser.add_argument( + '--port', + type = int, + default = DEFAULT_PORT, + + help = 'Port number to connect to (defaults to {default}).'.format( + default = DEFAULT_PORT, + ), + ) + + main(**vars(parser.parse_args(argv[1:]))) diff --git a/iota/__init__.py b/iota/__init__.py index 6fe5e43..cdb1a32 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -1,4 +1,14 @@ # coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +DEFAULT_PORT = 14265 + +# Make some imports accessible from the top level of the package. +# noinspection PyUnresolvedReferences +from .adapter import * +# noinspection PyUnresolvedReferences +from .api import * + __version__ = '1.0.0' diff --git a/iota/adapter.py b/iota/adapter.py new file mode 100644 index 0000000..a89be43 --- /dev/null +++ b/iota/adapter.py @@ -0,0 +1,74 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from abc import ABCMeta, abstractmethod as abstract_method +from socket import getdefaulttimeout as get_default_timeout +from typing import Text + +import requests +from six import with_metaclass + +from iota import DEFAULT_PORT + +__all__ = [ + 'BadApiResponse', + 'HttpAdapter', +] + + +class BadApiResponse(ValueError): + """ + Indicates that a non-success response was received from the node. + """ + pass + + +class BaseAdapter(with_metaclass(ABCMeta)): + """ + Interface for IOTA API adapters. + + Adapters make it easy to customize the way an IotaApi instance + communicates with a node. + """ + @abstract_method + def send_request(self, payload): + # type: (dict) -> dict + """ + Sends an API request to the node. + + :param payload: JSON payload. + + :return: Decoded response from the node. + :raise: BadApiResponse if a non-success response was received. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + +class HttpAdapter(BaseAdapter): + """ + Sends standard HTTP requests. + """ + def __init__(self, host, port=DEFAULT_PORT): + # type: (Text, int) -> None + super(HttpAdapter, self).__init__() + + self.host = host + self.port = port + + @property + def node_url(self): + # type: () -> Text + """Returns the node URL.""" + return 'http://{host}:{port}/'.format( + host = self.host, + port = self.port, + ) + + def send_request(self, payload, **kwargs): + # type: (dict, dict) -> dict + kwargs.setdefault('timeout', get_default_timeout()) + response = requests.post(self.node_url, json=payload, **kwargs) + return response.json() diff --git a/iota/api.py b/iota/api.py new file mode 100644 index 0000000..6533b5f --- /dev/null +++ b/iota/api.py @@ -0,0 +1,47 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Text + +from iota.adapter import BaseAdapter + +__all__ = [ + 'IotaApi', +] + + +class IotaApi(object): + """ + API to send HTTP requests for communicating with an IOTA node. + + :see: https://iota.readme.io/docs/getting-started + """ + def __init__(self, adapter): + # type: (BaseAdapter) -> None + super(IotaApi, self).__init__() + + self.adapter = adapter # type: BaseAdapter + + def __call__(self, command, **kwargs): + # type: (Text, dict) -> dict + """ + Sends an arbitrary API command to the node. + + This method is useful for invoking unsupported or experimental + methods, or if you just want to troll your node for awhile. + + :param command: The name of the command to send. + :param kwargs: Additional parameters to send with the command. + + :return: Decoded response from the node. + """ + return self.adapter.send_request(dict(command=command, **kwargs)) + + def get_node_info(self): + """ + Returns information about the node. + + :see: https://iota.readme.io/docs/getnodeinfo + """ + return self.__call__('getNodeInfo') diff --git a/setup.py b/setup.py index 14042c8..762e167 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/env python # coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import absolute_import, division, print_function, \ + unicode_literals from setuptools import setup @@ -13,6 +14,12 @@ version = __version__, packages = ['iota'], + install_requires = [ + 'requests', + 'six', + 'typing ; python_version < "3.5"', + ], + data_files = [ ('', ['LICENSE']), ], From 8eb1a11155027091b68ad6e05c6fe7a96a6397b0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 28 Nov 2016 18:44:57 -0500 Subject: [PATCH 006/239] Fixed error during fresh install. --- iota/__init__.py | 1 + setup.py | 4 +--- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/iota/__init__.py b/iota/__init__.py index cdb1a32..c0e0430 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -11,4 +11,5 @@ from .api import * +# Don't forget to update version number in setup.py! __version__ = '1.0.0' diff --git a/setup.py b/setup.py index 762e167..9b560b1 100644 --- a/setup.py +++ b/setup.py @@ -5,13 +5,11 @@ from setuptools import setup -from iota import __version__ - setup( name = 'PyOTA', description = 'IOTA API library for Python', url = 'https://github.com/iotaledger/iota.lib.py', - version = __version__, + version = '1.0.0', packages = ['iota'], install_requires = [ From 82c68ff71c6d1760cca5af8d04d878783f68746b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 28 Nov 2016 20:04:50 -0500 Subject: [PATCH 007/239] Implemented error handling for HttpAdapter. --- iota/adapter.py | 42 +++++++++++++-- test/__init__.py | 3 ++ test/adapter_test.py | 122 +++++++++++++++++++++++++++++++++++++++++++ tox.ini | 1 + 4 files changed, 164 insertions(+), 4 deletions(-) create mode 100644 test/__init__.py create mode 100644 test/adapter_test.py diff --git a/iota/adapter.py b/iota/adapter.py index a89be43..8f4647f 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import json from abc import ABCMeta, abstractmethod as abstract_method from socket import getdefaulttimeout as get_default_timeout from typing import Text @@ -32,12 +33,13 @@ class BaseAdapter(with_metaclass(ABCMeta)): communicates with a node. """ @abstract_method - def send_request(self, payload): - # type: (dict) -> dict + 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: BadApiResponse if a non-success response was received. @@ -69,6 +71,38 @@ def node_url(self): def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict + response = self._send_http_request(payload, **kwargs) + + raw_content = response.text + if not raw_content: + raise BadApiResponse('Empty response from node.') + + try: + decoded = json.loads(raw_content) # type: dict + # :bc: py2k doesn't have JSONDecodeError + except ValueError: + raise BadApiResponse('Non-JSON response from node: ' + raw_content) + + try: + # Response always has 200 status, even for errors, so the only way + # to check for success is to inspect the response body. + # :see: https://github.com/iotaledger/iri/issues/9 + error = decoded.get('error') + except AttributeError: + raise BadApiResponse('Invalid response from node: ' + raw_content) + + if error: + raise BadApiResponse(error) + + return decoded + + def _send_http_request(self, payload, **kwargs): + # type: (dict, dict) -> requests.Response + """ + Sends the actual HTTP request. + + Split into its own method so that it can be mocked during unit + tests. + """ kwargs.setdefault('timeout', get_default_timeout()) - response = requests.post(self.node_url, json=payload, **kwargs) - return response.json() + return requests.post(self.node_url, json=payload, **kwargs) diff --git a/test/__init__.py b/test/__init__.py new file mode 100644 index 0000000..3f3d02d --- /dev/null +++ b/test/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/test/adapter_test.py b/test/adapter_test.py new file mode 100644 index 0000000..f498abb --- /dev/null +++ b/test/adapter_test.py @@ -0,0 +1,122 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import json +from typing import Text +from unittest import TestCase + +import requests +from mock import Mock, patch +from six import BytesIO, text_type as text + +from iota import BadApiResponse, HttpAdapter + + +class HttpAdapterTestCase(TestCase): + def setUp(self): + super(HttpAdapterTestCase, self).setUp() + + self.adapter = HttpAdapter('localhost') + + def test_success_response(self): + """ + Simulates sending a command to the node and getting a success + response. + """ + expected_result = { + 'message': 'Hello, IOTA!', + } + + mocked_response = self._create_response(json.dumps(expected_result)) + mocked_sender = Mock(return_value=mocked_response) + + # noinspection PyUnresolvedReferences + with patch.object(self.adapter, '_send_http_request', mocked_sender): + result = self.adapter.send_request({'command': 'helloWorld'}) + + self.assertEqual(result, expected_result) + + def test_error_response(self): + """ + Simulates sending a command to the node and getting an error + response. + """ + expected_result = 'Command \u0027helloWorld\u0027 is unknown' + + mocked_response = self._create_response(json.dumps({ + 'error': expected_result, + 'duration': 42, + })) + + mocked_sender = Mock(return_value=mocked_response) + + # noinspection PyUnresolvedReferences + with patch.object(self.adapter, '_send_http_request', mocked_sender): + with self.assertRaises(BadApiResponse) as context: + self.adapter.send_request({'command': 'helloWorld'}) + + self.assertEqual(text(context.exception), expected_result) + + def test_empty_response(self): + """The response is empty.""" + mocked_response = self._create_response('') + + mocked_sender = Mock(return_value=mocked_response) + + # noinspection PyUnresolvedReferences + with patch.object(self.adapter, '_send_http_request', mocked_sender): + with self.assertRaises(BadApiResponse) as context: + self.adapter.send_request({'command': 'helloWorld'}) + + self.assertEqual(text(context.exception), 'Empty response from node.') + + def test_non_json_response(self): + """The response is not JSON.""" + invalid_response = 'EHLO iotatoken.com' # Erm... + mocked_response = self._create_response(invalid_response) + + mocked_sender = Mock(return_value=mocked_response) + + # noinspection PyUnresolvedReferences + with patch.object(self.adapter, '_send_http_request', mocked_sender): + with self.assertRaises(BadApiResponse) as context: + self.adapter.send_request({'command': 'helloWorld'}) + + self.assertEqual( + text(context.exception), + 'Non-JSON response from node: ' + invalid_response, + ) + + def test_non_object_response(self): + """The response is valid JSON, but it's not an object.""" + invalid_response = '["message", "Hello, IOTA!"]' + mocked_response = self._create_response(invalid_response) + + mocked_sender = Mock(return_value=mocked_response) + + # noinspection PyUnresolvedReferences + with patch.object(self.adapter, '_send_http_request', mocked_sender): + with self.assertRaises(BadApiResponse) as context: + self.adapter.send_request({'command': 'helloWorld'}) + + self.assertEqual( + text(context.exception), + 'Invalid response from node: ' + invalid_response, + ) + + @staticmethod + def _create_response(content): + # type: (Text) -> requests.Response + """Creates a Response object for a test.""" + # :see: requests.adapters.HTTPAdapter.build_response + response = requests.Response() + + # Response status is always 200, even for an error. + # :see: https://github.com/iotaledger/iri/issues/9 + response.status_code = 200 + + response.encoding = 'utf-8' + response.raw = BytesIO(content.encode('utf-8')) + + return response diff --git a/tox.ini b/tox.ini index 9453a08..9d73524 100644 --- a/tox.ini +++ b/tox.ini @@ -9,4 +9,5 @@ envlist = py27, py35 [testenv] commands = nosetests deps = + mock nose From 72e47ea4e39d6e3427f112db662b7fae07830f5b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 28 Nov 2016 20:15:12 -0500 Subject: [PATCH 008/239] Fixed incorrect magic method. --- iota/api.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/iota/api.py b/iota/api.py index 6533b5f..fa1479e 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Text +from typing import Callable, Text from iota.adapter import BaseAdapter @@ -23,8 +23,8 @@ def __init__(self, adapter): self.adapter = adapter # type: BaseAdapter - def __call__(self, command, **kwargs): - # type: (Text, dict) -> dict + def __getattr__(self, command): + # type: (Text, dict) -> Callable[[...], dict] """ Sends an arbitrary API command to the node. @@ -32,11 +32,13 @@ def __call__(self, command, **kwargs): methods, or if you just want to troll your node for awhile. :param command: The name of the command to send. - :param kwargs: Additional parameters to send with the command. :return: Decoded response from the node. + :raise: BadApiResponse if the node sends back an error response. """ - return self.adapter.send_request(dict(command=command, **kwargs)) + def command_sender(**kwargs): + return self.adapter.send_request(dict(command=command, **kwargs)) + return command_sender def get_node_info(self): """ @@ -44,4 +46,4 @@ def get_node_info(self): :see: https://iota.readme.io/docs/getnodeinfo """ - return self.__call__('getNodeInfo') + return self.__getattr__('getNodeInfo')() From 178984844664f021ee49387490bc2f4e27e355c8 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 28 Nov 2016 20:20:27 -0500 Subject: [PATCH 009/239] Better error handling in hello world script. --- examples/hello_world.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/examples/hello_world.py b/examples/hello_world.py index 01d2342..9640e93 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -12,6 +12,7 @@ from sys import argv from typing import Text +from requests.exceptions import ConnectionError from six import text_type as text from iota import BadApiResponse, DEFAULT_PORT, HttpAdapter, IotaApi, \ @@ -24,8 +25,17 @@ def main(host, port): try: node_info = api.get_node_info() + except ConnectionError as e: + print("Hm. {host}:{port} isn't responding. Is the node running?".format( + host = host, + port = port, + )) + print(e) except BadApiResponse as e: - print("Looks like {host}:{port} isn't very talkative today ):") + print("Looks like {host}:{port} isn't very talkative today ):".format( + host = host, + port = port, + )) print(e) else: print('Hello {host}:{port}!'.format(host=host, port=port)) From 445adf35901270bdd3ec5530dfbfb355b3c9b84f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 28 Nov 2016 20:53:29 -0500 Subject: [PATCH 010/239] Stubbed-out API methods. --- iota/api.py | 196 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 195 insertions(+), 1 deletion(-) diff --git a/iota/api.py b/iota/api.py index fa1479e..967cb02 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Callable, Text +from typing import Callable, Iterable, Optional, Text from iota.adapter import BaseAdapter @@ -40,10 +40,204 @@ def command_sender(**kwargs): return self.adapter.send_request(dict(command=command, **kwargs)) return command_sender + def add_neighbors(self, uris): + # type: (Iterable[Text]) -> dict + """ + Add one or more neighbors to the node. Lasts until the node is + restarted. + + :param uris: Use format `udp://:`. + Example: `add_neighbors(['udp://example.com:14265'])` + + :see: https://iota.readme.io/docs/addneighors + """ + return self.__getattr__('addNeighbors')(uris=uris) + + def attach_to_tangle( + self, + trunk_transaction, + branch_transaction, + min_weight_magnitude, + trytes + ): + # type: (Text, Text, int, Iterable[Text]) -> dict + """ + Attaches the specified transactions (trytes) to the Tangle by doing + Proof of Work. You need to supply branchTransaction as well as + trunkTransaction (basically the tips which you're going to + validate and reference with this transaction) - both of which + you'll get through the getTransactionsToApprove API call. + + The returned value is a different set of tryte values which you can + input into `broadcast_transactions` and `store_transactions`. + + :see: https://iota.readme.io/docs/attachtotangle + """ + raise NotImplementedError('Not implemented yet.') + + def broadcast_transactions(self, trytes): + # type: (Iterable[Text]) -> dict + """ + Broadcast a list of transactions to all neighbors. + + The input trytes for this call are provided by `attach_to_tangle`. + + :see: https://iota.readme.io/docs/broadcasttransactions + """ + raise NotImplementedError('Not implemented yet.') + + def find_transactions( + self, + bundles = None, + addresses = None, + tags = None, + approvees = None, + ): + # type: (Optional[Iterable[Text]], Optional[Iterable[Text]], Optional[Iterable[Text]], Optional[Iterable[Text]]) -> dict + """ + Find the transactions which match the specified input and return. + + All input values are lists, for which a list of return values + (transaction hashes), in the same order, is returned for all + individual elements. + + Using multiple of these input fields returns the intersection of + the values. + + :param bundles: List of bundle hashes. The hashes will be extended + to 81 trytes if necessary. + :param addresses: List of addresses. + :param tags: List of tags. Each tag must be 27 trytes. + :param approvees: List of approvee transaction hashes. + + :see: https://iota.readme.io/docs/findtransactions + """ + raise NotImplementedError('Not implemented yet.') + + def get_balances(self, addresses, threshold=100): + # type: (Iterable[Text], int) -> dict + """ + Similar to `get_inclusion_states`. Returns the confirmed balance + which a list of addresses have at the latest confirmed milestone. + + In addition to the balances, it also returns the milestone as well + as the index with which the confirmed balance was determined. + The balances are returned as a list in the same order as the + addresses were provided as input. + + :param addresses: List of addresses to get the confirmed balance + for. + :param threshold: Confirmation threshold. + + :see: https://iota.readme.io/docs/getbalances + """ + raise NotImplementedError('Not implemented yet.') + + def get_inclusion_states(self, transactions, tips): + # type: (Iterable[Text], Iterable[Text]) -> dict + """ + Get the inclusion states of a set of transactions. This is for + determining if a transaction was accepted and confirmed by the + network or not. You can search for multiple tips (and thus, + milestones) to get past inclusion states of transactions. + + :param transactions: List of transactions you want to get the + inclusion state for. + :param tips: List of tips (including milestones) you want to search + for the inclusion state. + + :see: https://iota.readme.io/docs/getinclusionstates + """ + raise NotImplementedError('Not implemented yet.') + + def get_neighbors(self): + # type: () -> dict + """ + Returns the set of neighbors the node is connected with, as well as + their activity count. + + The activity counter is reset after restarting IRI. + + :see: https://iota.readme.io/docs/getneighborsactivity + """ + return self.__getattr__('getNeighbors')() + def get_node_info(self): + # type: () -> dict """ Returns information about the node. :see: https://iota.readme.io/docs/getnodeinfo """ return self.__getattr__('getNodeInfo')() + + def get_tips(self): + # type: () -> dict + """ + Returns the list of tips (transactions which have no other + transactions referencing them). + + :see: https://iota.readme.io/docs/gettips + :see: https://iota.readme.io/docs/glossary#iota-terms + """ + return self.__getattr__('getTips')() + + def get_transactions_to_approve(self, depth): + # type: (int) -> dict + """ + Tip selection which returns `trunkTransaction` and + `branchTransaction`. + + :param depth: Determines how many bundles to go back to when + finding the transactions to approve. + + The higher the depth value, the more "babysitting" the node will + perform for the network (as it will confirm more transactions + that way). + + :see: https://iota.readme.io/docs/gettransactionstoapprove + """ + return self.__getattr__('getTransactionsToApprove')(depth=depth) + + def get_trytes(self, hashes): + # type: (Iterable[Text]) -> dict + """ + Returns the raw transaction data (trytes) of a specific + transaction. + + :see: https://iota.readme.io/docs/gettrytes + """ + raise NotImplementedError('Not implemented yet.') + + def interrupt_attaching_to_tangle(self): + # type: () -> dict + """ + Interrupts and completely aborts the `attach_to_tangle` process. + + :see: https://iota.readme.io/docs/interruptattachingtotangle + """ + return self.__getattr__('interruptAttachingToTangle')() + + def remove_neighbors(self, uris): + # type: (Iterable[Text]) -> dict + """ + Removes one or more neighbors from the node. Lasts until the node + is restarted. + + :param uris: Use format `udp://:`. + Example: `remove_neighbors(['udp://example.com:14265'])` + + :see: https://iota.readme.io/docs/removeneighors + """ + return self.__getattr__('removeNeighbors')(uris=uris) + + def store_transactions(self, trytes): + # type: (Iterable[Text]) -> dict + """ + Store transactions into local storage. + + The input trytes for this call are provided by `attach_to_tangle`. + + :see: https://iota.readme.io/docs/storetransactions + """ + raise NotImplementedError('Not implemented yet.') From ed2aa7872bcb7b657e4bc6dea8db1fb4a765d502 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 19:29:51 -0500 Subject: [PATCH 011/239] IotaApi now can accept a URI. --- examples/hello_world.py | 37 +++------- iota/adapter.py | 113 +++++++++++++++++++++++++++-- iota/api.py | 12 +++- test/adapter_test.py | 154 ++++++++++++++++++++++++++++++++++++---- test/api_test.py | 17 +++++ 5 files changed, 284 insertions(+), 49 deletions(-) create mode 100644 test/api_test.py diff --git a/examples/hello_world.py b/examples/hello_world.py index 9640e93..ce1ca0f 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -15,30 +15,23 @@ from requests.exceptions import ConnectionError from six import text_type as text -from iota import BadApiResponse, DEFAULT_PORT, HttpAdapter, IotaApi, \ - __version__ +from iota import BadApiResponse, IotaApi, __version__ -def main(host, port): +def main(uri): # type: (Text, int) -> None - api = IotaApi(HttpAdapter(host, port)) + api = IotaApi(uri) try: node_info = api.get_node_info() except ConnectionError as e: - print("Hm. {host}:{port} isn't responding. Is the node running?".format( - host = host, - port = port, - )) + print("Hm. {uri} isn't responding. Is the node running?".format(uri=uri)) print(e) except BadApiResponse as e: - print("Looks like {host}:{port} isn't very talkative today ):".format( - host = host, - port = port, - )) + print("Looks like {uri} isn't very talkative today ):".format(uri=uri)) print(e) else: - print('Hello {host}:{port}!'.format(host=host, port=port)) + print('Hello {uri}!'.format(uri=uri)) pprint(node_info) @@ -49,23 +42,13 @@ def main(host, port): ) parser.add_argument( - '--host', + '--uri', type = text, - default = 'localhost', + default = 'udp://localhost:14265/', help = - 'Hostname or IP address of the node to connect to ' - '(defaults to localhost).', - ) - - parser.add_argument( - '--port', - type = int, - default = DEFAULT_PORT, - - help = 'Port number to connect to (defaults to {default}).'.format( - default = DEFAULT_PORT, - ), + 'URI of the node to connect to ' + '(defaults to udp://localhost:14265/).', ) main(**vars(parser.parse_args(argv[1:]))) diff --git a/iota/adapter.py b/iota/adapter.py index 8f4647f..449a4d6 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -4,8 +4,9 @@ import json from abc import ABCMeta, abstractmethod as abstract_method +from inspect import isabstract as is_abstract from socket import getdefaulttimeout as get_default_timeout -from typing import Text +from typing import Dict, Text import requests from six import with_metaclass @@ -14,7 +15,7 @@ __all__ = [ 'BadApiResponse', - 'HttpAdapter', + 'InvalidUri', ] @@ -24,8 +25,49 @@ class BadApiResponse(ValueError): """ pass +class InvalidUri(ValueError): + """ + Indicates that an invalid URI was provided to `resolve_adapter`. + """ + pass + + +adapter_registry = {} # type: Dict[Text, BaseAdapter] +"""Keeps track of available adapters and their supported protocols.""" + + +def resolve_adapter(uri): + # type: (Text) -> BaseAdapter + """Given a URI, returns a properly-configured adapter instance.""" + try: + protocol, _ = uri.split('://', 1) + except ValueError: + raise InvalidUri('URI must begin with "://" (e.g., "udp://").') + + try: + adapter_type = adapter_registry[protocol] + except KeyError: + raise InvalidUri('Unrecognized protocol {protocol!r}.'.format( + protocol = protocol, + )) + + return adapter_type.configure(uri) + -class BaseAdapter(with_metaclass(ABCMeta)): +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', ()): + adapter_registry[protocol] = cls + + +class BaseAdapter(with_metaclass(AdapterMeta)): """ Interface for IOTA API adapters. @@ -48,25 +90,86 @@ def send_request(self, payload, **kwargs): 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) + @classmethod + def configure(cls, uri): + # type: (Text) -> BaseAdapter + """ + Creates a new instance using the specified URI. + """ + return cls(uri) + class HttpAdapter(BaseAdapter): """ Sends standard HTTP requests. """ - def __init__(self, host, port=DEFAULT_PORT): + supported_protocols = ('udp', 'http',) + """Used by `resolve_adapter`.""" + + @classmethod + def configure(cls, uri): + # type: (Text) -> HttpAdapter + """ + Creates a new instance using the specified URI. + + :param uri: E.g., `udp://localhost:14265/` + """ + try: + protocol, config = uri.split('://', 1) + except ValueError: + raise InvalidUri('No protocol specified in URI {uri!r}.'.format(uri=uri)) + else: + if protocol not in cls.supported_protocols: + raise InvalidUri('Unsupported protocol {protocol!r}.'.format( + protocol = protocol, + )) + + try: + server, path = config.split('/', 1) + except ValueError: + server = config + path = '/' + else: + # Restore the '/' delimiter that we used to split the string. + path = '/' + path + + try: + host, port = server.split(':', 1) + except ValueError: + host = server + + if protocol == 'http': + port = 80 + else: + port = DEFAULT_PORT + + if not host: + raise InvalidUri('Empty hostname in URI {uri!r}.'.format(uri=uri)) + + try: + port = int(port) + except ValueError: + raise InvalidUri('Non-numeric port in URI {uri!r}.'.format(uri=uri)) + + return cls(host, port, path) + + + def __init__(self, host, port=DEFAULT_PORT, path='/'): # type: (Text, int) -> None super(HttpAdapter, self).__init__() self.host = host self.port = port + self.path = path @property def node_url(self): # type: () -> Text """Returns the node URL.""" - return 'http://{host}:{port}/'.format( + return 'http://{host}:{port}{path}'.format( host = self.host, port = self.port, + path = self.path, ) def send_request(self, payload, **kwargs): diff --git a/iota/api.py b/iota/api.py index 967cb02..d704e8d 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,9 +2,9 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Callable, Iterable, Optional, Text +from typing import Callable, Iterable, Optional, Text, Union -from iota.adapter import BaseAdapter +from iota.adapter import BaseAdapter, resolve_adapter __all__ = [ 'IotaApi', @@ -18,9 +18,15 @@ class IotaApi(object): :see: https://iota.readme.io/docs/getting-started """ def __init__(self, adapter): - # type: (BaseAdapter) -> None + # type: (Union[Text, BaseAdapter]) -> None + """ + :param adapter: URI string or BaseAdapter instance. + """ super(IotaApi, self).__init__() + if not isinstance(adapter, BaseAdapter): + adapter = resolve_adapter(adapter) + self.adapter = adapter # type: BaseAdapter def __getattr__(self, command): diff --git a/test/adapter_test.py b/test/adapter_test.py index f498abb..3eb9a44 100644 --- a/test/adapter_test.py +++ b/test/adapter_test.py @@ -10,20 +10,138 @@ from mock import Mock, patch from six import BytesIO, text_type as text -from iota import BadApiResponse, HttpAdapter +from iota import BadApiResponse, DEFAULT_PORT, InvalidUri +from iota.adapter import HttpAdapter, resolve_adapter + + +class ResolveAdapterTestCase(TestCase): + """Unit tests for the `resolve_adapter` function.""" + def test_udp(self): + """Resolving a valid udp:// URI.""" + adapter = resolve_adapter('udp://localhost:14265/') + self.assertIsInstance(adapter, HttpAdapter) + + def test_http(self): + """Resolving a valid http:// URI.""" + adapter = resolve_adapter('http://localhost:14265/') + self.assertIsInstance(adapter, HttpAdapter) + + def test_missing_protocol(self): + """The URI does not include a protocol.""" + with self.assertRaises(InvalidUri): + resolve_adapter('localhost:14265') + + def test_unknown_protocol(self): + """The URI references a protocol that has no associated adapter.""" + with self.assertRaises(InvalidUri): + resolve_adapter('foobar://localhost:14265') class HttpAdapterTestCase(TestCase): - def setUp(self): - super(HttpAdapterTestCase, self).setUp() + def test_configure_udp(self): + """Configuring an HttpAdapter using a valid udp:// URI.""" + adapter = HttpAdapter.configure('udp://localhost:14265/') + + self.assertEqual(adapter.host, 'localhost') + self.assertEqual(adapter.port, 14265) + self.assertEqual(adapter.path, '/') + + def test_configure_http(self): + """Configuring HttpAdapter using a valid http:// URI.""" + adapter = HttpAdapter.configure('http://localhost:14265/') + + self.assertEqual(adapter.host, 'localhost') + self.assertEqual(adapter.port, 14265) + self.assertEqual(adapter.path, '/') + + def test_configure_ipv4_address(self): + """Configuring an HttpAdapter using an IPv4 address.""" + adapter = HttpAdapter.configure('udp://127.0.0.1:8080/') + + self.assertEqual(adapter.host, '127.0.0.1') + self.assertEqual(adapter.port, 8080) + self.assertEqual(adapter.path, '/') + + def test_configure_default_port_udp(self): + """Implicitly use default UDP port for HttpAdapter.""" + adapter = HttpAdapter.configure('udp://iotatoken.com/') + + self.assertEqual(adapter.host, 'iotatoken.com') + self.assertEqual(adapter.port, DEFAULT_PORT) + self.assertEqual(adapter.path, '/') + + def test_configure_default_port_http(self): + """Implicitly use default HTTP port for HttpAdapter.""" + adapter = HttpAdapter.configure('http://iotatoken.com/') + + self.assertEqual(adapter.host, 'iotatoken.com') + self.assertEqual(adapter.port, 80) + self.assertEqual(adapter.path, '/') + + def test_configure_path(self): + """Specifying a different path for HttpAdapter.""" + adapter = HttpAdapter.configure('http://iotatoken.com:443/node') + + self.assertEqual(adapter.host, 'iotatoken.com') + self.assertEqual(adapter.port, 443) + self.assertEqual(adapter.path, '/node') + + def test_configure_custom_path_default_port(self): + """ + Configuring HttpAdapter to use a custom path but implicitly use + default port. + """ + adapter = HttpAdapter.configure('http://iotatoken.com/node') + + self.assertEqual(adapter.host, 'iotatoken.com') + self.assertEqual(adapter.port, 80) + self.assertEqual(adapter.path, '/node') + + def test_configure_default_path(self): + """Implicitly use default path for HttpAdapter.""" + adapter = HttpAdapter.configure('udp://example.com:8000') + + self.assertEqual(adapter.host, 'example.com') + self.assertEqual(adapter.port, 8000) + self.assertEqual(adapter.path, '/') - self.adapter = HttpAdapter('localhost') + def test_configure_default_port_and_path(self): + """Implicitly use default port and path for HttpAdapter.""" + adapter = HttpAdapter.configure('udp://localhost') + + self.assertEqual(adapter.host, 'localhost') + self.assertEqual(adapter.port, DEFAULT_PORT) + self.assertEqual(adapter.path, '/') + + def test_configure_error_missing_protocol(self): + """Forgetting to add the protocol to the URI.""" + with self.assertRaises(InvalidUri): + HttpAdapter.configure('localhost:14265') + + def test_configure_error_invalid_protocol(self): + """ + Attempting to configure HttpAdapter with unsupported protocol. + """ + with self.assertRaises(InvalidUri): + HttpAdapter.configure('ftp://localhost:14265/') + + def test_configure_error_empty_host(self): + """Attempting to configure HttpAdapter with empty host.""" + with self.assertRaises(InvalidUri): + HttpAdapter.configure('udp://:14265') + + def test_configure_error_non_numeric_port(self): + """Attempting to configure HttpAdapter with non-numeric port.""" + with self.assertRaises(InvalidUri): + HttpAdapter.configure('udp://localhost:iota/') def test_success_response(self): """ Simulates sending a command to the node and getting a success response. """ + adapter = HttpAdapter('localhost') + expected_result = { 'message': 'Hello, IOTA!', } @@ -32,8 +150,8 @@ def test_success_response(self): mocked_sender = Mock(return_value=mocked_response) # noinspection PyUnresolvedReferences - with patch.object(self.adapter, '_send_http_request', mocked_sender): - result = self.adapter.send_request({'command': 'helloWorld'}) + with patch.object(adapter, '_send_http_request', mocked_sender): + result = adapter.send_request({'command': 'helloWorld'}) self.assertEqual(result, expected_result) @@ -42,6 +160,8 @@ def test_error_response(self): Simulates sending a command to the node and getting an error response. """ + adapter = HttpAdapter('localhost') + expected_result = 'Command \u0027helloWorld\u0027 is unknown' mocked_response = self._create_response(json.dumps({ @@ -52,36 +172,40 @@ def test_error_response(self): mocked_sender = Mock(return_value=mocked_response) # noinspection PyUnresolvedReferences - with patch.object(self.adapter, '_send_http_request', mocked_sender): + with patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - self.adapter.send_request({'command': 'helloWorld'}) + adapter.send_request({'command': 'helloWorld'}) self.assertEqual(text(context.exception), expected_result) def test_empty_response(self): """The response is empty.""" + adapter = HttpAdapter('localhost') + mocked_response = self._create_response('') mocked_sender = Mock(return_value=mocked_response) # noinspection PyUnresolvedReferences - with patch.object(self.adapter, '_send_http_request', mocked_sender): + with patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - self.adapter.send_request({'command': 'helloWorld'}) + adapter.send_request({'command': 'helloWorld'}) self.assertEqual(text(context.exception), 'Empty response from node.') def test_non_json_response(self): """The response is not JSON.""" + adapter = HttpAdapter('localhost') + invalid_response = 'EHLO iotatoken.com' # Erm... mocked_response = self._create_response(invalid_response) mocked_sender = Mock(return_value=mocked_response) # noinspection PyUnresolvedReferences - with patch.object(self.adapter, '_send_http_request', mocked_sender): + with patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - self.adapter.send_request({'command': 'helloWorld'}) + adapter.send_request({'command': 'helloWorld'}) self.assertEqual( text(context.exception), @@ -90,15 +214,17 @@ def test_non_json_response(self): def test_non_object_response(self): """The response is valid JSON, but it's not an object.""" + adapter = HttpAdapter('localhost') + invalid_response = '["message", "Hello, IOTA!"]' mocked_response = self._create_response(invalid_response) mocked_sender = Mock(return_value=mocked_response) # noinspection PyUnresolvedReferences - with patch.object(self.adapter, '_send_http_request', mocked_sender): + with patch.object(adapter, '_send_http_request', mocked_sender): with self.assertRaises(BadApiResponse) as context: - self.adapter.send_request({'command': 'helloWorld'}) + adapter.send_request({'command': 'helloWorld'}) self.assertEqual( text(context.exception), diff --git a/test/api_test.py b/test/api_test.py new file mode 100644 index 0000000..321f160 --- /dev/null +++ b/test/api_test.py @@ -0,0 +1,17 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota import IotaApi +from iota.adapter import HttpAdapter + + +class IotaApiTestCase(TestCase): + def test_init_with_uri(self): + """ + Passing a URI to the initializer instead of an adapter instance. + """ + api = IotaApi('udp://localhost:14265/') + self.assertIsInstance(api.adapter, HttpAdapter) From 20e1598511e322aa8d89d9e132b391b87164206e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 19:48:43 -0500 Subject: [PATCH 012/239] Minor cleanup, added unit tests. --- iota/adapter.py | 9 +++++-- iota/api.py | 8 +++--- test/api_test.py | 69 +++++++++++++++++++++++++++++++++++++++++++++--- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/iota/adapter.py b/iota/adapter.py index 449a4d6..cf6dfab 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -6,7 +6,7 @@ from abc import ABCMeta, abstractmethod as abstract_method from inspect import isabstract as is_abstract from socket import getdefaulttimeout as get_default_timeout -from typing import Dict, Text +from typing import Dict, Text, Tuple import requests from six import with_metaclass @@ -74,6 +74,12 @@ class BaseAdapter(with_metaclass(AdapterMeta)): Adapters make it easy to customize the way an IotaApi instance communicates with a node. """ + supported_protocols = () # type: Tuple[Text] + """ + Protocols that `resolve_adapter` can use to identify this adapter + type. + """ + @abstract_method def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict @@ -104,7 +110,6 @@ class HttpAdapter(BaseAdapter): Sends standard HTTP requests. """ supported_protocols = ('udp', 'http',) - """Used by `resolve_adapter`.""" @classmethod def configure(cls, uri): diff --git a/iota/api.py b/iota/api.py index d704e8d..0b1dfc6 100644 --- a/iota/api.py +++ b/iota/api.py @@ -57,7 +57,7 @@ def add_neighbors(self, uris): :see: https://iota.readme.io/docs/addneighors """ - return self.__getattr__('addNeighbors')(uris=uris) + return self.addNeighbors(uris=uris) def attach_to_tangle( self, @@ -166,7 +166,7 @@ def get_neighbors(self): :see: https://iota.readme.io/docs/getneighborsactivity """ - return self.__getattr__('getNeighbors')() + return self.getNeighbors() def get_node_info(self): # type: () -> dict @@ -186,7 +186,7 @@ def get_tips(self): :see: https://iota.readme.io/docs/gettips :see: https://iota.readme.io/docs/glossary#iota-terms """ - return self.__getattr__('getTips')() + return self.getTips() def get_transactions_to_approve(self, depth): # type: (int) -> dict @@ -203,7 +203,7 @@ def get_transactions_to_approve(self, depth): :see: https://iota.readme.io/docs/gettransactionstoapprove """ - return self.__getattr__('getTransactionsToApprove')(depth=depth) + return self.getTransactionsToApprove(depth=depth) def get_trytes(self, hashes): # type: (Iterable[Text]) -> dict diff --git a/test/api_test.py b/test/api_test.py index 321f160..a44cd0b 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -2,10 +2,28 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Optional from unittest import TestCase from iota import IotaApi -from iota.adapter import HttpAdapter +from iota.adapter import BaseAdapter + + +class MockAdapter(BaseAdapter): + """An adapter for IotaApi that always returns a mocked response.""" + supported_protocols = ('mock',) + + def __init__(self, response=None): + # type: (Optional[dict]) -> None + super(MockAdapter, self).__init__() + + self.response = response + + self.requests = [] + + def send_request(self, payload, **kwargs): + self.requests.append((payload, kwargs)) + return self.response class IotaApiTestCase(TestCase): @@ -13,5 +31,50 @@ def test_init_with_uri(self): """ Passing a URI to the initializer instead of an adapter instance. """ - api = IotaApi('udp://localhost:14265/') - self.assertIsInstance(api.adapter, HttpAdapter) + api = IotaApi('mock://') + self.assertIsInstance(api.adapter, MockAdapter) + + def test_custom_command(self): + """Sending an experimental/unsupported command.""" + expected_response = {'message': 'Hello, IOTA!'} + + adapter = MockAdapter(expected_response) + api = IotaApi(adapter) + + response = api.helloWorld() + + self.assertEqual(response, expected_response) + + self.assertListEqual( + adapter.requests, + [({'command': 'helloWorld'}, {})], + ) + + def test_custom_command_with_arguments(self): + """Sending an experimental/unsupported command with arguments.""" + expected_response = {'message': 'Hello, IOTA!'} + + adapter = MockAdapter(expected_response) + api = IotaApi(adapter) + + response = api.helloWorld(foo='bar', baz='luhrmann') + + self.assertEqual(response, expected_response) + + self.assertListEqual( + adapter.requests, + [({'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}, {})], + ) + + def test_supported_command(self): + """Sending a supported command.""" + expected_response = {'appName': 'IRI', 'appVersion': '1.1.1'} + + adapter = MockAdapter(expected_response) + api = IotaApi(adapter) + + response = api.get_node_info() + + self.assertEqual(response, expected_response) + + self.assertListEqual(adapter.requests, [({'command': 'getNodeInfo'}, {})]) From bc0c2f6b2a94e85cf6839dcb3fd1b9cc8fd47ecc Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 19:53:25 -0500 Subject: [PATCH 013/239] Added PyOTAShell script. --- examples/shell.py | 54 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) create mode 100644 examples/shell.py diff --git a/examples/shell.py b/examples/shell.py new file mode 100644 index 0000000..d2aa42d --- /dev/null +++ b/examples/shell.py @@ -0,0 +1,54 @@ +# coding=utf-8 +"""Launches a Python shell with a configured API client ready to go.""" + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from argparse import ArgumentParser +from sys import argv +from typing import Text + +from six import text_type as text + +from iota import IotaApi, __version__ + + +def main(uri): + # type: (Text) -> None + iota = IotaApi(uri) + + _banner = ( + 'IOTA API client for {uri} initialized as variable `iota`. ' + 'Type `help(iota)` for help.'.format( + uri = uri, + ) + ) + + try: + # noinspection PyUnresolvedReferences + import IPython + except ImportError: + from code import InteractiveConsole + InteractiveConsole(locals={'iota': iota}).interact(_banner) + else: + IPython.embed(header=_banner) + + +if __name__ == '__main__': + parser = ArgumentParser( + description = __doc__, + epilog = 'PyOTA v{version}'.format(version=__version__), + ) + + parser.add_argument( + '--uri', + type = text, + default = 'udp://localhost:14265/', + + help = + 'URI of the node to connect to ' + '(defaults to udp://localhost:14265/).', + ) + + main(**vars(parser.parse_args(argv[1:]))) + From 1186d74cef05048ff0bf082aed46c0887cf4759a Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 20:50:57 -0500 Subject: [PATCH 014/239] Implemented Trit type. --- iota/types.py | 62 ++++++++++++++++++++++ test/types_test.py | 127 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+) create mode 100644 iota/types.py create mode 100644 test/types_test.py diff --git a/iota/types.py b/iota/types.py new file mode 100644 index 0000000..8ad2f42 --- /dev/null +++ b/iota/types.py @@ -0,0 +1,62 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Optional, Text + + +class Trit(object): + """ + Ternary version of a bit. + + A trit is similar to a bit, except that it has 3 states: 1, 0 and + unknown. + """ + def __init__(self, value): + # type: (int) -> None + if type(value) is not int: + raise TypeError('Allowed types for Trit: [int].') + + if not -1 <= value <= 1: + raise ValueError('Allowed values for Trit: [-1, 0, 1].') + + self.value = value + + def __repr__(self): + # type: () -> Text + return 'Trit({value})'.format(value=self.value) + + def __eq__(self, other): + # type: (Trit) -> bool + return self.__cmp__(other) in (0,) + + def __ne__(self, other): + # type: (Trit) -> bool + return self.__cmp__(other) in (-1, 1, None) + + def __lt__(self, other): + # type: (Trit) -> bool + return self.__cmp__(other) in (-1,) + + def __le__(self, other): + # type: (Trit) -> bool + return self.__cmp__(other) in (-1, 0) + + def __gt__(self, other): + # type: (Trit) -> bool + return self.__cmp__(other) in (1,) + + def __ge__(self, other): + # type: (Trit) -> bool + return self.__cmp__(other) in (0, 1) + + def __cmp__(self, other): + # type: (Trit) -> Optional[int] + if not isinstance(other, Trit): + raise TypeError('Trits can only be compared to other Trits.') + + if -1 in (self.value, other.value): + return None + + # :see: https://docs.python.org/3/whatsnew/3.0.html#ordering-comparisons + return (self.value > other.value) - (self.value < other.value) diff --git a/test/types_test.py b/test/types_test.py new file mode 100644 index 0000000..9e07483 --- /dev/null +++ b/test/types_test.py @@ -0,0 +1,127 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota.types import Trit + + +class TritTestCase(TestCase): + def test_comparison(self): + """Testing equality and comparison of trit values.""" + on = Trit(1) + off = Trit(0) + unknown = Trit(-1) + + self.assertTrue(on == on) + self.assertFalse(on != on) + self.assertTrue(on <= on) + self.assertFalse(on < on) + self.assertTrue(on >= on) + self.assertFalse(on > on) + + self.assertTrue(off == off) + self.assertFalse(off != off) + self.assertTrue(off <= off) + self.assertFalse(off < off) + self.assertTrue(off >= off) + self.assertFalse(off > off) + + # `unknown` behaves similarly to NaN. + # :see: https://en.wikipedia.org/wiki/IEEE_754 + self.assertFalse(unknown == unknown) + self.assertTrue(unknown != unknown) + self.assertFalse(unknown <= unknown) + self.assertFalse(unknown < unknown) + self.assertFalse(unknown >= unknown) + self.assertFalse(unknown > unknown) + + self.assertFalse(on == off) + self.assertTrue(on != off) + self.assertFalse(on <= off) + self.assertFalse(on < off) + self.assertTrue(on > off) + self.assertTrue(on >= off) + + self.assertFalse(off == on) + self.assertTrue(off != on) + self.assertTrue(off <= on) + self.assertTrue(off < on) + self.assertFalse(off >= on) + self.assertFalse(off > on) + + self.assertFalse(on == unknown) + self.assertTrue(on != unknown) + self.assertFalse(on < unknown) + self.assertFalse(on <= unknown) + self.assertFalse(on > unknown) + self.assertFalse(on >= unknown) + + self.assertFalse(off == unknown) + self.assertTrue(off != unknown) + self.assertFalse(off < unknown) + self.assertFalse(off <= unknown) + self.assertFalse(off > unknown) + self.assertFalse(off >= unknown) + + self.assertTrue(on is on) + self.assertTrue(unknown is unknown) + + self.assertFalse(on is off) + self.assertFalse(on is Trit(1)) + self.assertFalse(unknown is Trit(-1)) + + # Identity comparison also works for non-Trits. + self.assertFalse(on is 1) + self.assertFalse(unknown is None) + + def test_error_invalid_value(self): + """Attempting to initialize a trit with an invalid value.""" + with self.assertRaises(ValueError): + Trit(2) + + with self.assertRaises(ValueError): + Trit(-2) + + # noinspection PyTypeChecker + def test_error_invalid_type(self): + """Trits may only be initialized using ints.""" + with self.assertRaises(TypeError): + Trit(1.0) + + with self.assertRaises(TypeError): + # This is not allowed because it is ambiguous; did you mean + # `Trit(0)` or `Trit(ord('0'))`? + Trit('0') + + with self.assertRaises(TypeError): + # Technically, booleans are ints, but we still don't allow them + # because Trits and booleans mean very different things. + Trit(False) + + with self.assertRaises(TypeError): + # It's too easy to accidentally pass a null to the initializer. + # For safety, this also is not allowed. + Trit(None) + + # noinspection PyTypeChecker + def test_error_invalid_comparison(self): + """Trits can only be compared to other trits.""" + with self.assertRaises(TypeError): + Trit(0) == 0 + + with self.assertRaises(TypeError): + Trit(1) != {} + + with self.assertRaises(TypeError): + Trit(0) <= 0 + + with self.assertRaises(TypeError): + Trit(1) >= '0' + + with self.assertRaises(TypeError): + Trit(-1) < 1 + + with self.assertRaises(TypeError): + Trit(-1) > False From c8664d511c990490f990b528a7fe6d00d081ddc5 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 21:18:01 -0500 Subject: [PATCH 015/239] Implemented Tryte type (needs more unit tests). --- iota/types.py | 49 +++++++++++++++++++++++++++++++++- test/types_test.py | 66 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 113 insertions(+), 2 deletions(-) diff --git a/iota/types.py b/iota/types.py index 8ad2f42..3c5de47 100644 --- a/iota/types.py +++ b/iota/types.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Optional, Text +from typing import Iterable, List, Optional, Text, Union class Trit(object): @@ -60,3 +60,50 @@ def __cmp__(self, other): # :see: https://docs.python.org/3/whatsnew/3.0.html#ordering-comparisons return (self.value > other.value) - (self.value < other.value) + + +class Tryte(object): + """ + Ternary version of a byte. + + A tryte is a sequence of three trits. + """ + def __init__(self, trits, pad=False): + # type: (Iterable[Union[Trit, int]], bool) -> None + """ + :param trits: Iterable of 3 trits (or compatible integers). + :param pad: Whether to pad `trits` if it has less than 3 trits. + """ + super(Tryte, self).__init__() + + if not isinstance(trits, Iterable): + raise TypeError('Allowed types for Tryte: [Iterable[Trit]].') + + self.trits = [] # type: List[Trit] + + for trit in trits: + self.trits.append( + trit + if isinstance(trit, Trit) + else Trit(trit) + ) + + if pad and (len(self.trits) < 3): + self.trits =\ + [Trit(-1) for _ in range(0, 3 - len(self.trits))] + self.trits + + if len(self.trits) != 3: + raise ValueError( + 'Incorrect number of Trits for Tryte ' + '(expected 3, actual {count}).'.format( + count = len(self.trits), + ), + ) + + def __repr__(self): + # type: () -> Text + return 'Tryte({trits})'.format(trits=[trit.value for trit in self.trits]) + + def __getitem__(self, item): + # type: (int) -> Trit + return self.trits[item] diff --git a/test/types_test.py b/test/types_test.py index 9e07483..e077598 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -4,7 +4,7 @@ from unittest import TestCase -from iota.types import Trit +from iota.types import Trit, Tryte class TritTestCase(TestCase): @@ -125,3 +125,67 @@ def test_error_invalid_comparison(self): with self.assertRaises(TypeError): Trit(-1) > False + + +class TryteTestCase(TestCase): + def test_comparison(self): + """Testing equality and comparison of tryte values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_init_mixed_types(self): + """ + As a convenience, you are allowed to initialize a Tryte using ints. + """ + # Mixing Trits and ints is also OK. + tryte = Tryte([Trit(1), 1, -1]) + + self.assertEqual(tryte[0], Trit(1)) + self.assertEqual(tryte[1], Trit(1)) + + # Well, shucks. + self.assertNotEqual(tryte[2], Trit(-1)) + self.assertEqual(tryte[2].value, -1) + + # noinspection PyTypeChecker + def test_error_init_wrong_type(self): + """ + Attempting to initialize a tryte with something other than an array + of trits. + """ + with self.assertRaises(TypeError): + Tryte(Trit(1)) + + with self.assertRaises(TypeError): + Tryte(['1', False, None]) + + def test_error_init_not_enough_trits(self): + """Attempting to initialize a tryte with less than 3 trits.""" + with self.assertRaises(ValueError): + Tryte([]) + + with self.assertRaises(ValueError): + Tryte([Trit(0)]) + + with self.assertRaises(ValueError): + Tryte([Trit(0), Trit(0)]) + + def test_init_with_padding(self): + """Padding a tryte so that it has the correct number of trits.""" + tryte1 = Tryte([Trit(1)], pad=True) + + # Note that padding trits are applied first. + self.assertEqual(tryte1[0].value, -1) + self.assertEqual(tryte1[1].value, -1) + self.assertEqual(tryte1[2].value, 1) + + tryte2 = Tryte([Trit(0), Trit(1)], pad=True) + + self.assertEqual(tryte2[0].value, -1) + self.assertEqual(tryte2[1].value, 0) + self.assertEqual(tryte2[2].value, 1) + + def test_error_init_too_many_trits(self): + """Attempting to initialize a tryte with more than 3 trits.""" + with self.assertRaises(ValueError): + Tryte([Trit(-1), Trit(-1), Trit(-1), Trit(-1)]) From dee2b8d88e8ed9fff2ae1ec3bbe04a6f4a5f5b60 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 22:48:03 -0500 Subject: [PATCH 016/239] Replaced Trit/Tryte impl with TryteString. --- iota/types.py | 141 +++++++++++++------------------- test/types_test.py | 195 ++++----------------------------------------- 2 files changed, 71 insertions(+), 265 deletions(-) diff --git a/iota/types.py b/iota/types.py index 3c5de47..182321f 100644 --- a/iota/types.py +++ b/iota/types.py @@ -2,108 +2,81 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Iterable, List, Optional, Text, Union +from typing import Text, Union +from six import PY2, binary_type -class Trit(object): - """ - Ternary version of a bit. - A trit is similar to a bit, except that it has 3 states: 1, 0 and - unknown. +class TryteString(object): """ - def __init__(self, value): - # type: (int) -> None - if type(value) is not int: - raise TypeError('Allowed types for Trit: [int].') - - if not -1 <= value <= 1: - raise ValueError('Allowed values for Trit: [-1, 0, 1].') - - self.value = value - - def __repr__(self): - # type: () -> Text - return 'Trit({value})'.format(value=self.value) + A string representation of a sequence of trytes. - def __eq__(self, other): - # type: (Trit) -> bool - return self.__cmp__(other) in (0,) + A trit can be thought of as the ternary version of a bit. It can + have one of three values: 1, 0 or unknown. - def __ne__(self, other): - # type: (Trit) -> bool - return self.__cmp__(other) in (-1, 1, None) + A tryte can be thought of as the ternary version of a byte. It is a + sequence of 3 trits. - def __lt__(self, other): - # type: (Trit) -> bool - return self.__cmp__(other) in (-1,) + A tryte string is similar in concept to Python's byte string, except + it has a more limited alphabet. Byte strings are limited to ASCII + (256 possible values), while the tryte string alphabet only has + 27 characters (one for each possible tryte configuration). + """ + # :bc: Without the bytearray cast, Python 2 will populate the dict + # with characters instead of integers. + # noinspection SpellCheckingInspection + alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) + index = dict(zip(alphabet.values(), alphabet.keys())) - def __le__(self, other): - # type: (Trit) -> bool - return self.__cmp__(other) in (-1, 0) + @classmethod + def from_bytes(cls, bytes_): + # type: (Union[binary_type, bytearray]) -> TryteString + """Creates a TryteString from a byte string.""" + tryte_string = bytearray() - def __gt__(self, other): - # type: (Trit) -> bool - return self.__cmp__(other) in (1,) + # :bc: In Python 2, iterating over a byte string yields characters + # instead of integers. + if not isinstance(bytes_, bytearray): + bytes_ = bytearray(bytes_) - def __ge__(self, other): - # type: (Trit) -> bool - return self.__cmp__(other) in (0, 1) + for c in bytes_: + second, first = divmod(c, len(cls.alphabet)) - def __cmp__(self, other): - # type: (Trit) -> Optional[int] - if not isinstance(other, Trit): - raise TypeError('Trits can only be compared to other Trits.') + tryte_string.append(cls.alphabet[first]) + tryte_string.append(cls.alphabet[second]) - if -1 in (self.value, other.value): - return None + return cls(tryte_string) - # :see: https://docs.python.org/3/whatsnew/3.0.html#ordering-comparisons - return (self.value > other.value) - (self.value < other.value) + def __init__(self, value, pad=False): + # type: (Union[binary_type, bytearray], bool) -> None + super(TryteString, self).__init__() + if len(value) % 2: + if pad: + value += self.alphabet[0] + else: + raise ValueError( + 'Length of TryteString must be divisible by 2.' + ) -class Tryte(object): - """ - Ternary version of a byte. + self.value = value if isinstance(value, bytearray) else bytearray(value) - A tryte is a sequence of three trits. - """ - def __init__(self, trits, pad=False): - # type: (Iterable[Union[Trit, int]], bool) -> None - """ - :param trits: Iterable of 3 trits (or compatible integers). - :param pad: Whether to pad `trits` if it has less than 3 trits. - """ - super(Tryte, self).__init__() - - if not isinstance(trits, Iterable): - raise TypeError('Allowed types for Tryte: [Iterable[Trit]].') - - self.trits = [] # type: List[Trit] - - for trit in trits: - self.trits.append( - trit - if isinstance(trit, Trit) - else Trit(trit) - ) + def __repr__(self): + # type: () -> Text + return 'TryteString({value!r})'.format(value=binary_type(self.value)) - if pad and (len(self.trits) < 3): - self.trits =\ - [Trit(-1) for _ in range(0, 3 - len(self.trits))] + self.trits + def __bytes__(self): + # type: () -> Text + """Converts the TryteString into a byte string.""" + byte_string = bytearray() - if len(self.trits) != 3: - raise ValueError( - 'Incorrect number of Trits for Tryte ' - '(expected 3, actual {count}).'.format( - count = len(self.trits), - ), + for i in range(0, len(self.value), 2): + byte_string.append( + self.index[self.value[i]] + + (self.index[self.value[i+1]] * len(self.index)) ) - def __repr__(self): - # type: () -> Text - return 'Tryte({trits})'.format(trits=[trit.value for trit in self.trits]) + return binary_type(byte_string) - def __getitem__(self, item): - # type: (int) -> Trit - return self.trits[item] + if PY2: + __str__ = __bytes__ diff --git a/test/types_test.py b/test/types_test.py index e077598..188097d 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -4,188 +4,21 @@ from unittest import TestCase -from iota.types import Trit, Tryte +from six import binary_type +from iota.types import TryteString -class TritTestCase(TestCase): - def test_comparison(self): - """Testing equality and comparison of trit values.""" - on = Trit(1) - off = Trit(0) - unknown = Trit(-1) - self.assertTrue(on == on) - self.assertFalse(on != on) - self.assertTrue(on <= on) - self.assertFalse(on < on) - self.assertTrue(on >= on) - self.assertFalse(on > on) +# noinspection SpellCheckingInspection +class TryteStringTestCase(TestCase): + def test_hello_world(self): + """PoC test for TryteString""" + self.assertEqual( + TryteString.from_bytes(b'Hello, world!').value, + b'RBTC9D9DCDQAEAKDCDFD9DSCFA', + ) - self.assertTrue(off == off) - self.assertFalse(off != off) - self.assertTrue(off <= off) - self.assertFalse(off < off) - self.assertTrue(off >= off) - self.assertFalse(off > off) - - # `unknown` behaves similarly to NaN. - # :see: https://en.wikipedia.org/wiki/IEEE_754 - self.assertFalse(unknown == unknown) - self.assertTrue(unknown != unknown) - self.assertFalse(unknown <= unknown) - self.assertFalse(unknown < unknown) - self.assertFalse(unknown >= unknown) - self.assertFalse(unknown > unknown) - - self.assertFalse(on == off) - self.assertTrue(on != off) - self.assertFalse(on <= off) - self.assertFalse(on < off) - self.assertTrue(on > off) - self.assertTrue(on >= off) - - self.assertFalse(off == on) - self.assertTrue(off != on) - self.assertTrue(off <= on) - self.assertTrue(off < on) - self.assertFalse(off >= on) - self.assertFalse(off > on) - - self.assertFalse(on == unknown) - self.assertTrue(on != unknown) - self.assertFalse(on < unknown) - self.assertFalse(on <= unknown) - self.assertFalse(on > unknown) - self.assertFalse(on >= unknown) - - self.assertFalse(off == unknown) - self.assertTrue(off != unknown) - self.assertFalse(off < unknown) - self.assertFalse(off <= unknown) - self.assertFalse(off > unknown) - self.assertFalse(off >= unknown) - - self.assertTrue(on is on) - self.assertTrue(unknown is unknown) - - self.assertFalse(on is off) - self.assertFalse(on is Trit(1)) - self.assertFalse(unknown is Trit(-1)) - - # Identity comparison also works for non-Trits. - self.assertFalse(on is 1) - self.assertFalse(unknown is None) - - def test_error_invalid_value(self): - """Attempting to initialize a trit with an invalid value.""" - with self.assertRaises(ValueError): - Trit(2) - - with self.assertRaises(ValueError): - Trit(-2) - - # noinspection PyTypeChecker - def test_error_invalid_type(self): - """Trits may only be initialized using ints.""" - with self.assertRaises(TypeError): - Trit(1.0) - - with self.assertRaises(TypeError): - # This is not allowed because it is ambiguous; did you mean - # `Trit(0)` or `Trit(ord('0'))`? - Trit('0') - - with self.assertRaises(TypeError): - # Technically, booleans are ints, but we still don't allow them - # because Trits and booleans mean very different things. - Trit(False) - - with self.assertRaises(TypeError): - # It's too easy to accidentally pass a null to the initializer. - # For safety, this also is not allowed. - Trit(None) - - # noinspection PyTypeChecker - def test_error_invalid_comparison(self): - """Trits can only be compared to other trits.""" - with self.assertRaises(TypeError): - Trit(0) == 0 - - with self.assertRaises(TypeError): - Trit(1) != {} - - with self.assertRaises(TypeError): - Trit(0) <= 0 - - with self.assertRaises(TypeError): - Trit(1) >= '0' - - with self.assertRaises(TypeError): - Trit(-1) < 1 - - with self.assertRaises(TypeError): - Trit(-1) > False - - -class TryteTestCase(TestCase): - def test_comparison(self): - """Testing equality and comparison of tryte values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_init_mixed_types(self): - """ - As a convenience, you are allowed to initialize a Tryte using ints. - """ - # Mixing Trits and ints is also OK. - tryte = Tryte([Trit(1), 1, -1]) - - self.assertEqual(tryte[0], Trit(1)) - self.assertEqual(tryte[1], Trit(1)) - - # Well, shucks. - self.assertNotEqual(tryte[2], Trit(-1)) - self.assertEqual(tryte[2].value, -1) - - # noinspection PyTypeChecker - def test_error_init_wrong_type(self): - """ - Attempting to initialize a tryte with something other than an array - of trits. - """ - with self.assertRaises(TypeError): - Tryte(Trit(1)) - - with self.assertRaises(TypeError): - Tryte(['1', False, None]) - - def test_error_init_not_enough_trits(self): - """Attempting to initialize a tryte with less than 3 trits.""" - with self.assertRaises(ValueError): - Tryte([]) - - with self.assertRaises(ValueError): - Tryte([Trit(0)]) - - with self.assertRaises(ValueError): - Tryte([Trit(0), Trit(0)]) - - def test_init_with_padding(self): - """Padding a tryte so that it has the correct number of trits.""" - tryte1 = Tryte([Trit(1)], pad=True) - - # Note that padding trits are applied first. - self.assertEqual(tryte1[0].value, -1) - self.assertEqual(tryte1[1].value, -1) - self.assertEqual(tryte1[2].value, 1) - - tryte2 = Tryte([Trit(0), Trit(1)], pad=True) - - self.assertEqual(tryte2[0].value, -1) - self.assertEqual(tryte2[1].value, 0) - self.assertEqual(tryte2[2].value, 1) - - def test_error_init_too_many_trits(self): - """Attempting to initialize a tryte with more than 3 trits.""" - with self.assertRaises(ValueError): - Tryte([Trit(-1), Trit(-1), Trit(-1), Trit(-1)]) + self.assertEqual( + binary_type(TryteString(b'RBTC9D9DCDQAEAKDCDFD9DSCFA')), + b'Hello, world!', + ) From cd134f55604eb6fbea893159b8d396cb419bee6a Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 22:53:45 -0500 Subject: [PATCH 017/239] Added a unit test. --- test/types_test.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/test/types_test.py b/test/types_test.py index 188097d..04f51d6 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -22,3 +22,11 @@ def test_hello_world(self): binary_type(TryteString(b'RBTC9D9DCDQAEAKDCDFD9DSCFA')), b'Hello, world!', ) + + def test_init_error_odd_length(self): + """ + Attempting to create a TryteString from a sequence with length not + divisible by 2. + """ + with self.assertRaises(ValueError): + TryteString(b'RBTC9D9DCDQAEAKDCDFD9DSCFA9') From 7878dfbfa59a9bbe45c57878268e7578141f8ac7 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 23:02:20 -0500 Subject: [PATCH 018/239] Renamed symbols to be more descriptive. --- iota/types.py | 32 +++++++++++++++++--------------- test/types_test.py | 2 +- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/iota/types.py b/iota/types.py index 182321f..b9c496b 100644 --- a/iota/types.py +++ b/iota/types.py @@ -32,7 +32,7 @@ class TryteString(object): def from_bytes(cls, bytes_): # type: (Union[binary_type, bytearray]) -> TryteString """Creates a TryteString from a byte string.""" - tryte_string = bytearray() + trytes = bytearray() # :bc: In Python 2, iterating over a byte string yields characters # instead of integers. @@ -42,41 +42,43 @@ def from_bytes(cls, bytes_): for c in bytes_: second, first = divmod(c, len(cls.alphabet)) - tryte_string.append(cls.alphabet[first]) - tryte_string.append(cls.alphabet[second]) + trytes.append(cls.alphabet[first]) + trytes.append(cls.alphabet[second]) - return cls(tryte_string) + return cls(trytes) - def __init__(self, value, pad=False): + def __init__(self, trytes, pad=False): # type: (Union[binary_type, bytearray], bool) -> None super(TryteString, self).__init__() - if len(value) % 2: + if len(trytes) % 2: if pad: - value += self.alphabet[0] + trytes += self.alphabet[0] else: raise ValueError( 'Length of TryteString must be divisible by 2.' ) - self.value = value if isinstance(value, bytearray) else bytearray(value) + self.trytes =\ + trytes if isinstance(trytes, bytearray) else bytearray(trytes) def __repr__(self): # type: () -> Text - return 'TryteString({value!r})'.format(value=binary_type(self.value)) + return 'TryteString({trytes!r})'.format(trytes=binary_type(self.trytes)) def __bytes__(self): # type: () -> Text """Converts the TryteString into a byte string.""" - byte_string = bytearray() + bytes_ = bytearray() - for i in range(0, len(self.value), 2): - byte_string.append( - self.index[self.value[i]] - + (self.index[self.value[i+1]] * len(self.index)) + for i in range(0, len(self.trytes), 2): + bytes_.append( + self.index[self.trytes[i]] + + (self.index[self.trytes[i + 1]] * len(self.index)) ) - return binary_type(byte_string) + return binary_type(bytes_) + # :bc: Magic method has a different name in Python 2. if PY2: __str__ = __bytes__ diff --git a/test/types_test.py b/test/types_test.py index 04f51d6..1ba5a0d 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -14,7 +14,7 @@ class TryteStringTestCase(TestCase): def test_hello_world(self): """PoC test for TryteString""" self.assertEqual( - TryteString.from_bytes(b'Hello, world!').value, + TryteString.from_bytes(b'Hello, world!').trytes, b'RBTC9D9DCDQAEAKDCDFD9DSCFA', ) From 14fe3c5026e8ec052a5c0608138bbcf2aaf893f8 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 23:10:46 -0500 Subject: [PATCH 019/239] Added support for equality operators. --- iota/types.py | 14 ++++++++++++++ test/types_test.py | 44 +++++++++++++++++++++++++++++++++++++++----- 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/iota/types.py b/iota/types.py index b9c496b..d4951a5 100644 --- a/iota/types.py +++ b/iota/types.py @@ -82,3 +82,17 @@ def __bytes__(self): # :bc: Magic method has a different name in Python 2. if PY2: __str__ = __bytes__ + + def __eq__(self, other): + # type: (TryteString) -> bool + if not isinstance(other, TryteString): + raise TypeError( + 'TryteStrings can only be compared to other TryteStrings.', + ) + + return self.trytes == other.trytes + + # :bc: In Python 2 this must be defined explicitly. + def __ne__(self, other): + # type: (TryteString) -> bool + return not (self == other) diff --git a/test/types_test.py b/test/types_test.py index 1ba5a0d..9ae9185 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -14,19 +14,53 @@ class TryteStringTestCase(TestCase): def test_hello_world(self): """PoC test for TryteString""" self.assertEqual( - TryteString.from_bytes(b'Hello, world!').trytes, - b'RBTC9D9DCDQAEAKDCDFD9DSCFA', + TryteString.from_bytes(b'Hello, IOTA!').trytes, + b'RBTC9D9DCDQAEASBYBCCKBFA', ) self.assertEqual( - binary_type(TryteString(b'RBTC9D9DCDQAEAKDCDFD9DSCFA')), - b'Hello, world!', + binary_type(TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA')), + b'Hello, IOTA!', ) + def test_equality_comparison(self): + """Comparing TryteStrings for equality.""" + trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + trytes2 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + trytes3 = TryteString(b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA') + + self.assertTrue(trytes1 == trytes2) + self.assertFalse(trytes1 != trytes2) + + self.assertFalse(trytes1 == trytes3) + self.assertTrue(trytes1 != trytes3) + + self.assertTrue(trytes1 is trytes1) + self.assertFalse(trytes1 is trytes2) + self.assertFalse(trytes1 is trytes3) + + # noinspection PyTypeChecker + def test_equality_comparison_error_wrong_type(self): + """ + Attempting to compare a TryteString with something that is not a + TryteString. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + + with self.assertRaises(TypeError): + trytes == b'RBTC9D9DCDQAEASBYBCCKBFA' + + with self.assertRaises(TypeError): + trytes == bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA') + + # Identity comparison still works though. + self.assertFalse(trytes is b'RBTC9D9DCDQAEASBYBCCKBFA') + self.assertFalse(trytes is bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) + def test_init_error_odd_length(self): """ Attempting to create a TryteString from a sequence with length not divisible by 2. """ with self.assertRaises(ValueError): - TryteString(b'RBTC9D9DCDQAEAKDCDFD9DSCFA9') + TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') From a85865e4a43e602cb3d88b32b674ec82fc35cf83 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 23:30:58 -0500 Subject: [PATCH 020/239] Made padding more forgiving. We're gonna need it later. --- iota/types.py | 29 ++++++++++++++++++----------- test/types_test.py | 25 ++++++++++++++++++------- 2 files changed, 36 insertions(+), 18 deletions(-) diff --git a/iota/types.py b/iota/types.py index d4951a5..aecbfee 100644 --- a/iota/types.py +++ b/iota/types.py @@ -47,20 +47,27 @@ def from_bytes(cls, bytes_): return cls(trytes) - def __init__(self, trytes, pad=False): - # type: (Union[binary_type, bytearray], bool) -> None + def __init__(self, trytes, pad=None): + # type: (Union[binary_type, bytearray], int) -> None + """ + :param trytes: Byte string or bytearray. + :param pad: Ensure at least this many trytes. + If there are too few, additional Tryte([-1, -1, -1]) values + will be appended to the TryteString. + + Note: If the TryteString is too long, it will _not_ be + truncated! + """ super(TryteString, self).__init__() - if len(trytes) % 2: - if pad: - trytes += self.alphabet[0] - else: - raise ValueError( - 'Length of TryteString must be divisible by 2.' - ) + if not isinstance(trytes, bytearray): + trytes = bytearray(trytes) - self.trytes =\ - trytes if isinstance(trytes, bytearray) else bytearray(trytes) + if pad: + for i in range(0, max(0, pad - len(trytes))): + trytes.append(self.alphabet[0]) + + self.trytes = trytes def __repr__(self): # type: () -> Text diff --git a/test/types_test.py b/test/types_test.py index 9ae9185..7d67cf2 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -57,10 +57,21 @@ def test_equality_comparison_error_wrong_type(self): self.assertFalse(trytes is b'RBTC9D9DCDQAEASBYBCCKBFA') self.assertFalse(trytes is bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) - def test_init_error_odd_length(self): - """ - Attempting to create a TryteString from a sequence with length not - divisible by 2. - """ - with self.assertRaises(ValueError): - TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') + def test_init_padding(self): + """Apply padding to ensure a TryteString has a minimum length.""" + trytes = TryteString( + trytes = + b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQL' + b'QNLPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY', + + pad = 81, + ) + + self.assertEqual( + trytes.trytes, + + # Note the additional Tryte([-1, -1, -1]) values appended to the + # end of the sequence (represented in ASCII as '9'). + b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' + b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999' + ) From ab23b6f21ba445f3cdb10fbad3f5dcf1dabf0635 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 29 Nov 2016 23:40:26 -0500 Subject: [PATCH 021/239] Try to decode trytes with odd length. --- iota/types.py | 10 ++++++++-- test/types_test.py | 16 ++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/iota/types.py b/iota/types.py index aecbfee..49b4505 100644 --- a/iota/types.py +++ b/iota/types.py @@ -79,9 +79,15 @@ def __bytes__(self): bytes_ = bytearray() for i in range(0, len(self.trytes), 2): + try: + first, second = self.trytes[i:i+2] + except ValueError: + bytes_ += b'?' + continue + bytes_.append( - self.index[self.trytes[i]] - + (self.index[self.trytes[i + 1]] * len(self.index)) + self.index[first] + + (self.index[second] * len(self.index)) ) return binary_type(bytes_) diff --git a/test/types_test.py b/test/types_test.py index 7d67cf2..f0f815c 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -75,3 +75,19 @@ def test_init_padding(self): b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999' ) + + def test_bytes_conversion_partial_sequence(self): + """ + Attempting to convert an odd number of trytes into bytes. + + Note: This behavior is undefined. Think trying to decode a + sequence of octets using UTF-16, and finding that there's an odd + number of octets. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') + + # The un-decodable tryte is replaced with '?'. + self.assertEqual( + binary_type(trytes), + b'Hello, IOTA!?', + ) From ba7767ec54ea1bf775653977314a8dea6e203e5f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 00:44:01 -0500 Subject: [PATCH 022/239] Integrated bytes <-> trytes into codecs lib. --- iota/__init__.py | 5 +- iota/codecs.py | 133 ++++++++++++++++++++++++++++++++++++++++++++ iota/types.py | 62 +++++++++------------ test/codecs_test.py | 106 +++++++++++++++++++++++++++++++++++ test/types_test.py | 23 ++++++++ 5 files changed, 290 insertions(+), 39 deletions(-) create mode 100644 iota/codecs.py create mode 100644 test/codecs_test.py diff --git a/iota/__init__.py b/iota/__init__.py index c0e0430..cbe42c5 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -5,11 +5,12 @@ DEFAULT_PORT = 14265 # Make some imports accessible from the top level of the package. -# noinspection PyUnresolvedReferences from .adapter import * -# noinspection PyUnresolvedReferences from .api import * +# Activate TrytesCodec. +from .codecs import * + # Don't forget to update version number in setup.py! __version__ = '1.0.0' diff --git a/iota/codecs.py b/iota/codecs.py new file mode 100644 index 0000000..39d35c2 --- /dev/null +++ b/iota/codecs.py @@ -0,0 +1,133 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from codecs import Codec, CodecInfo, register as lookup_function + +from six import binary_type + +__all__ = [ + 'TrytesCodec', + 'TrytesDecodeError', +] + + +class TrytesDecodeError(ValueError): + """Indicates that a tryte string could not be decoded to bytes.""" + pass + + +class TrytesCodec(Codec): + """Codec for converting byte strings into trytes and vice versa.""" + name = 'trytes' + + # :bc: Without the bytearray cast, Python 2 will populate the dict + # with characters instead of integers. + # noinspection SpellCheckingInspection + alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) + index = dict(zip(alphabet.values(), alphabet.keys())) + + @classmethod + def get_codec_info(cls): + """ + Returns information used by the codecs library to configure the + codec for use. + """ + codec = cls() + + return CodecInfo( + encode = codec.encode, + decode = codec.decode, + _is_text_encoding = False, + ) + + # noinspection PyShadowingBuiltins + def encode(self, input, errors='strict'): + """Encodes a byte string into trytes.""" + if isinstance(input, memoryview): + input = input.tobytes() + + if not isinstance(input, (binary_type, bytearray)): + raise TypeError("Can't encode {type}; byte string expected.".format( + type = type(input).__name__, + )) + + trytes = bytearray() + + # :bc: In Python 2, iterating over a byte string yields characters + # instead of integers. + if not isinstance(input, bytearray): + input = bytearray(input) + + for c in input: + second, first = divmod(c, len(self.alphabet)) + + trytes.append(self.alphabet[first]) + trytes.append(self.alphabet[second]) + + return binary_type(trytes), len(input) + + # noinspection PyShadowingBuiltins + def decode(self, input, errors='strict'): + """Decodes a tryte string into bytes.""" + if isinstance(input, memoryview): + input = input.tobytes() + + if not isinstance(input, (binary_type, bytearray)): + raise TypeError("Can't decode {type}; byte string expected.".format( + type = type(input).__name__, + )) + + # :bc: In Python 2, iterating over a byte string yields characters + # instead of integers. + if not isinstance(input, bytearray): + input = bytearray(input) + + bytes_ = bytearray() + + for i in range(0, len(input), 2): + try: + first, second = input[i:i+2] + except ValueError: + if errors == 'strict': + raise TrytesDecodeError( + "'{name}' codec can't decode value; " + "tryte sequence has odd length.".format( + name = self.name, + ), + ) + elif errors == 'replace': + bytes_ += b'?' + + continue + + try: + bytes_.append( + self.index[first] + + (self.index[second] * len(self.index)) + ) + except ValueError: + # This combination of trytes yields a value > 255 when + # decoded. Naturally, we can't represent this using ASCII. + if errors == 'strict': + raise TrytesDecodeError( + "'{name}' codec can't decode trytes {pair} at position {i}-{j}: " + "ordinal not in range(255)".format( + name = self.name, + pair = chr(first) + chr(second), + i = i, + j = i+1, + ), + ) + elif errors == 'replace': + bytes_ += b'?' + + return binary_type(bytes_), len(input) + + +@lookup_function +def check_trytes_codec(encoding): + if encoding == TrytesCodec.name: + return TrytesCodec.get_codec_info() + + return None diff --git a/iota/types.py b/iota/types.py index 49b4505..54cd044 100644 --- a/iota/types.py +++ b/iota/types.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from codecs import encode, decode from typing import Text, Union from six import PY2, binary_type @@ -22,30 +23,11 @@ class TryteString(object): (256 possible values), while the tryte string alphabet only has 27 characters (one for each possible tryte configuration). """ - # :bc: Without the bytearray cast, Python 2 will populate the dict - # with characters instead of integers. - # noinspection SpellCheckingInspection - alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) - index = dict(zip(alphabet.values(), alphabet.keys())) - @classmethod def from_bytes(cls, bytes_): # type: (Union[binary_type, bytearray]) -> TryteString """Creates a TryteString from a byte string.""" - trytes = bytearray() - - # :bc: In Python 2, iterating over a byte string yields characters - # instead of integers. - if not isinstance(bytes_, bytearray): - bytes_ = bytearray(bytes_) - - for c in bytes_: - second, first = divmod(c, len(cls.alphabet)) - - trytes.append(cls.alphabet[first]) - trytes.append(cls.alphabet[second]) - - return cls(trytes) + return cls(encode(bytes_, 'trytes')) def __init__(self, trytes, pad=None): # type: (Union[binary_type, bytearray], int) -> None @@ -64,8 +46,7 @@ def __init__(self, trytes, pad=None): trytes = bytearray(trytes) if pad: - for i in range(0, max(0, pad - len(trytes))): - trytes.append(self.alphabet[0]) + trytes += b'9' * max(0, pad - len(trytes)) self.trytes = trytes @@ -75,27 +56,34 @@ def __repr__(self): def __bytes__(self): # type: () -> Text - """Converts the TryteString into a byte string.""" - bytes_ = bytearray() - - for i in range(0, len(self.trytes), 2): - try: - first, second = self.trytes[i:i+2] - except ValueError: - bytes_ += b'?' - continue - - bytes_.append( - self.index[first] - + (self.index[second] * len(self.index)) - ) + """ + Converts the TryteString into a byte string. - return binary_type(bytes_) + If the value contains any trytes that can't be converted, they will + be replaced with '?'. + + If you want different handling of un-convertible trytes, use + `as_bytes` instead. + """ + return self.as_bytes(errors='replace') # :bc: Magic method has a different name in Python 2. if PY2: __str__ = __bytes__ + def as_bytes(self, errors='strict'): + # type: (Text) -> binary_type + """ + Converts the TryteString into a byte string. + + :param errors: How to handle trytes that can't be converted: + - 'strict': raise a TrytesDecodeError. + - 'replace': replace with '?'. + - 'ignore': omit the tryte from the byte string. + """ + # :bc: In Python 2, `decode` does not accept keyword arguments. + return decode(self.trytes, 'trytes', errors) + def __eq__(self, other): # type: (TryteString) -> bool if not isinstance(other, TryteString): diff --git a/test/codecs_test.py b/test/codecs_test.py new file mode 100644 index 0000000..79c44ed --- /dev/null +++ b/test/codecs_test.py @@ -0,0 +1,106 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from codecs import encode, decode +from unittest import TestCase + + +# noinspection SpellCheckingInspection +from iota.codecs import TrytesDecodeError + + +# noinspection SpellCheckingInspection +class TrytesCodecTestCase(TestCase): + def test_encode_byte_string(self): + """Encoding a byte string into trytes.""" + self.assertEqual( + encode(b'Hello, IOTA!', 'trytes'), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + def test_encode_bytearray(self): + """Encoding a bytearray into trytes.""" + self.assertEqual( + encode(bytearray(b'Hello, IOTA!'), 'trytes'), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + def test_encode_error_wrong_type(self): + """Attempting to encode a value with an incompatible type.""" + with self.assertRaises(TypeError): + # List value not accepted; it can contain things other than bytes + # (ordinals in range(255), that is). + encode([72, 101, 108, 108, 111, 44, 32, 73, 79, 84, 65, 33], 'trytes') + + with self.assertRaises(TypeError): + # Unicode strings not accepted; it is ambiguous whether and how + # to encode to bytes. + encode('Hello, IOTA!', 'trytes') + + def test_decode_byte_string(self): + """Decoding trytes to a byte string.""" + self.assertEqual( + decode(b'RBTC9D9DCDQAEASBYBCCKBFA', 'trytes'), + b'Hello, IOTA!', + ) + + def test_decode_bytearray(self): + """Decoding a bytearray of trytes into a byte string.""" + self.assertEqual( + decode(bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA'), 'trytes'), + b'Hello, IOTA!', + ) + + def test_decode_wrong_length_errors_strict(self): + """ + Attempting to decode an odd number of trytes with errors='strict'. + """ + with self.assertRaises(TrytesDecodeError): + decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', 'trytes', 'strict') + + def test_decode_wrong_length_errors_ignore(self): + """ + Attempting to decode an odd number of trytes with errors='ignore'. + """ + self.assertEqual( + decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', 'trytes', 'ignore'), + b'Hello, IOTA!', + ) + + def test_decode_wrong_length_errors_replace(self): + """ + Attempting to decode an odd number of trytes with errors='replace'. + """ + self.assertEqual( + decode(b'RBTC9D9DCDQAEASBYBCCKBFA9', 'trytes', 'replace'), + b'Hello, IOTA!?', + ) + + def test_decode_invalid_pair_errors_strict(self): + """ + Attempting to decode an un-decodable pair of trytes with + errors='strict'. + """ + with self.assertRaises(TrytesDecodeError): + decode(b'ZJVYUGTDRPDYFGFXMK', 'trytes', 'strict') + + def test_decode_invalid_pair_errors_ignore(self): + """ + Attempting to decode an un-decodable pair of trytes with + errors='ignore'. + """ + self.assertEqual( + decode(b'ZJVYUGTDRPDYFGFXMK', 'trytes', 'ignore'), + b'\xd2\x80\xc3', + ) + + def test_decode_invalid_pair_errors_replace(self): + """ + Attempting to decode an un-decodable pair of trytes with + errors='replace'. + """ + self.assertEqual( + decode(b'ZJVYUGTDRPDYFGFXMK', 'trytes', 'replace'), + b'??\xd2\x80??\xc3??', + ) diff --git a/test/types_test.py b/test/types_test.py index f0f815c..66bda30 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -91,3 +91,26 @@ def test_bytes_conversion_partial_sequence(self): binary_type(trytes), b'Hello, IOTA!?', ) + + def test_bytes_conversion_non_ascii(self): + """ + Converting a sequence of trytes into bytes yields non-ASCII + characters. + + This most likely indicates that the trytes didn't start out as + bytes. Think trying to decode a sequence of octets using UTF-8, + but the octets are actually JPEG data. + """ + trytes = TryteString( + b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' + b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', + ) + + self.assertEqual( + binary_type(trytes), + + # It's a pretty safe bet that this particular sequence of trytes + # was never meant to be decoded to bytes. + b'??\xd2\x80??\xc3???\x16?\xd0?Q??????' + b'\xcd?)????\x0f??\xf5???\xb7??\x19\x00?', + ) From df8f78992fdeca08a3b20d2988c400b7c1f004c2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 00:44:41 -0500 Subject: [PATCH 023/239] Minor code cleanup. --- iota/codecs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iota/codecs.py b/iota/codecs.py index 39d35c2..e1c59b2 100644 --- a/iota/codecs.py +++ b/iota/codecs.py @@ -52,13 +52,13 @@ def encode(self, input, errors='strict'): type = type(input).__name__, )) - trytes = bytearray() - # :bc: In Python 2, iterating over a byte string yields characters # instead of integers. if not isinstance(input, bytearray): input = bytearray(input) + trytes = bytearray() + for c in input: second, first = divmod(c, len(self.alphabet)) From 0c068c0e470ed63d1b1b1a57b0caad912268383c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 00:53:55 -0500 Subject: [PATCH 024/239] Made tryte -> byte errors consistent. `errors="strict"` is the default for other codecs, it should be the default for trytes as well. --- iota/types.py | 2 +- test/types_test.py | 86 ++++++++++++++++++++++++++++++++++++++++------ 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/iota/types.py b/iota/types.py index 54cd044..5eabcb2 100644 --- a/iota/types.py +++ b/iota/types.py @@ -65,7 +65,7 @@ def __bytes__(self): If you want different handling of un-convertible trytes, use `as_bytes` instead. """ - return self.as_bytes(errors='replace') + return self.as_bytes() # :bc: Magic method has a different name in Python 2. if PY2: diff --git a/test/types_test.py b/test/types_test.py index 66bda30..19bac88 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -6,6 +6,7 @@ from six import binary_type +from iota import TrytesDecodeError from iota.types import TryteString @@ -86,11 +87,8 @@ def test_bytes_conversion_partial_sequence(self): """ trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') - # The un-decodable tryte is replaced with '?'. - self.assertEqual( - binary_type(trytes), - b'Hello, IOTA!?', - ) + with self.assertRaises(TrytesDecodeError): + binary_type(trytes) def test_bytes_conversion_non_ascii(self): """ @@ -106,11 +104,79 @@ def test_bytes_conversion_non_ascii(self): b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', ) + # This tryte sequence cannot be converted into a byte string; it + # contains too many ordinals > 255. + with self.assertRaises(TrytesDecodeError): + binary_type(trytes) + + def test_as_bytes_partial_sequence_errors_strict(self): + """ + Attempting to convert an odd number of trytes into bytes using the + `as_bytes` method with errors='strict'. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') + + with self.assertRaises(TrytesDecodeError): + trytes.as_bytes(errors='strict') + + def test_as_bytes_partial_sequence_errors_ignore(self): + """ + Attempting to convert an odd number of trytes into bytes using the + `as_bytes` method with errors='ignore'. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') + self.assertEqual( - binary_type(trytes), + trytes.as_bytes(errors='ignore'), + + # The extra tryte is ignored. + b'Hello, IOTA!', + ) + + def test_as_bytes_partial_sequence_errors_replace(self): + """ + Attempting to convert an odd number of trytes into bytes using the + `as_bytes` method with errors='replace'. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') - # It's a pretty safe bet that this particular sequence of trytes - # was never meant to be decoded to bytes. - b'??\xd2\x80??\xc3???\x16?\xd0?Q??????' - b'\xcd?)????\x0f??\xf5???\xb7??\x19\x00?', + self.assertEqual( + trytes.as_bytes(errors='replace'), + + # The extra tryte is replaced with '?'. + b'Hello, IOTA!?', + ) + + def test_as_bytes_non_ascii_errors_strict(self): + """ + Converting a sequence of trytes into bytes using the `as_bytes` + method yields non-ASCII characters, and errors='strict'. + """ + trytes = TryteString(b'ZJVYUGTDRPDYFGFXMK') + + with self.assertRaises(TrytesDecodeError): + trytes.as_bytes(errors='strict') + + def test_as_bytes_non_ascii_errors_ignore(self): + """ + Converting a sequence of trytes into bytes using the `as_bytes` + method yields non-ASCII characters, and errors='ignore'. + """ + trytes = TryteString(b'ZJVYUGTDRPDYFGFXMK') + + self.assertEqual( + trytes.as_bytes(errors='ignore'), + b'\xd2\x80\xc3', + ) + + def test_as_bytes_non_ascii_errors_replace(self): + """ + Converting a sequence of trytes into bytes using the `as_bytes` + method yields non-ASCII characters, and errors='replace'. + """ + trytes = TryteString(b'ZJVYUGTDRPDYFGFXMK') + + self.assertEqual( + trytes.as_bytes(errors='replace'), + b'??\xd2\x80??\xc3??', ) From f941c808705e768b9ad6813ee404c9f93559f61f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 01:03:39 -0500 Subject: [PATCH 025/239] Add validation to TryteString.__init__. --- iota/types.py | 12 ++++++++++++ test/types_test.py | 8 ++++++++ 2 files changed, 20 insertions(+) diff --git a/iota/types.py b/iota/types.py index 5eabcb2..f2b0988 100644 --- a/iota/types.py +++ b/iota/types.py @@ -7,6 +7,8 @@ from six import PY2, binary_type +from iota import TrytesCodec + class TryteString(object): """ @@ -45,6 +47,16 @@ def __init__(self, trytes, pad=None): if not isinstance(trytes, bytearray): trytes = bytearray(trytes) + for i, ordinal in enumerate(trytes): + if ordinal not in TrytesCodec.index: + raise ValueError( + 'Invalid character {char} at position {i} ' + '(expected A-Z or 9).'.format( + char = chr(ordinal), + i = i, + ), + ) + if pad: trytes += b'9' * max(0, pad - len(trytes)) diff --git a/test/types_test.py b/test/types_test.py index 19bac88..c567bfc 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -77,6 +77,14 @@ def test_init_padding(self): b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999' ) + def test_init_error_invalid_characters(self): + """ + Attempting to initialize a TryteString with a value that contains + invalid characters. + """ + with self.assertRaises(ValueError): + TryteString(b'not valid') + def test_bytes_conversion_partial_sequence(self): """ Attempting to convert an odd number of trytes into bytes. From 2634b09e87ddc8288d9a8dc577a142111d7d7a2c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 01:15:34 -0500 Subject: [PATCH 026/239] Convert trytes in getNodeInfo response. --- iota/__init__.py | 7 +++---- iota/api.py | 16 +++++++++++++--- test/api_test.py | 39 ++++++++++++++++++++++++++++++++++----- 3 files changed, 50 insertions(+), 12 deletions(-) diff --git a/iota/__init__.py b/iota/__init__.py index cbe42c5..7acdbdb 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -4,13 +4,12 @@ DEFAULT_PORT = 14265 -# Make some imports accessible from the top level of the package. -from .adapter import * -from .api import * - # Activate TrytesCodec. from .codecs import * +# Make some imports accessible from the top level of the package. +from .adapter import * +from .api import * # Don't forget to update version number in setup.py! __version__ = '1.0.0' diff --git a/iota/api.py b/iota/api.py index 0b1dfc6..a8e10d0 100644 --- a/iota/api.py +++ b/iota/api.py @@ -4,7 +4,10 @@ from typing import Callable, Iterable, Optional, Text, Union +from six import binary_type + from iota.adapter import BaseAdapter, resolve_adapter +from iota.types import TryteString __all__ = [ 'IotaApi', @@ -175,7 +178,14 @@ def get_node_info(self): :see: https://iota.readme.io/docs/getnodeinfo """ - return self.__getattr__('getNodeInfo')() + response = self.getNodeInfo() + + for key in ('latestMilestone', 'latestSolidSubtangleMilestone'): + trytes = response.get(key) + if trytes: + response[key] = TryteString(trytes.encode('ascii')) + + return response def get_tips(self): # type: () -> dict @@ -222,7 +232,7 @@ def interrupt_attaching_to_tangle(self): :see: https://iota.readme.io/docs/interruptattachingtotangle """ - return self.__getattr__('interruptAttachingToTangle')() + return self.interruptAttachingToTangle() def remove_neighbors(self, uris): # type: (Iterable[Text]) -> dict @@ -235,7 +245,7 @@ def remove_neighbors(self, uris): :see: https://iota.readme.io/docs/removeneighors """ - return self.__getattr__('removeNeighbors')(uris=uris) + return self.removeNeighbors(uris=uris) def store_transactions(self, trytes): # type: (Iterable[Text]) -> dict diff --git a/test/api_test.py b/test/api_test.py index a44cd0b..8466843 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -7,6 +7,7 @@ from iota import IotaApi from iota.adapter import BaseAdapter +from iota.types import TryteString class MockAdapter(BaseAdapter): @@ -66,15 +67,43 @@ def test_custom_command_with_arguments(self): [({'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}, {})], ) - def test_supported_command(self): - """Sending a supported command.""" - expected_response = {'appName': 'IRI', 'appVersion': '1.1.1'} + def test_get_node_info_happy_path(self): + """Successful invocation of `getNodeInfo`.""" + # noinspection SpellCheckingInspection + expected_response = { + 'appName': 'IRI', + 'appVersion': '1.0.8.nu', + 'duration': 1, + 'jreAvailableProcessors': 4, + 'jreFreeMemory': 91707424, + 'jreMaxMemory': 1908932608, + 'jreTotalMemory': 122683392, + 'latestMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', + 'latestMilestoneIndex': 107, + 'latestSolidSubtangleMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', + 'latestSolidSubtangleMilestoneIndex': 107, + 'neighbors': 2, + 'packetsQueueSize': 0, + 'time': 1477037811737, + 'tips': 3, + 'transactionsToRequest': 0 + } adapter = MockAdapter(expected_response) api = IotaApi(adapter) response = api.get_node_info() - self.assertEqual(response, expected_response) + self.assertDictEqual(response, expected_response) + + self.assertIsInstance(response['latestMilestone'], TryteString) + + self.assertIsInstance( + response['latestSolidSubtangleMilestone'], + TryteString, + ) - self.assertListEqual(adapter.requests, [({'command': 'getNodeInfo'}, {})]) + self.assertListEqual( + adapter.requests, + [({'command': 'getNodeInfo'}, {})], + ) From bba9994171f2d3a16f1d1129f60b103cdbd7ef95 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 01:20:40 -0500 Subject: [PATCH 027/239] Removed unused import. --- iota/api.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/iota/api.py b/iota/api.py index a8e10d0..e7757c9 100644 --- a/iota/api.py +++ b/iota/api.py @@ -4,8 +4,6 @@ from typing import Callable, Iterable, Optional, Text, Union -from six import binary_type - from iota.adapter import BaseAdapter, resolve_adapter from iota.types import TryteString From d7ecfa558cf2d8bef660ae4d060224c53a98ef84 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 20:51:33 -0500 Subject: [PATCH 028/239] Implemented `attachToTangle`. --- iota/api.py | 73 ++++++++++- iota/types.py | 40 ++++-- test/api_test.py | 308 ++++++++++++++++++++++++++++++++++++++++++++- test/types_test.py | 48 ++++++- 4 files changed, 444 insertions(+), 25 deletions(-) diff --git a/iota/api.py b/iota/api.py index e7757c9..55f64b1 100644 --- a/iota/api.py +++ b/iota/api.py @@ -5,7 +5,7 @@ from typing import Callable, Iterable, Optional, Text, Union from iota.adapter import BaseAdapter, resolve_adapter -from iota.types import TryteString +from iota.types import TransactionId, TryteString __all__ = [ 'IotaApi', @@ -64,10 +64,10 @@ def attach_to_tangle( self, trunk_transaction, branch_transaction, - min_weight_magnitude, - trytes + trytes, + min_weight_magnitude = 18, ): - # type: (Text, Text, int, Iterable[Text]) -> dict + # type: (TransactionId, TransactionId, Iterable[TryteString], int) -> dict """ Attaches the specified transactions (trytes) to the Tangle by doing Proof of Work. You need to supply branchTransaction as well as @@ -80,7 +80,70 @@ def attach_to_tangle( :see: https://iota.readme.io/docs/attachtotangle """ - raise NotImplementedError('Not implemented yet.') + if not isinstance(trunk_transaction, TransactionId): + raise TypeError( + 'trunk_transaction has wrong type ' + '(expected TransactionID, actual {type}).'.format( + type = type(trunk_transaction).__name__, + ), + ) + + if not isinstance(branch_transaction, TransactionId): + raise TypeError( + 'branch_transaction has wrong type ' + '(expected TransactionID, actual {type}).'.format( + type = type(branch_transaction).__name__, + ), + ) + + if type(min_weight_magnitude) is not int: + raise TypeError( + 'min_weight_magnitude has wrong type ' + '(expected int, actual {type}).'.format( + type = type(min_weight_magnitude).__name__, + ), + ) + + if min_weight_magnitude < 18: + raise ValueError( + 'min_weight_magnitude is too small ' + '(expected >= 18, actual {value}).'.format( + value = min_weight_magnitude, + ), + ) + + if not isinstance(trytes, Iterable): + raise TypeError( + 'trytes has wrong type (expected Iterable, actual {type}).'.format( + type = type(trytes).__name__, + ), + ) + + if not trytes: + raise ValueError('trytes must not be empty.') + + for i, t in enumerate(trytes): + if not isinstance(t, TryteString): + raise TypeError( + 'trytes[{i}] has wrong type ' + '(expected TryteString, actual {type}).'.format( + i = i, + type = type(t).__name__, + ), + ) + + response = self.attachToTangle( + trunkTransaction = trunk_transaction.trytes, + branchTransaction = branch_transaction.trytes, + minWeightMagnitude = min_weight_magnitude, + trytes = [t.trytes for t in trytes], + ) + + trytes = response.get('trytes') + if trytes: + response['trytes'] = [TryteString(t.encode('ascii')) for t in trytes] + + return response def broadcast_transactions(self, trytes): # type: (Iterable[Text]) -> dict diff --git a/iota/types.py b/iota/types.py index f2b0988..18a9cdd 100644 --- a/iota/types.py +++ b/iota/types.py @@ -32,7 +32,7 @@ def from_bytes(cls, bytes_): return cls(encode(bytes_, 'trytes')) def __init__(self, trytes, pad=None): - # type: (Union[binary_type, bytearray], int) -> None + # type: (Union[binary_type, bytearray, TryteString], int) -> None """ :param trytes: Byte string or bytearray. :param pad: Ensure at least this many trytes. @@ -44,18 +44,23 @@ def __init__(self, trytes, pad=None): """ super(TryteString, self).__init__() - if not isinstance(trytes, bytearray): - trytes = bytearray(trytes) - - for i, ordinal in enumerate(trytes): - if ordinal not in TrytesCodec.index: - raise ValueError( - 'Invalid character {char} at position {i} ' - '(expected A-Z or 9).'.format( - char = chr(ordinal), - i = i, - ), - ) + if isinstance(trytes, TryteString): + # Create a copy of the incoming TryteString's trytes, to ensure + # we don't modify it when we apply padding. + trytes = bytearray(trytes.trytes) + else: + if not isinstance(trytes, bytearray): + trytes = bytearray(trytes) + + for i, ordinal in enumerate(trytes): + if ordinal not in TrytesCodec.index: + raise ValueError( + 'Invalid character {char} at position {i} ' + '(expected A-Z or 9).'.format( + char = chr(ordinal), + i = i, + ), + ) if pad: trytes += b'9' * max(0, pad - len(trytes)) @@ -109,3 +114,12 @@ def __eq__(self, other): def __ne__(self, other): # type: (TryteString) -> bool return not (self == other) + + +class TransactionId(TryteString): + """A TryteString that acts as a transaction ID.""" + def __init__(self, trytes): + super(TransactionId, self).__init__(trytes, pad=81) + + if len(self.trytes) > 81: + raise ValueError('TransactionIds must be 81 trytes long.') diff --git a/test/api_test.py b/test/api_test.py index 8466843..8826bef 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -7,7 +7,7 @@ from iota import IotaApi from iota.adapter import BaseAdapter -from iota.types import TryteString +from iota.types import TryteString, TransactionId class MockAdapter(BaseAdapter): @@ -67,9 +67,306 @@ def test_custom_command_with_arguments(self): [({'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}, {})], ) + +# noinspection SpellCheckingInspection +class AttachToTangleTestCase(TestCase): + def setUp(self): + super(AttachToTangleTestCase, self).setUp() + + self.adapter = MockAdapter() + self.api = IotaApi(self.adapter) + + def test_happy_path(self): + """Successful invocation of `attachToTangle`.""" + expected_response = { + 'trytes':['TRYTEVALUEHERE'] + } + + self.adapter.response = expected_response + + trunk_transaction =\ + TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + min_weight_magnitude = 20 + trytes = [TryteString(b'TRYTVALUEHERE')] + + response = self.api.attach_to_tangle( + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + min_weight_magnitude = min_weight_magnitude, + trytes = trytes, + ) + + self.assertDictEqual(response, expected_response) + + self.assertListEqual( + list(map(type, response['trytes'])), + [TryteString], + ) + + self.assertListEqual( + self.adapter.requests, + [( + { + 'command': 'attachToTangle', + 'trunkTransaction': trunk_transaction.trytes, + 'branchTransaction': branch_transaction.trytes, + 'minWeightMagnitude': min_weight_magnitude, + 'trytes': [trytes[0].trytes], + }, + + {}, + )] + ) + + # noinspection PyTypeChecker + def test_error_trunk_transaction_invalid(self): + """ + Attempting to call `attachToTangle`, but the `trunkTransaction` + parameter is not valid. + """ + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + trytes = [TryteString(b'TRYTEVALUEHERE')] + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Nope; the trytes have to be in a container. + trunk_transaction = + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Sorry, not good enough; it's gotta be a TransactionId. + trunk_transaction = + TryteString( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ), + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Now you're not making any sense. + trunk_transaction = None, + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Are you even listening to me? + trunk_transaction = 42, + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + # noinspection PyTypeChecker + def test_error_branch_transaction_invalid(self): + """ + Attempting to call `attachToTangle`, but the `branchTransaction` + parameter is not valid. + """ + trunk_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + trytes = [TryteString(b'TRYTEVALUEHERE')] + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Nope; the trytes have to be in a container. + branch_transaction = + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Sorry, not good enough; it's gotta be a TransactionId. + branch_transaction = + TryteString( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ), + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Now you're not making any sense. + branch_transaction = None, + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Are you even listening to me? + branch_transaction = 42, + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + # noinspection PyTypeChecker + def test_error_min_weight_magnitude_invalid(self): + """ + Attempting to call `attachToTangle`, but the `minWeightMagnitude` + parameter is not valid. + """ + trunk_transaction =\ + TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + trytes = [TryteString(b'TRYTVALUEHERE')] + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Nice try, but it's gotta be an int. + min_weight_magnitude = 18.0, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # Oh, come on. You know what I meant! + min_weight_magnitude = True, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # I swear you're doing this on purpose just to annoy me. + min_weight_magnitude = 'eighteen', + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # This parameter is required. + min_weight_magnitude = None, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(ValueError): + self.api.attach_to_tangle( + # Minimum value for this parameter is 18. + min_weight_magnitude = 17, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + # noinspection PyTypeChecker + def test_error_trytes_invalid(self): + """ + Attempting to call `attachToTangle`, but the `trytes` parameter is + not valid. + """ + trunk_transaction =\ + TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # It's gotta be a list. + trytes = TryteString(b'TRYTEVALUEHERE'), + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + ) + + with self.assertRaises(ValueError): + self.api.attach_to_tangle( + # Ok, you got the list part down, but you have to put something + # inside it. + trytes = [], + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + ) + + with self.assertRaises(TypeError): + self.api.attach_to_tangle( + # No, no, no! They all have to be TryteStrings! + trytes = [TryteString(b'TRYTEVALUEHERE'), b'QUACK'], + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + ) + + +# noinspection SpellCheckingInspection +class GetNodeInfoTestCase(TestCase): + def setUp(self): + super(GetNodeInfoTestCase, self).setUp() + + self.adapter = MockAdapter() + self.api = IotaApi(self.adapter) + def test_get_node_info_happy_path(self): """Successful invocation of `getNodeInfo`.""" - # noinspection SpellCheckingInspection expected_response = { 'appName': 'IRI', 'appVersion': '1.0.8.nu', @@ -89,10 +386,9 @@ def test_get_node_info_happy_path(self): 'transactionsToRequest': 0 } - adapter = MockAdapter(expected_response) - api = IotaApi(adapter) + self.adapter.response = expected_response - response = api.get_node_info() + response = self.api.get_node_info() self.assertDictEqual(response, expected_response) @@ -104,6 +400,6 @@ def test_get_node_info_happy_path(self): ) self.assertListEqual( - adapter.requests, + self.adapter.requests, [({'command': 'getNodeInfo'}, {})], ) diff --git a/test/types_test.py b/test/types_test.py index c567bfc..f160105 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -7,7 +7,7 @@ from six import binary_type from iota import TrytesDecodeError -from iota.types import TryteString +from iota.types import TransactionId, TryteString # noinspection SpellCheckingInspection @@ -58,6 +58,14 @@ def test_equality_comparison_error_wrong_type(self): self.assertFalse(trytes is b'RBTC9D9DCDQAEASBYBCCKBFA') self.assertFalse(trytes is bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) + def test_init_from_tryte_string(self): + """Initializing a TryteString from another TryteString.""" + trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + trytes2 = TryteString(trytes1) + + self.assertFalse(trytes1 is trytes2) + self.assertTrue(trytes1 == trytes2) + def test_init_padding(self): """Apply padding to ensure a TryteString has a minimum length.""" trytes = TryteString( @@ -77,6 +85,19 @@ def test_init_padding(self): b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999' ) + def test_init_from_tryte_string_with_padding(self): + """ + Initializing a TryteString from another TryteString, and padding + the new one to a specific length. + """ + trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + trytes2 = TryteString(trytes1, pad=27) + + self.assertFalse(trytes1 is trytes2) + self.assertFalse(trytes1 == trytes2) + + self.assertEqual(trytes2.trytes, b'RBTC9D9DCDQAEASBYBCCKBFA999') + def test_init_error_invalid_characters(self): """ Attempting to initialize a TryteString with a value that contains @@ -188,3 +209,28 @@ def test_as_bytes_non_ascii_errors_replace(self): trytes.as_bytes(errors='replace'), b'??\xd2\x80??\xc3??', ) + +# noinspection SpellCheckingInspection +class TransactionIdTestCase(TestCase): + def test_init_automatic_pad(self): + """Transaction IDs are automatically padded to 81 trytes.""" + txn = TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' + ) + + self.assertEqual( + txn.trytes, + + # Note the extra 9's added to the end. + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + def test_init_error_too_long(self): + """Attempting to create a transaction ID longer than 81 trytes.""" + with self.assertRaises(ValueError): + TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' + ) From 81b73cb085382f8fcae89b12240caa912bbde84e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 20:57:52 -0500 Subject: [PATCH 029/239] Injected command pattern into IotaApi. --- iota/api.py | 42 +++++++++++++++++++++++++++++++++++------- 1 file changed, 35 insertions(+), 7 deletions(-) diff --git a/iota/api.py b/iota/api.py index 55f64b1..a7dafc0 100644 --- a/iota/api.py +++ b/iota/api.py @@ -31,7 +31,7 @@ def __init__(self, adapter): self.adapter = adapter # type: BaseAdapter def __getattr__(self, command): - # type: (Text, dict) -> Callable[[...], dict] + # type: (Text, dict) -> Command """ Sends an arbitrary API command to the node. @@ -39,13 +39,8 @@ def __getattr__(self, command): methods, or if you just want to troll your node for awhile. :param command: The name of the command to send. - - :return: Decoded response from the node. - :raise: BadApiResponse if the node sends back an error response. """ - def command_sender(**kwargs): - return self.adapter.send_request(dict(command=command, **kwargs)) - return command_sender + return Command(self.adapter, command) def add_neighbors(self, uris): # type: (Iterable[Text]) -> dict @@ -318,3 +313,36 @@ def store_transactions(self, trytes): :see: https://iota.readme.io/docs/storetransactions """ raise NotImplementedError('Not implemented yet.') + + +class Command(object): + """An API command ready to send to the node.""" + def __init__(self, adapter, command): + # type: (BaseAdapter, Text) -> None + super(Command, self).__init__() + + self.adapter = adapter + self.command = command + self.response = None # type: dict + + def __call__(self, **kwargs): + # type: (dict) -> dict + """Sends the command to the node.""" + if self.called: + raise ValueError('Command has already been called.') + + self.response = self.adapter.send_request(self.create_payload(kwargs)) + return self.response + + @property + def called(self): + # type: () -> bool + """Returns whether this command has been called.""" + return self.response is not None + + def create_payload(self, params): + # type: (dict) -> dict + """Returns the actual payload to be sent to the node.""" + payload = {'command': self.command} + payload.update(params) + return payload From 5006451e005c52c52ee3ee37b4e1c72dfae73bdc Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 21:28:23 -0500 Subject: [PATCH 030/239] Converted get_node_info into a Command object. --- iota/adapter.py | 2 +- iota/api.py | 131 ++++++++++++++++++--------- iota/commands/__init__.py | 3 + iota/commands/get_node_info.py | 23 +++++ test/__init__.py | 19 ++++ test/api_test.py | 133 ++++++++++------------------ test/commands/__init__.py | 3 + test/commands/get_node_info_test.py | 58 ++++++++++++ 8 files changed, 242 insertions(+), 130 deletions(-) create mode 100644 iota/commands/__init__.py create mode 100644 iota/commands/get_node_info.py create mode 100644 test/commands/__init__.py create mode 100644 test/commands/get_node_info_test.py diff --git a/iota/adapter.py b/iota/adapter.py index cf6dfab..41ea87f 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -32,7 +32,7 @@ class InvalidUri(ValueError): pass -adapter_registry = {} # type: Dict[Text, BaseAdapter] +adapter_registry = {} # type: Dict[Text, AdapterMeta] """Keeps track of available adapters and their supported protocols.""" diff --git a/iota/api.py b/iota/api.py index a7dafc0..aacb1eb 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,7 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Callable, Iterable, Optional, Text, Union +from abc import ABCMeta +from inspect import isabstract as is_abstract +from typing import Dict, Iterable, Optional, Text, Union + +from six import with_metaclass from iota.adapter import BaseAdapter, resolve_adapter from iota.types import TransactionId, TryteString @@ -12,6 +16,82 @@ ] +command_registry = {} # type: Dict[Text, CommandMeta] +"""Registry of commands, indexed by command name.""" + +class CommandMeta(ABCMeta): + """Automatically register new commands.""" + # noinspection PyShadowingBuiltins + def __init__(cls, what, bases=None, dict=None): + super(CommandMeta, cls).__init__(what, bases, dict) + + if not is_abstract(cls): + command = getattr(cls, 'command') + if command: + command_registry[command] = cls + + +class BaseCommand(with_metaclass(CommandMeta)): + """An API command ready to send to the node.""" + command = None # Text + + def __init__(self, adapter): + # type: (BaseAdapter) -> None + self.adapter = adapter + self.response = None # type: dict + + def __call__(self, **kwargs): + # type: (dict) -> dict + """Sends the command to the node.""" + if self.called: + raise ValueError('Command has already been called.') + + self.response = self.adapter.send_request(self._prepare_request(kwargs)) + + replacement = self._prepare_response(self.response) + if replacement is not None: + self.response = replacement + + return self.response + + @property + def called(self): + # type: () -> bool + """Returns whether this command has been called.""" + return self.response is not None + + def _prepare_request(self, params): + # type: (dict) -> dict + """Returns the actual payload to be sent to the node.""" + payload = {'command': self.command} + payload.update(params) + return payload + + def _prepare_response(self, response): + # type: (dict) -> Optional[dict] + """ + Modifies the response from the node. + + If this method returns a dict, it will replace the response + entirely. + """ + pass + + +class CustomCommand(BaseCommand): + """Used to execute experimental/undocumented commands.""" + def __init__(self, adapter, command): + # type: (BaseAdapter, Text) -> None + super(CustomCommand, self).__init__(adapter) + + self.command = command + + +# Populate the command registry. +# noinspection PyUnresolvedReferences +from iota.commands import * + + class IotaApi(object): """ API to send HTTP requests for communicating with an IOTA node. @@ -31,7 +111,7 @@ def __init__(self, adapter): self.adapter = adapter # type: BaseAdapter def __getattr__(self, command): - # type: (Text, dict) -> Command + # type: (Text, dict) -> CustomCommand """ Sends an arbitrary API command to the node. @@ -40,7 +120,10 @@ def __getattr__(self, command): :param command: The name of the command to send. """ - return Command(self.adapter, command) + try: + return command_registry[command](self.adapter) + except KeyError: + return CustomCommand(self.adapter, command) def add_neighbors(self, uris): # type: (Iterable[Text]) -> dict @@ -234,14 +317,7 @@ def get_node_info(self): :see: https://iota.readme.io/docs/getnodeinfo """ - response = self.getNodeInfo() - - for key in ('latestMilestone', 'latestSolidSubtangleMilestone'): - trytes = response.get(key) - if trytes: - response[key] = TryteString(trytes.encode('ascii')) - - return response + return self.getNodeInfo() def get_tips(self): # type: () -> dict @@ -313,36 +389,3 @@ def store_transactions(self, trytes): :see: https://iota.readme.io/docs/storetransactions """ raise NotImplementedError('Not implemented yet.') - - -class Command(object): - """An API command ready to send to the node.""" - def __init__(self, adapter, command): - # type: (BaseAdapter, Text) -> None - super(Command, self).__init__() - - self.adapter = adapter - self.command = command - self.response = None # type: dict - - def __call__(self, **kwargs): - # type: (dict) -> dict - """Sends the command to the node.""" - if self.called: - raise ValueError('Command has already been called.') - - self.response = self.adapter.send_request(self.create_payload(kwargs)) - return self.response - - @property - def called(self): - # type: () -> bool - """Returns whether this command has been called.""" - return self.response is not None - - def create_payload(self, params): - # type: (dict) -> dict - """Returns the actual payload to be sent to the node.""" - payload = {'command': self.command} - payload.update(params) - return payload diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py new file mode 100644 index 0000000..3f3d02d --- /dev/null +++ b/iota/commands/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py new file mode 100644 index 0000000..c5064a6 --- /dev/null +++ b/iota/commands/get_node_info.py @@ -0,0 +1,23 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.api import BaseCommand +from iota.types import TryteString + + +class GetNodeInfoCommand(BaseCommand): + """ + Executes `getNodeInfo` command. + + :see: iota.IotaApi.get_node_info + """ + command = 'getNodeInfo' + + def _prepare_response(self, response): + for key in ('latestMilestone', 'latestSolidSubtangleMilestone'): + trytes = response.get(key) + if trytes: + response[key] = TryteString(trytes.encode('ascii')) + + diff --git a/test/__init__.py b/test/__init__.py index 3f3d02d..e6f718d 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -1,3 +1,22 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ unicode_literals + +from iota.adapter import BaseAdapter + + +class MockAdapter(BaseAdapter): + """An adapter for IotaApi that always returns a mocked response.""" + supported_protocols = ('mock',) + + def __init__(self, response=None): + # type: (Optional[dict]) -> None + super(MockAdapter, self).__init__() + + self.response = response + + self.requests = [] + + def send_request(self, payload, **kwargs): + self.requests.append((payload, kwargs)) + return self.response diff --git a/test/api_test.py b/test/api_test.py index 8826bef..5186b21 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -2,72 +2,83 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Optional from unittest import TestCase from iota import IotaApi -from iota.adapter import BaseAdapter +from iota.api import CustomCommand +from iota.commands.get_node_info import GetNodeInfoCommand from iota.types import TryteString, TransactionId +from test import MockAdapter -class MockAdapter(BaseAdapter): - """An adapter for IotaApi that always returns a mocked response.""" - supported_protocols = ('mock',) - - def __init__(self, response=None): - # type: (Optional[dict]) -> None - super(MockAdapter, self).__init__() - - self.response = response - - self.requests = [] - - def send_request(self, payload, **kwargs): - self.requests.append((payload, kwargs)) - return self.response - +class CustomCommandTestCase(TestCase): + def setUp(self): + super(CustomCommandTestCase, self).setUp() -class IotaApiTestCase(TestCase): - def test_init_with_uri(self): - """ - Passing a URI to the initializer instead of an adapter instance. - """ - api = IotaApi('mock://') - self.assertIsInstance(api.adapter, MockAdapter) + self.name = 'helloWorld' + self.adapter = MockAdapter() + self.command = CustomCommand(self.adapter, self.name) - def test_custom_command(self): - """Sending an experimental/unsupported command.""" + def test_call(self): + """Sending a custom command.""" expected_response = {'message': 'Hello, IOTA!'} - adapter = MockAdapter(expected_response) - api = IotaApi(adapter) + self.adapter.response = expected_response - response = api.helloWorld() + response = self.command() self.assertEqual(response, expected_response) + self.assertTrue(self.command.called) self.assertListEqual( - adapter.requests, + self.adapter.requests, [({'command': 'helloWorld'}, {})], ) - def test_custom_command_with_arguments(self): - """Sending an experimental/unsupported command with arguments.""" + def test_call_with_parameters(self): + """Sending a custom command with parameters.""" expected_response = {'message': 'Hello, IOTA!'} - adapter = MockAdapter(expected_response) - api = IotaApi(adapter) + self.adapter.response = expected_response - response = api.helloWorld(foo='bar', baz='luhrmann') + response = self.command(foo='bar', baz='luhrmann') self.assertEqual(response, expected_response) + self.assertTrue(self.command.called) self.assertListEqual( - adapter.requests, + self.adapter.requests, [({'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}, {})], ) +class IotaApiTestCase(TestCase): + def test_init_with_uri(self): + """ + Passing a URI to the initializer instead of an adapter instance. + """ + api = IotaApi('mock://') + self.assertIsInstance(api.adapter, MockAdapter) + + def test_registered_command(self): + """Preparing a documented command.""" + api = IotaApi(MockAdapter()) + + # We just need to make sure the correct command type is + # instantiated; individual commands have their own unit tests. + command = api.getNodeInfo + self.assertIsInstance(command, GetNodeInfoCommand) + + def test_custom_command(self): + """Preparing an experimental/undocumented command.""" + api = IotaApi(MockAdapter()) + + # We just need to make sure the correct command type is + # instantiated; custom commands have their own unit tests. + command = api.helloWorld + self.assertIsInstance(command, CustomCommand) + + # noinspection SpellCheckingInspection class AttachToTangleTestCase(TestCase): def setUp(self): @@ -355,51 +366,3 @@ def test_error_trytes_invalid(self): trunk_transaction = trunk_transaction, branch_transaction = branch_transaction, ) - - -# noinspection SpellCheckingInspection -class GetNodeInfoTestCase(TestCase): - def setUp(self): - super(GetNodeInfoTestCase, self).setUp() - - self.adapter = MockAdapter() - self.api = IotaApi(self.adapter) - - def test_get_node_info_happy_path(self): - """Successful invocation of `getNodeInfo`.""" - expected_response = { - 'appName': 'IRI', - 'appVersion': '1.0.8.nu', - 'duration': 1, - 'jreAvailableProcessors': 4, - 'jreFreeMemory': 91707424, - 'jreMaxMemory': 1908932608, - 'jreTotalMemory': 122683392, - 'latestMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', - 'latestMilestoneIndex': 107, - 'latestSolidSubtangleMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', - 'latestSolidSubtangleMilestoneIndex': 107, - 'neighbors': 2, - 'packetsQueueSize': 0, - 'time': 1477037811737, - 'tips': 3, - 'transactionsToRequest': 0 - } - - self.adapter.response = expected_response - - response = self.api.get_node_info() - - self.assertDictEqual(response, expected_response) - - self.assertIsInstance(response['latestMilestone'], TryteString) - - self.assertIsInstance( - response['latestSolidSubtangleMilestone'], - TryteString, - ) - - self.assertListEqual( - self.adapter.requests, - [({'command': 'getNodeInfo'}, {})], - ) diff --git a/test/commands/__init__.py b/test/commands/__init__.py new file mode 100644 index 0000000..3f3d02d --- /dev/null +++ b/test/commands/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/test/commands/get_node_info_test.py b/test/commands/get_node_info_test.py new file mode 100644 index 0000000..0ccea2e --- /dev/null +++ b/test/commands/get_node_info_test.py @@ -0,0 +1,58 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + + +# noinspection SpellCheckingInspection +from unittest import TestCase + +from iota.commands.get_node_info import GetNodeInfoCommand +from iota.types import TryteString +from test import MockAdapter + + +class GetNodeInfoTestCase(TestCase): + def setUp(self): + super(GetNodeInfoTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = GetNodeInfoCommand(self.adapter) + + def test_happy_path(self): + """Successful invocation of `getNodeInfo`.""" + expected_response = { + 'appName': 'IRI', + 'appVersion': '1.0.8.nu', + 'duration': 1, + 'jreAvailableProcessors': 4, + 'jreFreeMemory': 91707424, + 'jreMaxMemory': 1908932608, + 'jreTotalMemory': 122683392, + 'latestMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', + 'latestMilestoneIndex': 107, + 'latestSolidSubtangleMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', + 'latestSolidSubtangleMilestoneIndex': 107, + 'neighbors': 2, + 'packetsQueueSize': 0, + 'time': 1477037811737, + 'tips': 3, + 'transactionsToRequest': 0 + } + + self.adapter.response = expected_response + + response = self.command() + + self.assertDictEqual(response, expected_response) + + self.assertIsInstance(response['latestMilestone'], TryteString) + + self.assertIsInstance( + response['latestSolidSubtangleMilestone'], + TryteString, + ) + + self.assertListEqual( + self.adapter.requests, + [({'command': 'getNodeInfo'}, {})], + ) From d024a6ed9c5aa9c33c5e9b8396a7db1767474aa6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 21:53:15 -0500 Subject: [PATCH 031/239] Converted `attachToTangle` into Command. --- iota/api.py | 141 ++++++------ iota/commands/attach_to_tangle.py | 87 ++++++++ iota/commands/get_node_info.py | 12 +- test/api_test.py | 289 ------------------------ test/commands/attach_to_tangle_test.py | 298 +++++++++++++++++++++++++ test/commands/get_node_info_test.py | 7 +- 6 files changed, 462 insertions(+), 372 deletions(-) create mode 100644 iota/commands/attach_to_tangle.py create mode 100644 test/commands/attach_to_tangle_test.py diff --git a/iota/api.py b/iota/api.py index aacb1eb..82ec4f5 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,9 +2,9 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from abc import ABCMeta +from abc import ABCMeta, abstractmethod as abstract_method from inspect import isabstract as is_abstract -from typing import Dict, Iterable, Optional, Text, Union +from typing import Any, Callable, Dict, Iterable, Optional, Text, Union from six import with_metaclass @@ -37,7 +37,8 @@ class BaseCommand(with_metaclass(CommandMeta)): def __init__(self, adapter): # type: (BaseAdapter) -> None - self.adapter = adapter + self.adapter = adapter + self.request = None # type: dict self.response = None # type: dict def __call__(self, **kwargs): @@ -46,7 +47,15 @@ def __call__(self, **kwargs): if self.called: raise ValueError('Command has already been called.') - self.response = self.adapter.send_request(self._prepare_request(kwargs)) + self.request = kwargs + + replacement = self._prepare_request(self.request) + if replacement is not None: + self.request = replacement + + self.request['command'] = self.command + + self.response = self.adapter.send_request(self.request) replacement = self._prepare_response(self.response) if replacement is not None: @@ -60,13 +69,23 @@ def called(self): """Returns whether this command has been called.""" return self.response is not None - def _prepare_request(self, params): - # type: (dict) -> dict - """Returns the actual payload to be sent to the node.""" - payload = {'command': self.command} - payload.update(params) - return payload + @abstract_method + def _prepare_request(self, request): + # type: (dict) -> Optional[dict] + """ + Modifies the request before sending it to the node. + If this method returns a dict, it will replace the request + entirely. + + Note: the `command` parameter will be injected later; it is + not necessary for this method to include it. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + @abstract_method def _prepare_response(self, response): # type: (dict) -> Optional[dict] """ @@ -75,7 +94,33 @@ def _prepare_response(self, response): If this method returns a dict, it will replace the response entirely. """ - pass + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + @staticmethod + def _convert_response_values(response, keys, converter): + # type: (dict, Iterable[Text], Callable[Any, Any]) -> None + """ + Converts non-null response values at the specified keys to the + specified type. + """ + for k in keys: + value = response.get(k) + if value is not None: + response[k] = converter(value) + + def _convert_to_tryte_strings(self, response, keys, type_=TryteString): + # type: (dict, Iterable[Text], type) -> None + """ + Converts non-null response values at the specified keys to + TryteStrings. + """ + def converter(value): + # type: (Text) -> TryteString + return type_(value.encode('ascii')) + + self._convert_response_values(response, keys, converter) class CustomCommand(BaseCommand): @@ -86,6 +131,12 @@ def __init__(self, adapter, command): self.command = command + def _prepare_request(self, request): + pass + + def _prepare_response(self, response): + pass + # Populate the command registry. # noinspection PyUnresolvedReferences @@ -158,71 +209,13 @@ def attach_to_tangle( :see: https://iota.readme.io/docs/attachtotangle """ - if not isinstance(trunk_transaction, TransactionId): - raise TypeError( - 'trunk_transaction has wrong type ' - '(expected TransactionID, actual {type}).'.format( - type = type(trunk_transaction).__name__, - ), - ) - - if not isinstance(branch_transaction, TransactionId): - raise TypeError( - 'branch_transaction has wrong type ' - '(expected TransactionID, actual {type}).'.format( - type = type(branch_transaction).__name__, - ), - ) - - if type(min_weight_magnitude) is not int: - raise TypeError( - 'min_weight_magnitude has wrong type ' - '(expected int, actual {type}).'.format( - type = type(min_weight_magnitude).__name__, - ), - ) - - if min_weight_magnitude < 18: - raise ValueError( - 'min_weight_magnitude is too small ' - '(expected >= 18, actual {value}).'.format( - value = min_weight_magnitude, - ), - ) - - if not isinstance(trytes, Iterable): - raise TypeError( - 'trytes has wrong type (expected Iterable, actual {type}).'.format( - type = type(trytes).__name__, - ), - ) - - if not trytes: - raise ValueError('trytes must not be empty.') - - for i, t in enumerate(trytes): - if not isinstance(t, TryteString): - raise TypeError( - 'trytes[{i}] has wrong type ' - '(expected TryteString, actual {type}).'.format( - i = i, - type = type(t).__name__, - ), - ) - - response = self.attachToTangle( - trunkTransaction = trunk_transaction.trytes, - branchTransaction = branch_transaction.trytes, - minWeightMagnitude = min_weight_magnitude, - trytes = [t.trytes for t in trytes], + return self.attachToTangle( + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + min_weight_magnitude = min_weight_magnitude, + trytes = trytes, ) - trytes = response.get('trytes') - if trytes: - response['trytes'] = [TryteString(t.encode('ascii')) for t in trytes] - - return response - def broadcast_transactions(self, trytes): # type: (Iterable[Text]) -> dict """ diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py new file mode 100644 index 0000000..22be483 --- /dev/null +++ b/iota/commands/attach_to_tangle.py @@ -0,0 +1,87 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from collections import Iterable + +from iota.api import BaseCommand +from iota.types import TransactionId, TryteString + + +class AttachToTangleCommand(BaseCommand): + """ + Executes `attachToTangle` command. + + :see: iota.IotaApi.attach_to_tangle + """ + command = 'attachToTangle' + + def _prepare_request(self, params): + trunk_transaction = params.get('trunk_transaction') + branch_transaction = params.get('branch_transaction') + min_weight_magnitude = params.get('min_weight_magnitude', 18) + trytes = params.get('trytes') + + if not isinstance(trunk_transaction, TransactionId): + raise TypeError( + 'trunk_transaction has wrong type ' + '(expected TransactionID, actual {type}).'.format( + type = type(trunk_transaction).__name__, + ), + ) + + if not isinstance(branch_transaction, TransactionId): + raise TypeError( + 'branch_transaction has wrong type ' + '(expected TransactionID, actual {type}).'.format( + type = type(branch_transaction).__name__, + ), + ) + + if type(min_weight_magnitude) is not int: + raise TypeError( + 'min_weight_magnitude has wrong type ' + '(expected int, actual {type}).'.format( + type = type(min_weight_magnitude).__name__, + ), + ) + + if min_weight_magnitude < 18: + raise ValueError( + 'min_weight_magnitude is too small ' + '(expected >= 18, actual {value}).'.format( + value = min_weight_magnitude, + ), + ) + + if not isinstance(trytes, Iterable): + raise TypeError( + 'trytes has wrong type (expected Iterable, actual {type}).'.format( + type = type(trytes).__name__, + ), + ) + + if not trytes: + raise ValueError('trytes must not be empty.') + + for i, t in enumerate(trytes): + if not isinstance(t, TryteString): + raise TypeError( + 'trytes[{i}] has wrong type ' + '(expected TryteString, actual {type}).'.format( + i = i, + type = type(t).__name__, + ), + ) + + return { + 'trunkTransaction': trunk_transaction.trytes, + 'branchTransaction': branch_transaction.trytes, + 'minWeightMagnitude': min_weight_magnitude, + 'trytes': [t.trytes for t in trytes], + } + + def _prepare_response(self, response): + trytes = response.get('trytes') + if trytes: + response['trytes'] = [TryteString(t.encode('ascii')) for t in trytes] diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index c5064a6..6aeaefa 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -3,7 +3,6 @@ unicode_literals from iota.api import BaseCommand -from iota.types import TryteString class GetNodeInfoCommand(BaseCommand): @@ -14,10 +13,13 @@ class GetNodeInfoCommand(BaseCommand): """ command = 'getNodeInfo' + def _prepare_request(self, request): + pass + def _prepare_response(self, response): - for key in ('latestMilestone', 'latestSolidSubtangleMilestone'): - trytes = response.get(key) - if trytes: - response[key] = TryteString(trytes.encode('ascii')) + self._convert_to_tryte_strings( + response = response, + keys = ('latestMilestone', 'latestSolidSubtangleMilestone'), + ) diff --git a/test/api_test.py b/test/api_test.py index 5186b21..62efec4 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -77,292 +77,3 @@ def test_custom_command(self): # instantiated; custom commands have their own unit tests. command = api.helloWorld self.assertIsInstance(command, CustomCommand) - - -# noinspection SpellCheckingInspection -class AttachToTangleTestCase(TestCase): - def setUp(self): - super(AttachToTangleTestCase, self).setUp() - - self.adapter = MockAdapter() - self.api = IotaApi(self.adapter) - - def test_happy_path(self): - """Successful invocation of `attachToTangle`.""" - expected_response = { - 'trytes':['TRYTEVALUEHERE'] - } - - self.adapter.response = expected_response - - trunk_transaction =\ - TransactionId( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ) - - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - min_weight_magnitude = 20 - trytes = [TryteString(b'TRYTVALUEHERE')] - - response = self.api.attach_to_tangle( - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - min_weight_magnitude = min_weight_magnitude, - trytes = trytes, - ) - - self.assertDictEqual(response, expected_response) - - self.assertListEqual( - list(map(type, response['trytes'])), - [TryteString], - ) - - self.assertListEqual( - self.adapter.requests, - [( - { - 'command': 'attachToTangle', - 'trunkTransaction': trunk_transaction.trytes, - 'branchTransaction': branch_transaction.trytes, - 'minWeightMagnitude': min_weight_magnitude, - 'trytes': [trytes[0].trytes], - }, - - {}, - )] - ) - - # noinspection PyTypeChecker - def test_error_trunk_transaction_invalid(self): - """ - Attempting to call `attachToTangle`, but the `trunkTransaction` - parameter is not valid. - """ - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - trytes = [TryteString(b'TRYTEVALUEHERE')] - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Nope; the trytes have to be in a container. - trunk_transaction = - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Sorry, not good enough; it's gotta be a TransactionId. - trunk_transaction = - TryteString( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ), - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Now you're not making any sense. - trunk_transaction = None, - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Are you even listening to me? - trunk_transaction = 42, - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - # noinspection PyTypeChecker - def test_error_branch_transaction_invalid(self): - """ - Attempting to call `attachToTangle`, but the `branchTransaction` - parameter is not valid. - """ - trunk_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - trytes = [TryteString(b'TRYTEVALUEHERE')] - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Nope; the trytes have to be in a container. - branch_transaction = - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Sorry, not good enough; it's gotta be a TransactionId. - branch_transaction = - TryteString( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ), - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Now you're not making any sense. - branch_transaction = None, - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Are you even listening to me? - branch_transaction = 42, - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - # noinspection PyTypeChecker - def test_error_min_weight_magnitude_invalid(self): - """ - Attempting to call `attachToTangle`, but the `minWeightMagnitude` - parameter is not valid. - """ - trunk_transaction =\ - TransactionId( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ) - - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - trytes = [TryteString(b'TRYTVALUEHERE')] - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Nice try, but it's gotta be an int. - min_weight_magnitude = 18.0, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # Oh, come on. You know what I meant! - min_weight_magnitude = True, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # I swear you're doing this on purpose just to annoy me. - min_weight_magnitude = 'eighteen', - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # This parameter is required. - min_weight_magnitude = None, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(ValueError): - self.api.attach_to_tangle( - # Minimum value for this parameter is 18. - min_weight_magnitude = 17, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - # noinspection PyTypeChecker - def test_error_trytes_invalid(self): - """ - Attempting to call `attachToTangle`, but the `trytes` parameter is - not valid. - """ - trunk_transaction =\ - TransactionId( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ) - - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # It's gotta be a list. - trytes = TryteString(b'TRYTEVALUEHERE'), - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - ) - - with self.assertRaises(ValueError): - self.api.attach_to_tangle( - # Ok, you got the list part down, but you have to put something - # inside it. - trytes = [], - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - ) - - with self.assertRaises(TypeError): - self.api.attach_to_tangle( - # No, no, no! They all have to be TryteStrings! - trytes = [TryteString(b'TRYTEVALUEHERE'), b'QUACK'], - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - ) diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py new file mode 100644 index 0000000..2b07c04 --- /dev/null +++ b/test/commands/attach_to_tangle_test.py @@ -0,0 +1,298 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota.commands.attach_to_tangle import AttachToTangleCommand +from iota.types import TransactionId, TryteString +from test import MockAdapter + + +# noinspection SpellCheckingInspection +class AttachToTangleCommandTestCase(TestCase): + def setUp(self): + super(AttachToTangleCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = AttachToTangleCommand(self.adapter) + + def test_happy_path(self): + """Successful invocation of `attachToTangle`.""" + expected_response = { + 'trytes':['TRYTEVALUEHERE'] + } + + self.adapter.response = expected_response + + trunk_transaction =\ + TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + min_weight_magnitude = 20 + trytes = [TryteString(b'TRYTVALUEHERE')] + + response = self.command( + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + min_weight_magnitude = min_weight_magnitude, + trytes = trytes, + ) + + self.assertDictEqual(response, expected_response) + + self.assertListEqual( + list(map(type, response['trytes'])), + [TryteString], + ) + + self.assertListEqual( + self.adapter.requests, + [( + { + 'command': 'attachToTangle', + 'trunkTransaction': trunk_transaction.trytes, + 'branchTransaction': branch_transaction.trytes, + 'minWeightMagnitude': min_weight_magnitude, + 'trytes': [trytes[0].trytes], + }, + + {}, + )] + ) + + # noinspection PyTypeChecker + def test_error_trunk_transaction_invalid(self): + """ + Attempting to call `attachToTangle`, but the `trunkTransaction` + parameter is not valid. + """ + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + trytes = [TryteString(b'TRYTEVALUEHERE')] + + with self.assertRaises(TypeError): + self.command( + # Nope; the trytes have to be in a container. + trunk_transaction = + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # Sorry, not good enough; it's gotta be a TransactionId. + trunk_transaction = + TryteString( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ), + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # Now you're not making any sense. + trunk_transaction = None, + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # Are you even listening to me? + trunk_transaction = 42, + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + # noinspection PyTypeChecker + def test_error_branch_transaction_invalid(self): + """ + Attempting to call `attachToTangle`, but the `branchTransaction` + parameter is not valid. + """ + trunk_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + trytes = [TryteString(b'TRYTEVALUEHERE')] + + with self.assertRaises(TypeError): + self.command( + # Nope; the trytes have to be in a container. + branch_transaction = + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # Sorry, not good enough; it's gotta be a TransactionId. + branch_transaction = + TryteString( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ), + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # Now you're not making any sense. + branch_transaction = None, + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # Are you even listening to me? + branch_transaction = 42, + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + # noinspection PyTypeChecker + def test_error_min_weight_magnitude_invalid(self): + """ + Attempting to call `attachToTangle`, but the `minWeightMagnitude` + parameter is not valid. + """ + trunk_transaction =\ + TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + trytes = [TryteString(b'TRYTVALUEHERE')] + + with self.assertRaises(TypeError): + self.command( + # Nice try, but it's gotta be an int. + min_weight_magnitude = 18.0, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # Oh, come on. You know what I meant! + min_weight_magnitude = True, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # I swear you're doing this on purpose just to annoy me. + min_weight_magnitude = 'eighteen', + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(TypeError): + self.command( + # This parameter is required. + min_weight_magnitude = None, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(ValueError): + self.command( + # Minimum value for this parameter is 18. + min_weight_magnitude = 17, + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + trytes = trytes, + ) + + # noinspection PyTypeChecker + def test_error_trytes_invalid(self): + """ + Attempting to call `attachToTangle`, but the `trytes` parameter is + not valid. + """ + trunk_transaction =\ + TransactionId( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + branch_transaction =\ + TransactionId( + b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' + b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' + ) + + with self.assertRaises(TypeError): + self.command( + # It's gotta be a list. + trytes = TryteString(b'TRYTEVALUEHERE'), + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + ) + + with self.assertRaises(ValueError): + self.command( + # Ok, you got the list part down, but you have to put something + # inside it. + trytes = [], + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + ) + + with self.assertRaises(TypeError): + self.command( + # No, no, no! They all have to be TryteStrings! + trytes = [TryteString(b'TRYTEVALUEHERE'), b'QUACK'], + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + ) diff --git a/test/commands/get_node_info_test.py b/test/commands/get_node_info_test.py index 0ccea2e..e575134 100644 --- a/test/commands/get_node_info_test.py +++ b/test/commands/get_node_info_test.py @@ -2,8 +2,6 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals - -# noinspection SpellCheckingInspection from unittest import TestCase from iota.commands.get_node_info import GetNodeInfoCommand @@ -11,9 +9,10 @@ from test import MockAdapter -class GetNodeInfoTestCase(TestCase): +# noinspection SpellCheckingInspection +class GetNodeInfoCommandTestCase(TestCase): def setUp(self): - super(GetNodeInfoTestCase, self).setUp() + super(GetNodeInfoCommandTestCase, self).setUp() self.adapter = MockAdapter() self.command = GetNodeInfoCommand(self.adapter) From 4c42f58e4a7e7bab06c7768dd9c7245195dfdc8d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 21:57:17 -0500 Subject: [PATCH 032/239] Better exception type for Command already called. --- iota/api.py | 12 +++++------- test/api_test.py | 10 +++++++++- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/iota/api.py b/iota/api.py index 82ec4f5..35d4bb9 100644 --- a/iota/api.py +++ b/iota/api.py @@ -38,6 +38,8 @@ class BaseCommand(with_metaclass(CommandMeta)): def __init__(self, adapter): # type: (BaseAdapter) -> None self.adapter = adapter + + self.called = False self.request = None # type: dict self.response = None # type: dict @@ -45,7 +47,7 @@ def __call__(self, **kwargs): # type: (dict) -> dict """Sends the command to the node.""" if self.called: - raise ValueError('Command has already been called.') + raise RuntimeError('Command has already been called.') self.request = kwargs @@ -61,13 +63,9 @@ def __call__(self, **kwargs): if replacement is not None: self.response = replacement - return self.response + self.called = True - @property - def called(self): - # type: () -> bool - """Returns whether this command has been called.""" - return self.response is not None + return self.response @abstract_method def _prepare_request(self, request): diff --git a/test/api_test.py b/test/api_test.py index 62efec4..665fb33 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -7,7 +7,6 @@ from iota import IotaApi from iota.api import CustomCommand from iota.commands.get_node_info import GetNodeInfoCommand -from iota.types import TryteString, TransactionId from test import MockAdapter @@ -51,6 +50,15 @@ def test_call_with_parameters(self): [({'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}, {})], ) + def test_call_error_already_called(self): + """A command can only be called once.""" + self.command() + + with self.assertRaises(RuntimeError): + self.command(extra='params') + + self.assertDictEqual(self.command.request, {'command': 'helloWorld'}) + class IotaApiTestCase(TestCase): def test_init_with_uri(self): From ce1d257e343d7e5eda18fa170aeb314e02f6ad3e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 30 Nov 2016 22:00:37 -0500 Subject: [PATCH 033/239] Minor cleanup. --- iota/commands/attach_to_tangle.py | 4 ++++ iota/commands/get_node_info.py | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index 22be483..88eef01 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -7,6 +7,10 @@ from iota.api import BaseCommand from iota.types import TransactionId, TryteString +__all__ = [ + 'AttachToTangleCommand', +] + class AttachToTangleCommand(BaseCommand): """ diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index 6aeaefa..35f19de 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -4,6 +4,10 @@ from iota.api import BaseCommand +__all__ = [ + 'GetNodeInfoCommand', +] + class GetNodeInfoCommand(BaseCommand): """ From 3f7c70f310c9ed1aeaf543f3a20dfab263c4f5a0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 1 Dec 2016 19:27:22 -0500 Subject: [PATCH 034/239] Improved DX (developer experience). - bytes() now yields the trytes instead of doing a byte conversion (use `as_bytes` to convert). - TryteStrings now have length and are iterable. - `attachToTangle` (and all future Commands) parameters can have any type so long as they can be converted to the correct type. - Made code a little more DRY. --- iota/api.py | 9 +- iota/codecs.py | 7 +- iota/commands/attach_to_tangle.py | 54 ++++------- iota/types.py | 54 +++++++---- test/__init__.py | 5 +- test/commands/attach_to_tangle_test.py | 126 +++++++++++++------------ test/types_test.py | 91 ++++++++++-------- 7 files changed, 190 insertions(+), 156 deletions(-) diff --git a/iota/api.py b/iota/api.py index 35d4bb9..04cd96c 100644 --- a/iota/api.py +++ b/iota/api.py @@ -6,7 +6,7 @@ from inspect import isabstract as is_abstract from typing import Any, Callable, Dict, Iterable, Optional, Text, Union -from six import with_metaclass +from six import text_type as text, with_metaclass from iota.adapter import BaseAdapter, resolve_adapter from iota.types import TransactionId, TryteString @@ -115,8 +115,11 @@ def _convert_to_tryte_strings(self, response, keys, type_=TryteString): TryteStrings. """ def converter(value): - # type: (Text) -> TryteString - return type_(value.encode('ascii')) + if isinstance(value, text): + return type_(value.encode('ascii')) + + elif isinstance(value, Iterable): + return list(map(converter, value)) self._convert_response_values(response, keys, converter) diff --git a/iota/codecs.py b/iota/codecs.py index e1c59b2..472cf06 100644 --- a/iota/codecs.py +++ b/iota/codecs.py @@ -24,8 +24,11 @@ class TrytesCodec(Codec): # :bc: Without the bytearray cast, Python 2 will populate the dict # with characters instead of integers. # noinspection SpellCheckingInspection - alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) - index = dict(zip(alphabet.values(), alphabet.keys())) + alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) + """Used to encode bytes into trytes.""" + + index = dict(zip(alphabet.values(), alphabet.keys())) + """Used to decode trytes into bytes.""" @classmethod def get_codec_info(cls): diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index 88eef01..47320d6 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -2,7 +2,9 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from collections import Iterable +from typing import Generator, Sequence + +from six import binary_type from iota.api import BaseCommand from iota.types import TransactionId, TryteString @@ -21,26 +23,13 @@ class AttachToTangleCommand(BaseCommand): command = 'attachToTangle' def _prepare_request(self, params): - trunk_transaction = params.get('trunk_transaction') - branch_transaction = params.get('branch_transaction') - min_weight_magnitude = params.get('min_weight_magnitude', 18) - trytes = params.get('trytes') - - if not isinstance(trunk_transaction, TransactionId): - raise TypeError( - 'trunk_transaction has wrong type ' - '(expected TransactionID, actual {type}).'.format( - type = type(trunk_transaction).__name__, - ), - ) + # Required parameters. + trunk_transaction = params['trunk_transaction'] + branch_transaction = params['branch_transaction'] + trytes = params['trytes'] - if not isinstance(branch_transaction, TransactionId): - raise TypeError( - 'branch_transaction has wrong type ' - '(expected TransactionID, actual {type}).'.format( - type = type(branch_transaction).__name__, - ), - ) + # Optional parameters. + min_weight_magnitude = params.get('min_weight_magnitude', 18) if type(min_weight_magnitude) is not int: raise TypeError( @@ -58,7 +47,10 @@ def _prepare_request(self, params): ), ) - if not isinstance(trytes, Iterable): + # Technically, we only need `trytes` to be an Iterable, but some + # types (such as TryteString) are Iterable yet not acceptable + # here. + if not isinstance(trytes, (Sequence, Generator)): raise TypeError( 'trytes has wrong type (expected Iterable, actual {type}).'.format( type = type(trytes).__name__, @@ -68,24 +60,12 @@ def _prepare_request(self, params): if not trytes: raise ValueError('trytes must not be empty.') - for i, t in enumerate(trytes): - if not isinstance(t, TryteString): - raise TypeError( - 'trytes[{i}] has wrong type ' - '(expected TryteString, actual {type}).'.format( - i = i, - type = type(t).__name__, - ), - ) - return { - 'trunkTransaction': trunk_transaction.trytes, - 'branchTransaction': branch_transaction.trytes, + 'trunkTransaction': binary_type(TransactionId(trunk_transaction)), + 'branchTransaction': binary_type(TransactionId(branch_transaction)), 'minWeightMagnitude': min_weight_magnitude, - 'trytes': [t.trytes for t in trytes], + 'trytes': [binary_type(TryteString(t)) for t in trytes], } def _prepare_response(self, response): - trytes = response.get('trytes') - if trytes: - response['trytes'] = [TryteString(t.encode('ascii')) for t in trytes] + self._convert_to_tryte_strings(response, ('trytes',)) diff --git a/iota/types.py b/iota/types.py index 18a9cdd..6c1f526 100644 --- a/iota/types.py +++ b/iota/types.py @@ -3,7 +3,7 @@ unicode_literals from codecs import encode, decode -from typing import Text, Union +from typing import Generator, Text, Union from six import PY2, binary_type @@ -44,6 +44,16 @@ def __init__(self, trytes, pad=None): """ super(TryteString, self).__init__() + if isinstance(trytes, int): + # This is potentially a valid use case, and we might support it + # at some point. + raise TypeError( + 'Converting {type} to {cls} is not supported.'.format( + type = type(trytes).__name__, + cls = type(self).__name__, + ), + ) + if isinstance(trytes, TryteString): # Create a copy of the incoming TryteString's trytes, to ensure # we don't modify it when we apply padding. @@ -55,7 +65,7 @@ def __init__(self, trytes, pad=None): for i, ordinal in enumerate(trytes): if ordinal not in TrytesCodec.index: raise ValueError( - 'Invalid character {char} at position {i} ' + 'Invalid character {char!r} at position {i} ' '(expected A-Z or 9).'.format( char = chr(ordinal), i = i, @@ -72,22 +82,28 @@ def __repr__(self): return 'TryteString({trytes!r})'.format(trytes=binary_type(self.trytes)) def __bytes__(self): - # type: () -> Text + # type: () -> binary_type """ - Converts the TryteString into a byte string. + Converts the TryteString into a string representation. - If the value contains any trytes that can't be converted, they will - be replaced with '?'. - - If you want different handling of un-convertible trytes, use - `as_bytes` instead. + Note that this method will NOT convert the trytes back into bytes; + use `as_bytes` for that. """ - return self.as_bytes() + return binary_type(self.trytes) # :bc: Magic method has a different name in Python 2. if PY2: __str__ = __bytes__ + def __len__(self): + # type: () -> int + return len(self.trytes) + + def __iter__(self): + # type: () -> Generator[binary_type] + # :see: http://stackoverflow.com/a/14267935/ + return (self.trytes[i:i+1] for i in range(len(self))) + def as_bytes(self, errors='strict'): # type: (Text) -> binary_type """ @@ -102,17 +118,23 @@ def as_bytes(self, errors='strict'): return decode(self.trytes, 'trytes', errors) def __eq__(self, other): - # type: (TryteString) -> bool - if not isinstance(other, TryteString): + # type: (Union[TryteString, binary_type, bytearray]) -> bool + if isinstance(other, TryteString): + return self.trytes == other.trytes + elif isinstance(other, (binary_type, bytearray)): + return self.trytes == other + else: raise TypeError( - 'TryteStrings can only be compared to other TryteStrings.', + 'Invalid type for TryteString comparison ' + '(expected Union[TryteString, binary_type, bytearray], ' + 'actual {type}).'.format( + type = type(other).__name__, + ), ) - return self.trytes == other.trytes - # :bc: In Python 2 this must be defined explicitly. def __ne__(self, other): - # type: (TryteString) -> bool + # type: (Union[TryteString, binary_type, bytearray]) -> bool return not (self == other) diff --git a/test/__init__.py b/test/__init__.py index e6f718d..b834eff 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Optional + from iota.adapter import BaseAdapter @@ -13,8 +15,7 @@ def __init__(self, response=None): # type: (Optional[dict]) -> None super(MockAdapter, self).__init__() - self.response = response - + self.response = response or {} self.requests = [] def send_request(self, payload, **kwargs): diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 2b07c04..940ee3d 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -20,7 +20,7 @@ def setUp(self): def test_happy_path(self): """Successful invocation of `attachToTangle`.""" expected_response = { - 'trytes':['TRYTEVALUEHERE'] + 'trytes': ['TRYTEVALUEHERE'] } self.adapter.response = expected_response @@ -69,6 +69,71 @@ def test_happy_path(self): )] ) + def test_compatible_types(self): + """ + Calling `attachToTangle` with parameters that can be converted into + the correct types. + """ + self.command( + # Any value that can be converted into a TransactionId is valid + # here. + trunk_transaction =\ + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + + branch_transaction =\ + TryteString( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + ), + + # This still has to be an int, however. + min_weight_magnitude = 30, + + # Just to be extra tricky, let's see what happens if `trytes` is + # a generator. + trytes = ( + t for t in [ + # `trytes` can contain any value that can be converted into a + # TryteString. + b'TRYTEVALUEHERE', + + # This is probably wrong, but maybe not. + TransactionId(b'TRANSACTIONIDHERE'), + ] + ), + ) + + # Not interested in the response, but we should check to make sure + # that the incoming values were converted correctly. + request = self.adapter.requests[0][0] + + self.assertDictEqual( + request, + + { + 'command': 'attachToTangle', + 'minWeightMagnitude': 30, + + 'trunkTransaction': + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + , + + 'branchTransaction': + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + , + + 'trytes': [ + b'TRYTEVALUEHERE', + + b'TRANSACTIONIDHERE99999999999999999999999' + b'99999999999999999999999999999999999999999', + ], + }, + ) + # noinspection PyTypeChecker def test_error_trunk_transaction_invalid(self): """ @@ -83,30 +148,6 @@ def test_error_trunk_transaction_invalid(self): trytes = [TryteString(b'TRYTEVALUEHERE')] - with self.assertRaises(TypeError): - self.command( - # Nope; the trytes have to be in a container. - trunk_transaction = - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.command( - # Sorry, not good enough; it's gotta be a TransactionId. - trunk_transaction = - TryteString( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ), - - branch_transaction = branch_transaction, - trytes = trytes, - ) - with self.assertRaises(TypeError): self.command( # Now you're not making any sense. @@ -125,7 +166,7 @@ def test_error_trunk_transaction_invalid(self): trytes = trytes, ) - # noinspection PyTypeChecker + # noinspection PyTypeChecker,PyUnresolvedReferences def test_error_branch_transaction_invalid(self): """ Attempting to call `attachToTangle`, but the `branchTransaction` @@ -139,30 +180,6 @@ def test_error_branch_transaction_invalid(self): trytes = [TryteString(b'TRYTEVALUEHERE')] - with self.assertRaises(TypeError): - self.command( - # Nope; the trytes have to be in a container. - branch_transaction = - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - with self.assertRaises(TypeError): - self.command( - # Sorry, not good enough; it's gotta be a TransactionId. - branch_transaction = - TryteString( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ), - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - with self.assertRaises(TypeError): self.command( # Now you're not making any sense. @@ -287,12 +304,3 @@ def test_error_trytes_invalid(self): trunk_transaction = trunk_transaction, branch_transaction = branch_transaction, ) - - with self.assertRaises(TypeError): - self.command( - # No, no, no! They all have to be TryteStrings! - trytes = [TryteString(b'TRYTEVALUEHERE'), b'QUACK'], - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - ) diff --git a/test/types_test.py b/test/types_test.py index f160105..e7d5ff2 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -12,18 +12,13 @@ # noinspection SpellCheckingInspection class TryteStringTestCase(TestCase): - def test_hello_world(self): - """PoC test for TryteString""" + def test_from_bytes(self): + """Converting a sequence of bytes into a TryteString""" self.assertEqual( TryteString.from_bytes(b'Hello, IOTA!').trytes, b'RBTC9D9DCDQAEASBYBCCKBFA', ) - self.assertEqual( - binary_type(TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA')), - b'Hello, IOTA!', - ) - def test_equality_comparison(self): """Comparing TryteStrings for equality.""" trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') @@ -37,8 +32,25 @@ def test_equality_comparison(self): self.assertTrue(trytes1 != trytes3) self.assertTrue(trytes1 is trytes1) + self.assertFalse(trytes1 is not trytes1) + self.assertFalse(trytes1 is trytes2) + self.assertTrue(trytes1 is not trytes2) + self.assertFalse(trytes1 is trytes3) + self.assertTrue(trytes1 is not trytes3) + + # Comparing against byte strings is also allowed. + self.assertTrue(trytes1 == b'RBTC9D9DCDQAEASBYBCCKBFA') + self.assertFalse(trytes1 != b'RBTC9D9DCDQAEASBYBCCKBFA') + self.assertFalse(trytes3 == b'RBTC9D9DCDQAEASBYBCCKBFA') + self.assertTrue(trytes3 != b'RBTC9D9DCDQAEASBYBCCKBFA') + + # Ditto for bytearrays. + self.assertTrue(trytes1 == bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) + self.assertFalse(trytes1 != bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) + self.assertFalse(trytes3 == bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) + self.assertTrue(trytes3 != bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) # noinspection PyTypeChecker def test_equality_comparison_error_wrong_type(self): @@ -49,14 +61,17 @@ def test_equality_comparison_error_wrong_type(self): trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') with self.assertRaises(TypeError): - trytes == b'RBTC9D9DCDQAEASBYBCCKBFA' + # Comparing against unicode strings is not allowed because it is + # ambiguous how to encode the unicode string for comparison. + trytes == 'RBTC9D9DCDQAEASBYBCCKBFA' with self.assertRaises(TypeError): - trytes == bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA') + # We might support this at some point, but not at the moment. + trytes == 42 # Identity comparison still works though. - self.assertFalse(trytes is b'RBTC9D9DCDQAEASBYBCCKBFA') - self.assertFalse(trytes is bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) + self.assertFalse(trytes is 'RBTC9D9DCDQAEASBYBCCKBFA') + self.assertTrue(trytes is not 'RBTC9D9DCDQAEASBYBCCKBFA') def test_init_from_tryte_string(self): """Initializing a TryteString from another TryteString.""" @@ -106,37 +121,39 @@ def test_init_error_invalid_characters(self): with self.assertRaises(ValueError): TryteString(b'not valid') - def test_bytes_conversion_partial_sequence(self): - """ - Attempting to convert an odd number of trytes into bytes. - - Note: This behavior is undefined. Think trying to decode a - sequence of octets using UTF-16, and finding that there's an odd - number of octets. - """ - trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') + # noinspection PyTypeChecker + def test_init_error_int(self): + """Attempting to initialize a TryteString from an int.""" + with self.assertRaises(TypeError): + TryteString(42) + + def test_length(self): + """Just like byte strings, TryteStrings have length.""" + self.assertEqual(len(TryteString(b'RBTC')), 4) + self.assertEqual(len(TryteString(b'RBTC', pad=81)), 81) + + def test_iterator(self): + """Just like byte strings, you can iterate over TryteStrings.""" + self.assertListEqual( + list(TryteString(b'RBTC')), + [b'R', b'B', b'T', b'C'], + ) - with self.assertRaises(TrytesDecodeError): - binary_type(trytes) + self.assertListEqual( + list(TryteString(b'RBTC', pad=6)), + [b'R', b'B', b'T', b'C', b'9', b'9'], + ) - def test_bytes_conversion_non_ascii(self): + def test_string_conversion(self): """ - Converting a sequence of trytes into bytes yields non-ASCII - characters. - - This most likely indicates that the trytes didn't start out as - bytes. Think trying to decode a sequence of octets using UTF-8, - but the octets are actually JPEG data. + A TryteString can be converted into an ASCII representation. """ - trytes = TryteString( - b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' - b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', - ) + self.assertEqual( + binary_type(TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA')), - # This tryte sequence cannot be converted into a byte string; it - # contains too many ordinals > 255. - with self.assertRaises(TrytesDecodeError): - binary_type(trytes) + # Note that the trytes are NOT converted into bytes! + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) def test_as_bytes_partial_sequence_errors_strict(self): """ From ac46bb50bc68ad0213033468ee71c87be061f685 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 1 Dec 2016 19:36:56 -0500 Subject: [PATCH 035/239] Happy path impl for `addNeighbors`. --- iota/commands/add_neighbors.py | 20 +++++++++++++ test/commands/add_neighbors_test.py | 44 +++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 iota/commands/add_neighbors.py create mode 100644 test/commands/add_neighbors_test.py diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py new file mode 100644 index 0000000..1d0df20 --- /dev/null +++ b/iota/commands/add_neighbors.py @@ -0,0 +1,20 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.api import BaseCommand + + +class AddNeighborsCommand(BaseCommand): + """ + Executes `addNeighbors` command. + + :see: iota.IotaApi.add_neighbors + """ + command = 'addNeighbors' + + def _prepare_request(self, request): + pass + + def _prepare_response(self, response): + pass diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py new file mode 100644 index 0000000..b675801 --- /dev/null +++ b/test/commands/add_neighbors_test.py @@ -0,0 +1,44 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota.commands.add_neighbors import AddNeighborsCommand +from test import MockAdapter + + +class AddNeighborsCommandTestCase(TestCase): + def setUp(self): + super(AddNeighborsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = AddNeighborsCommand(self.adapter) + + def test_happy_path(self): + """Successful invocation of `addNeighbors`.""" + expected_response = { + 'addedNeighbors': 0, + 'duration': 2, + } + + self.adapter.response = expected_response + + neighbors = ['udp://node1.iotatoken.com:14265/', 'http://localhost:14265/'] + + response = self.command(uris=neighbors) + + self.assertDictEqual(response, expected_response) + + self.assertListEqual( + self.adapter.requests, + + [( + { + 'command': 'addNeighbors', + 'uris': neighbors, + }, + + {}, + )] + ) From df81a70a16e1b6faa13f35a27b2f387a64822972 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 1 Dec 2016 19:52:34 -0500 Subject: [PATCH 036/239] Added validation to `addNeighbors` command. --- iota/api.py | 4 ++-- iota/commands/add_neighbors.py | 32 ++++++++++++++++++++++++++++- iota/commands/attach_to_tangle.py | 11 +++++++--- test/commands/add_neighbors_test.py | 14 +++++++++++++ 4 files changed, 55 insertions(+), 6 deletions(-) diff --git a/iota/api.py b/iota/api.py index 04cd96c..b191a2b 100644 --- a/iota/api.py +++ b/iota/api.py @@ -6,7 +6,7 @@ from inspect import isabstract as is_abstract from typing import Any, Callable, Dict, Iterable, Optional, Text, Union -from six import text_type as text, with_metaclass +from six import text_type, with_metaclass from iota.adapter import BaseAdapter, resolve_adapter from iota.types import TransactionId, TryteString @@ -115,7 +115,7 @@ def _convert_to_tryte_strings(self, response, keys, type_=TryteString): TryteStrings. """ def converter(value): - if isinstance(value, text): + if isinstance(value, text_type): return type_(value.encode('ascii')) elif isinstance(value, Iterable): diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index 1d0df20..07f4b6a 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -2,6 +2,9 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from six import string_types, text_type +from typing import Generator, Sequence + from iota.api import BaseCommand @@ -14,7 +17,34 @@ class AddNeighborsCommand(BaseCommand): command = 'addNeighbors' def _prepare_request(self, request): - pass + # Required parameters. + uris = request['uris'] + + if isinstance(uris, Generator): + # :see: https://youtrack.jetbrains.com/issue/PY-20709 + # noinspection PyTypeChecker + uris = list(uris) + + if isinstance(uris, string_types) or not isinstance(uris, Sequence): + raise TypeError( + 'uris has wrong type (expected Sequence, actual {type}).'.format( + type = type(uris).__name__, + ), + ) + + if not uris: + raise ValueError('uris must not be empty.') + + for i, u in enumerate(uris): + if not isinstance(u, string_types): + raise TypeError( + 'uris[{i}] has wrong type ' + '(expected {expected}, actual {type}).'.format( + i = i, + expected = text_type.__name__, + type = type(u).__name__, + ), + ) def _prepare_response(self, response): pass diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index 47320d6..2f09ec4 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -4,7 +4,7 @@ from typing import Generator, Sequence -from six import binary_type +from six import binary_type, string_types from iota.api import BaseCommand from iota.types import TransactionId, TryteString @@ -47,12 +47,17 @@ def _prepare_request(self, params): ), ) + if isinstance(trytes, Generator): + # :see: https://youtrack.jetbrains.com/issue/PY-20709 + # noinspection PyTypeChecker + trytes = list(trytes) + # Technically, we only need `trytes` to be an Iterable, but some # types (such as TryteString) are Iterable yet not acceptable # here. - if not isinstance(trytes, (Sequence, Generator)): + if isinstance(trytes, string_types) or not isinstance(trytes, Sequence): raise TypeError( - 'trytes has wrong type (expected Iterable, actual {type}).'.format( + 'trytes has wrong type (expected Sequence, actual {type}).'.format( type = type(trytes).__name__, ), ) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index b675801..9b7e0ca 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -42,3 +42,17 @@ def test_happy_path(self): {}, )] ) + + def test_uris_error_invalid(self): + """Attempting to call `addNeighbors`, but `uris` is invalid.""" + with self.assertRaises(TypeError): + # It's gotta be an array. + self.command(uris='http://localhost:8080/') + + with self.assertRaises(TypeError): + # I meant an array of strings! + self.command(uris=[42, 'http://localhost:8080/']) + + with self.assertRaises(ValueError): + # Insert "Forever Alone" meme here. + self.command(uris=[]) From 58807e1ed69885aeeca7bcc8cc4843a61f5cd3dd Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 1 Dec 2016 20:21:07 -0500 Subject: [PATCH 037/239] Implemented `broadcastTransactions` Command. --- iota/api.py | 2 +- iota/commands/broadcast_transactions.py | 45 +++++++++ test/commands/attach_to_tangle_test.py | 11 ++- test/commands/broadcast_transactions_test.py | 96 ++++++++++++++++++++ 4 files changed, 150 insertions(+), 4 deletions(-) create mode 100644 iota/commands/broadcast_transactions.py create mode 100644 test/commands/broadcast_transactions_test.py diff --git a/iota/api.py b/iota/api.py index b191a2b..50728e6 100644 --- a/iota/api.py +++ b/iota/api.py @@ -226,7 +226,7 @@ def broadcast_transactions(self, trytes): :see: https://iota.readme.io/docs/broadcasttransactions """ - raise NotImplementedError('Not implemented yet.') + return self.broadcastTransactions(trytes=trytes) def find_transactions( self, diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py new file mode 100644 index 0000000..d6629ce --- /dev/null +++ b/iota/commands/broadcast_transactions.py @@ -0,0 +1,45 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Generator, Sequence + +from six import binary_type, string_types + +from iota.api import BaseCommand +from iota.types import TryteString + + +class BroadcastTransactionsCommand(BaseCommand): + """ + Executes `broadcastTransactions` command. + + :see: iota.IotaApi.broadcast_transactions + """ + command = 'broadcastTransactions' + + def _prepare_request(self, request): + # Required parameters. + trytes = request['trytes'] + + if isinstance(trytes, Generator): + # :see: https://youtrack.jetbrains.com/issue/PY-20709 + # noinspection PyTypeChecker + trytes = list(trytes) + + if isinstance(trytes, string_types) or not isinstance(trytes, Sequence): + raise TypeError( + 'trytes has wrong type (expected Sequence, actual {type}).'.format( + type = type(trytes).__name__, + ), + ) + + if not trytes: + raise ValueError('trytes must not be empty.') + + return { + 'trytes': [binary_type(TryteString(t)) for t in trytes], + } + + def _prepare_response(self, response): + pass diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 940ee3d..fc0cb0e 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -4,6 +4,8 @@ from unittest import TestCase +from six import binary_type + from iota.commands.attach_to_tangle import AttachToTangleCommand from iota.types import TransactionId, TryteString from test import MockAdapter @@ -59,10 +61,13 @@ def test_happy_path(self): [( { 'command': 'attachToTangle', - 'trunkTransaction': trunk_transaction.trytes, - 'branchTransaction': branch_transaction.trytes, 'minWeightMagnitude': min_weight_magnitude, - 'trytes': [trytes[0].trytes], + + # We can't send TryteString objects across the wire, so + # trytes were converted into ASCII for transport. + 'trunkTransaction': binary_type(trunk_transaction), + 'branchTransaction': binary_type(branch_transaction), + 'trytes': [binary_type(trytes[0])], }, {}, diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py new file mode 100644 index 0000000..9c6e3c8 --- /dev/null +++ b/test/commands/broadcast_transactions_test.py @@ -0,0 +1,96 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota.commands.broadcast_transactions import BroadcastTransactionsCommand +from iota.types import TryteString +from test import MockAdapter + + +# noinspection SpellCheckingInspection +class BroadcastTransactionsCommandTestCase(TestCase): + def setUp(self): + super(BroadcastTransactionsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = BroadcastTransactionsCommand(self.adapter) + + def test_happy_path(self): + """Successful invocation of `broadcastTransactions`.""" + expected_response = {} + + self.adapter.response = expected_response + + response = self.command( + trytes = [ + # These values tend to get rather long, but for purposes of + # this test, we don't have to get too realistic. + TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH'), + ], + ) + + self.assertDictEqual(response, expected_response) + + self.assertListEqual( + self.adapter.requests, + + [( + { + 'command': 'broadcastTransactions', + + 'trytes': [ + b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', + ], + }, + + {}, + )] + ) + + def test_compatible_types(self): + """ + Invoking `broadcastTransactions` with parameters that can be + converted into the correct types. + """ + self.command( + trytes = [ + b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', + ], + ) + + self.assertListEqual( + self.adapter.requests, + + [( + { + 'command': 'broadcastTransactions', + + 'trytes': [ + b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', + ], + }, + + {}, + )] + ) + + def test_error_trytes_invalid(self): + """ + Attempting to call `broadcastTransactions` but `trytes` is invalid. + """ + with self.assertRaises(TypeError): + # This won't work; `trytes` has to be an array. + self.command( + trytes = TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQF'), + ) + + with self.assertRaises(TypeError): + # Seriously, you haven't figured this out yet? + self.command(trytes=['not a valid tryte string', 42]) + + with self.assertRaises(ValueError): + # Got everything set up, but nothing to broadcast? + # Welcome to your first YouTube channel! + self.command(trytes=[]) From 96fec93fbba2d3be5ee74148fd28038607457cc9 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 11:21:42 -0500 Subject: [PATCH 038/239] Converted README to rST for PyPi. --- README.md | 40 ---------------------------------------- README.rst | 51 +++++++++++++++++++++++++++++++++++++++++++++++++++ setup.py | 7 +++++++ 3 files changed, 58 insertions(+), 40 deletions(-) delete mode 100644 README.md create mode 100644 README.rst diff --git a/README.md b/README.md deleted file mode 100644 index 709b41c..0000000 --- a/README.md +++ /dev/null @@ -1,40 +0,0 @@ -# PyOTA -This is the official Python library for the IOTA Core. -It implements both the [official API](https://iota.readme.io/), as well as - newly proposed functionality (such as signing, bundles, utilities and - conversion). - -> **Join the Discussion** -> If you want to get involved in the community, need help with getting setup, -> have any issues related with the library or just want to discuss Blockchain, -> Distributed Ledgers and IoT with other people, feel free to join our -> [Slack](http://slack.iotatoken.com/). -> You can also ask questions on our -> [dedicated forum](http://forum.iotatoken.com/). - -# Dependencies -PyOTA requires Python v3.5 or v2.7. - -# Installation -To install the latest stable version: -``` -pip install https://github.com/iotaledger/pyota/archive/master.zip -``` - -To install the development version: -``` -pip install https://github.com/iotaledger/pyota/archive/develop.zip -``` - -# Documentation -For the full documentation of this library, please refer to the - [official API](https://iota.readme.io/) - -# Contributing -## Running Unit Tests -To run unit tests for the project: - -``` -pip install tox -tox -``` diff --git a/README.rst b/README.rst new file mode 100644 index 0000000..7e3b82f --- /dev/null +++ b/README.rst @@ -0,0 +1,51 @@ +===== +PyOTA +===== +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). + +Join the Discussion +=================== +If you want to get involved in the community, need help with getting setup, +have any issues related with the library or just want to discuss Blockchain, +Distributed Ledgers and IoT with other people, feel free to join our +`Slack `. + +You can also ask questions on our +`dedicated forum `. + +============ +Dependencies +============ +PyOTA requires Python v3.5 or v2.7. + +============ +Installation +============ +To install the latest stable version:: + + pip install https://github.com/iotaledger/pyota/archive/master.zip + +To install the development version:: + + pip install https://github.com/iotaledger/pyota/archive/develop.zip + +============= +Documentation +============= +For the full documentation of this library, please refer to the + `official API ` + +============ +Contributing +============ + +Running Unit Tests +================== +To run unit tests for the project:: + + pip install tox + tox diff --git a/setup.py b/setup.py index 9b560b1..611a038 100644 --- a/setup.py +++ b/setup.py @@ -3,8 +3,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from codecs import StreamReader, open from setuptools import setup + +with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader + long_description = f.read() + setup( name = 'PyOTA', description = 'IOTA API library for Python', @@ -12,6 +17,8 @@ version = '1.0.0', packages = ['iota'], + long_description = long_description, + install_requires = [ 'requests', 'six', From a9b6dac9df59cdea32c94eb1dca37e3f431d2e75 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 11:30:47 -0500 Subject: [PATCH 039/239] Added unit test info to setup.py. --- setup.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/setup.py b/setup.py index 611a038..97dad52 100644 --- a/setup.py +++ b/setup.py @@ -25,6 +25,13 @@ 'typing ; python_version < "3.5"', ], + test_suite = 'test', + test_loader = 'nose.loader:TestLoader', + tests_require = [ + 'mock', + 'nose', + ], + data_files = [ ('', ['LICENSE']), ], From 0394f27071d9ff0eed9ae59bd5dfdc13bfeac070 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 12:24:12 -0500 Subject: [PATCH 040/239] Fixed awkward type reference. --- iota/adapter.py | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/iota/adapter.py b/iota/adapter.py index 41ea87f..d5bcc4f 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -32,7 +32,7 @@ class InvalidUri(ValueError): pass -adapter_registry = {} # type: Dict[Text, AdapterMeta] +adapter_registry = {} # type: Dict[Text, _AdapterMeta] """Keeps track of available adapters and their supported protocols.""" @@ -54,20 +54,25 @@ def resolve_adapter(uri): return adapter_type.configure(uri) -class AdapterMeta(ABCMeta): +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) + super(_AdapterMeta, cls).__init__(what, bases, dict) if not is_abstract(cls): for protocol in getattr(cls, 'supported_protocols', ()): adapter_registry[protocol] = cls + def configure(cls, uri): + # type: (Text) -> BaseAdapter + """Creates a new adapter from the specified URI.""" + return cls(uri) -class BaseAdapter(with_metaclass(AdapterMeta)): + +class BaseAdapter(with_metaclass(_AdapterMeta)): """ Interface for IOTA API adapters. @@ -96,14 +101,6 @@ def send_request(self, payload, **kwargs): 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) - @classmethod - def configure(cls, uri): - # type: (Text) -> BaseAdapter - """ - Creates a new instance using the specified URI. - """ - return cls(uri) - class HttpAdapter(BaseAdapter): """ From f7c20a13ed8d6104f3a70256b8f9665749538c62 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 13:09:19 -0500 Subject: [PATCH 041/239] Integrated filters into `addNeighbors`. --- iota/adapter.py | 1 - iota/api.py | 135 +------------- iota/commands/__init__.py | 229 ++++++++++++++++++++++++ iota/commands/add_neighbors.py | 50 ++---- iota/commands/attach_to_tangle.py | 2 +- iota/commands/broadcast_transactions.py | 2 +- iota/commands/get_node_info.py | 2 +- iota/filters.py | 32 ++++ setup.py | 1 + test/api_test.py | 2 +- test/commands/add_neighbors_test.py | 7 +- test/filters_test.py | 49 +++++ 12 files changed, 336 insertions(+), 176 deletions(-) create mode 100644 iota/filters.py create mode 100644 test/filters_test.py diff --git a/iota/adapter.py b/iota/adapter.py index d5bcc4f..0ac3938 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -84,7 +84,6 @@ class BaseAdapter(with_metaclass(_AdapterMeta)): Protocols that `resolve_adapter` can use to identify this adapter type. """ - @abstract_method def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict diff --git a/iota/api.py b/iota/api.py index 50728e6..cbe17c7 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,13 +2,10 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from abc import ABCMeta, abstractmethod as abstract_method -from inspect import isabstract as is_abstract -from typing import Any, Callable, Dict, Iterable, Optional, Text, Union - -from six import text_type, with_metaclass +from typing import Iterable, Optional, Text, Union from iota.adapter import BaseAdapter, resolve_adapter +from iota.commands import CustomCommand, command_registry from iota.types import TransactionId, TryteString __all__ = [ @@ -16,134 +13,6 @@ ] -command_registry = {} # type: Dict[Text, CommandMeta] -"""Registry of commands, indexed by command name.""" - -class CommandMeta(ABCMeta): - """Automatically register new commands.""" - # noinspection PyShadowingBuiltins - def __init__(cls, what, bases=None, dict=None): - super(CommandMeta, cls).__init__(what, bases, dict) - - if not is_abstract(cls): - command = getattr(cls, 'command') - if command: - command_registry[command] = cls - - -class BaseCommand(with_metaclass(CommandMeta)): - """An API command ready to send to the node.""" - command = None # Text - - def __init__(self, adapter): - # type: (BaseAdapter) -> None - self.adapter = adapter - - self.called = False - self.request = None # type: dict - self.response = None # type: dict - - def __call__(self, **kwargs): - # type: (dict) -> dict - """Sends the command to the node.""" - if self.called: - raise RuntimeError('Command has already been called.') - - self.request = kwargs - - replacement = self._prepare_request(self.request) - if replacement is not None: - self.request = replacement - - self.request['command'] = self.command - - self.response = self.adapter.send_request(self.request) - - replacement = self._prepare_response(self.response) - if replacement is not None: - self.response = replacement - - self.called = True - - return self.response - - @abstract_method - def _prepare_request(self, request): - # type: (dict) -> Optional[dict] - """ - Modifies the request before sending it to the node. - - If this method returns a dict, it will replace the request - entirely. - - Note: the `command` parameter will be injected later; it is - not necessary for this method to include it. - """ - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) - - @abstract_method - def _prepare_response(self, response): - # type: (dict) -> Optional[dict] - """ - Modifies the response from the node. - - If this method returns a dict, it will replace the response - entirely. - """ - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) - - @staticmethod - def _convert_response_values(response, keys, converter): - # type: (dict, Iterable[Text], Callable[Any, Any]) -> None - """ - Converts non-null response values at the specified keys to the - specified type. - """ - for k in keys: - value = response.get(k) - if value is not None: - response[k] = converter(value) - - def _convert_to_tryte_strings(self, response, keys, type_=TryteString): - # type: (dict, Iterable[Text], type) -> None - """ - Converts non-null response values at the specified keys to - TryteStrings. - """ - def converter(value): - if isinstance(value, text_type): - return type_(value.encode('ascii')) - - elif isinstance(value, Iterable): - return list(map(converter, value)) - - self._convert_response_values(response, keys, converter) - - -class CustomCommand(BaseCommand): - """Used to execute experimental/undocumented commands.""" - def __init__(self, adapter, command): - # type: (BaseAdapter, Text) -> None - super(CustomCommand, self).__init__(adapter) - - self.command = command - - def _prepare_request(self, request): - pass - - def _prepare_response(self, response): - pass - - -# Populate the command registry. -# noinspection PyUnresolvedReferences -from iota.commands import * - - class IotaApi(object): """ API to send HTTP requests for communicating with an IOTA node. diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 3f3d02d..657d054 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -1,3 +1,232 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ unicode_literals + +from abc import ABCMeta, abstractmethod as abstract_method +from importlib import import_module +from inspect import isabstract as is_abstract +from pkgutil import walk_packages +from types import ModuleType +from typing import Any, Callable, Dict, Iterable, Optional, Text, Union + +from filters import BaseFilter, FilterRunner +from six import with_metaclass, text_type, string_types + +from iota.adapter import BaseAdapter +from iota.types import TryteString + +__all__ = [ + 'BaseCommand', + 'CustomCommand', + 'command_registry', +] + + +command_registry = {} # type: Dict[Text, CommandMeta] +"""Registry of commands, indexed by command name.""" + + +def discover_commands(package, recursively=True): + # type: (Union[ModuleType, Text], bool) -> None + """ + Automatically discover commands in the specified package. + + :param package: Package path or reference. + :param recursively: If True, will descend recursively into + sub-packages. + """ + # :see: http://stackoverflow.com/a/25562415/ + if isinstance(package, string_types): + package = import_module(package) # type: ModuleType + + for _, name, is_package in walk_packages(package.__path__): + # Loading the module is good enough; the CommandMeta metaclass will + # ensure that any commands in the module get registered. + sub_package = import_module(package.__name__ + '.' + name) + + if recursively and is_package: + discover_commands(sub_package) + + +class CommandMeta(ABCMeta): + """Automatically register new commands.""" + # noinspection PyShadowingBuiltins + def __init__(cls, what, bases=None, dict=None): + super(CommandMeta, cls).__init__(what, bases, dict) + + if not is_abstract(cls): + command = getattr(cls, 'command') + if command: + command_registry[command] = cls + + +class BaseCommand(with_metaclass(CommandMeta)): + """An API command ready to send to the node.""" + command = None # Text + + def __init__(self, adapter): + # type: (BaseAdapter) -> None + self.adapter = adapter + + self.called = False + self.request = None # type: dict + self.response = None # type: dict + + def __call__(self, **kwargs): + # type: (dict) -> dict + """Sends the command to the node.""" + if self.called: + raise RuntimeError('Command has already been called.') + + self.request = kwargs + + replacement = self._prepare_request(self.request) + if replacement is not None: + self.request = replacement + + self.request['command'] = self.command + + self.response = self.adapter.send_request(self.request) + + replacement = self._prepare_response(self.response) + if replacement is not None: + self.response = replacement + + self.called = True + + return self.response + + @abstract_method + def _prepare_request(self, request): + # type: (dict) -> Optional[dict] + """ + Modifies the request before sending it to the node. + + If this method returns a dict, it will replace the request + entirely. + + Note: the `command` parameter will be injected later; it is + not necessary for this method to include it. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + @abstract_method + def _prepare_response(self, response): + # type: (dict) -> Optional[dict] + """ + Modifies the response from the node. + + If this method returns a dict, it will replace the response + entirely. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + @staticmethod + def _convert_response_values(response, keys, converter): + # type: (dict, Iterable[Text], Callable[Any, Any]) -> None + """ + Converts non-null response values at the specified keys to the + specified type. + """ + for k in keys: + value = response.get(k) + if value is not None: + response[k] = converter(value) + + def _convert_to_tryte_strings(self, response, keys, type_=TryteString): + # type: (dict, Iterable[Text], type) -> None + """ + Converts non-null response values at the specified keys to + TryteStrings. + """ + def converter(value): + if isinstance(value, text_type): + return type_(value.encode('ascii')) + + elif isinstance(value, Iterable): + return list(map(converter, value)) + + self._convert_response_values(response, keys, converter) + + +class CustomCommand(BaseCommand): + """Used to execute experimental/undocumented commands.""" + def __init__(self, adapter, command): + # type: (BaseAdapter, Text) -> None + super(CustomCommand, self).__init__(adapter) + + self.command = command + + def _prepare_request(self, request): + pass + + def _prepare_response(self, response): + pass + + +class FilterError(ValueError): + """ + Indicates that the request or response passed to a FilterCommand + failed one or more filters. + """ + def __init__(self, message, filter_runner): + # type: (Text, FilterRunner) -> None + super(FilterError, self).__init__(message) + + self.context = filter_runner.get_errors(with_context=True) + + +class FilterCommand(with_metaclass(ABCMeta, BaseCommand)): + """Uses filters to manipulate request/response values.""" + @abstract_method + def get_request_filter(self): + # type: () -> Optional[BaseFilter] + """Returns the filter that should be applied to the request.""" + + @abstract_method + def get_response_filter(self): + # type: () -> Optional[BaseFilter] + """Returns the filter that should be applied to the response.""" + + def _prepare_request(self, request): + return self._apply_filter( + value = request, + filter_ = self.get_request_filter(), + failure_message = 'Request failed validation', + ) + + def _prepare_response(self, response): + return self._apply_filter( + value = response, + filter_ = self.get_response_filter(), + failure_message = 'Response failed validation', + ) + + @staticmethod + def _apply_filter(value, filter_, failure_message): + if filter_: + runner = FilterRunner(filter_, value) + + if runner.is_valid(): + return runner.cleaned_data + else: + raise FilterError( + message = + '{message} ({keys}) ' + '(`exc.context` contains more information).'.format( + message = failure_message, + keys = list(sorted(runner.filter_messages.keys())), + ), + + filter_runner = runner, + ) + + return value + + +# Autodiscover commands in this package. +discover_commands(__name__) diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index 07f4b6a..1281022 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -2,13 +2,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from six import string_types, text_type -from typing import Generator, Sequence +import filters as f -from iota.api import BaseCommand +from iota.commands import FilterCommand +from iota.filters import NodeUri -class AddNeighborsCommand(BaseCommand): +class AddNeighborsCommand(FilterCommand): """ Executes `addNeighbors` command. @@ -16,35 +16,15 @@ class AddNeighborsCommand(BaseCommand): """ command = 'addNeighbors' - def _prepare_request(self, request): - # Required parameters. - uris = request['uris'] - - if isinstance(uris, Generator): - # :see: https://youtrack.jetbrains.com/issue/PY-20709 - # noinspection PyTypeChecker - uris = list(uris) - - if isinstance(uris, string_types) or not isinstance(uris, Sequence): - raise TypeError( - 'uris has wrong type (expected Sequence, actual {type}).'.format( - type = type(uris).__name__, - ), - ) - - if not uris: - raise ValueError('uris must not be empty.') - - for i, u in enumerate(uris): - if not isinstance(u, string_types): - raise TypeError( - 'uris[{i}] has wrong type ' - '(expected {expected}, actual {type}).'.format( - i = i, - expected = text_type.__name__, - type = type(u).__name__, - ), - ) - - def _prepare_response(self, response): + def get_request_filter(self): + return f.FilterMapper( + { + 'uris': f.Required | f.FilterRepeater(NodeUri), + }, + + allow_extra_keys = False, + allow_missing_keys = False, + ) + + def get_response_filter(self): pass diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index 2f09ec4..2aed909 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -6,7 +6,7 @@ from six import binary_type, string_types -from iota.api import BaseCommand +from iota.commands import BaseCommand from iota.types import TransactionId, TryteString __all__ = [ diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index d6629ce..488cb19 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -6,7 +6,7 @@ from six import binary_type, string_types -from iota.api import BaseCommand +from iota.commands import BaseCommand from iota.types import TryteString diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index 35f19de..5b6e88a 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from iota.api import BaseCommand +from iota.commands import BaseCommand __all__ = [ 'GetNodeInfoCommand', diff --git a/iota/filters.py b/iota/filters.py new file mode 100644 index 0000000..0d4048a --- /dev/null +++ b/iota/filters.py @@ -0,0 +1,32 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Text + +import filters as f +from six import text_type + +from iota.adapter import resolve_adapter, InvalidUri + + +class NodeUri(f.BaseFilter): + """Validates a string as a node URI.""" + CODE_INVALID = 'not_node_uri' + + templates = { + CODE_INVALID: 'This value does not appear to be a valid node URI.', + } + + def _apply(self, value): + value = self._filter(value, f.Type(text_type)) # type: Text + + if self._has_errors: + return None + + try: + resolve_adapter(value) + except InvalidUri: + return self._invalid_value(value, self.CODE_INVALID, exc_info=True) + + return value diff --git a/setup.py b/setup.py index 97dad52..6eb060a 100644 --- a/setup.py +++ b/setup.py @@ -20,6 +20,7 @@ long_description = long_description, install_requires = [ + 'filters', 'requests', 'six', 'typing ; python_version < "3.5"', diff --git a/test/api_test.py b/test/api_test.py index 665fb33..ab586ba 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -5,7 +5,7 @@ from unittest import TestCase from iota import IotaApi -from iota.api import CustomCommand +from iota.commands import CustomCommand from iota.commands.get_node_info import GetNodeInfoCommand from test import MockAdapter diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index 9b7e0ca..0f29517 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -4,6 +4,7 @@ from unittest import TestCase +from iota.commands import FilterError from iota.commands.add_neighbors import AddNeighborsCommand from test import MockAdapter @@ -45,14 +46,14 @@ def test_happy_path(self): def test_uris_error_invalid(self): """Attempting to call `addNeighbors`, but `uris` is invalid.""" - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): # It's gotta be an array. self.command(uris='http://localhost:8080/') - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): # I meant an array of strings! self.command(uris=[42, 'http://localhost:8080/']) - with self.assertRaises(ValueError): + with self.assertRaises(FilterError): # Insert "Forever Alone" meme here. self.command(uris=[]) diff --git a/test/filters_test.py b/test/filters_test.py new file mode 100644 index 0000000..ccc5bb0 --- /dev/null +++ b/test/filters_test.py @@ -0,0 +1,49 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase + +from iota.filters import NodeUri + + +class NodeUriTestCase(BaseFilterTestCase): + filter_type = NodeUri + + def test_pass_none(self): + """ + ``None`` always passes this filter. + + Use ``Required | NodeUri`` to reject null values. + """ + self.assertFilterPasses(None) + + def test_pass_uri(self): + """The incoming value is a valid URI.""" + self.assertFilterPasses('udp://localhost:14265/node') + + def test_fail_not_a_uri(self): + """The incoming value is not a URI.""" + self.assertFilterErrors( + 'not a valid uri', + [NodeUri.CODE_INVALID], + ) + + def test_fail_bytes(self): + """ + To ensure consistent behavior in Python 2 and 3, bytes are not + accepted. + """ + self.assertFilterErrors( + b'udp://localhost:14265/node', + [f.Type.CODE_WRONG_TYPE], + ) + + def test_fail_wrong_type(self): + """The incoming value is not a string.""" + self.assertFilterErrors( + # Use ``FilterRepeater(NodeUri)`` to validate a sequence of URIs. + ['udp://localhost:14265/node'], + [f.Type.CODE_WRONG_TYPE], + ) From 2e5b80cd228c3cde57827629dc7520b3f3ae1569 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 14:56:15 -0500 Subject: [PATCH 042/239] Fixed rST error. --- README.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/README.rst b/README.rst index 7e3b82f..0d5e6a1 100644 --- a/README.rst +++ b/README.rst @@ -49,3 +49,4 @@ To run unit tests for the project:: pip install tox tox + From 48d9752c81c4f0608d3d038ae6dd48adfd8e150d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 14:57:08 -0500 Subject: [PATCH 043/239] Implemented Trytes filter. --- iota/commands/__init__.py | 13 +++-- iota/filters.py | 74 +++++++++++++++++++++++-- test/filters_test.py | 110 ++++++++++++++++++++++++++++++++++++-- 3 files changed, 184 insertions(+), 13 deletions(-) diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 657d054..01cc2bf 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -177,7 +177,9 @@ def __init__(self, message, filter_runner): # type: (Text, FilterRunner) -> None super(FilterError, self).__init__(message) - self.context = filter_runner.get_errors(with_context=True) + self.context = { + 'filter_errors': filter_runner.get_errors(with_context=True), + } class FilterCommand(with_metaclass(ABCMeta, BaseCommand)): @@ -216,10 +218,11 @@ def _apply_filter(value, filter_, failure_message): else: raise FilterError( message = - '{message} ({keys}) ' - '(`exc.context` contains more information).'.format( - message = failure_message, - keys = list(sorted(runner.filter_messages.keys())), + '{message} ({error_codes}) ' + '(`exc.context["filter_errors"]` ' + 'contains more information).'.format( + message = failure_message, + error_codes = runner.error_codes, ), filter_runner = runner, diff --git a/iota/filters.py b/iota/filters.py index 0d4048a..8f59933 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -2,20 +2,21 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Text +from typing import Text, Union import filters as f -from six import text_type +from six import binary_type, text_type from iota.adapter import resolve_adapter, InvalidUri +from iota.types import TryteString class NodeUri(f.BaseFilter): """Validates a string as a node URI.""" - CODE_INVALID = 'not_node_uri' + CODE_NOT_NODE_URI = 'not_node_uri' templates = { - CODE_INVALID: 'This value does not appear to be a valid node URI.', + CODE_NOT_NODE_URI: 'This value does not appear to be a valid node URI.', } def _apply(self, value): @@ -27,6 +28,69 @@ def _apply(self, value): try: resolve_adapter(value) except InvalidUri: - return self._invalid_value(value, self.CODE_INVALID, exc_info=True) + return self._invalid_value(value, self.CODE_NOT_NODE_URI, exc_info=True) return value + + +class Trytes(f.BaseFilter): + """Validates a sequence as a sequence of trytes.""" + CODE_NOT_TRYTES = 'not_trytes' + CODE_WRONG_FORMAT = 'wrong_format' + + templates = { + CODE_NOT_TRYTES: 'This value is not a valid tryte sequence.', + CODE_WRONG_FORMAT: 'This value is not a valid {result_type}.', + } + + def __init__(self, result_type=TryteString): + # type: (type) -> None + super(Trytes, self).__init__() + + if not isinstance(result_type, type): + raise TypeError( + 'Invalid result_type for {filter_type} ' + '(expected subclass of TryteString, ' + 'actual instance of {result_type}).'.format( + filter_type = type(self).__name__, + result_type = type(result_type).__name__, + ), + ) + + if not issubclass(result_type, TryteString): + raise ValueError( + 'Invalid result_type for {filter_type} ' + '(expected TryteString, actual {result_type}).'.format( + filter_type = type(self).__name__, + result_type = result_type.__name__, + ), + ) + + self.result_type = result_type + + def _apply(self, value): + value = self._filter(value, f.Type((binary_type, bytearray, TryteString))) # type: Union[binary_type, bytearray, TryteString] + + if self._has_errors: + return None + + try: + value = TryteString(value) + except ValueError: + return self._invalid_value(value, self.CODE_NOT_TRYTES, exc_info=True) + + if self.result_type is TryteString: + return value + + try: + return self.result_type(value) + except ValueError: + return self._invalid_value( + value = value, + reason = self.CODE_WRONG_FORMAT, + exc_info = True, + + template_vars = { + 'result_type': self.result_type.__name__, + } + ) diff --git a/test/filters_test.py b/test/filters_test.py index ccc5bb0..b353418 100644 --- a/test/filters_test.py +++ b/test/filters_test.py @@ -5,7 +5,8 @@ import filters as f from filters.test import BaseFilterTestCase -from iota.filters import NodeUri +from iota.filters import NodeUri, Trytes +from iota.types import TryteString, TransactionId class NodeUriTestCase(BaseFilterTestCase): @@ -24,10 +25,18 @@ def test_pass_uri(self): self.assertFilterPasses('udp://localhost:14265/node') def test_fail_not_a_uri(self): - """The incoming value is not a URI.""" + """ + The incoming value is not a URI. + + Note: Internally, the filter uses `resolve_adapter`, which has its + own unit tests. We won't duplicate them here; a simple smoke + check should suffice. + + :py:class:`test.adapter_test.ResolveAdapterTestCase` + """ self.assertFilterErrors( 'not a valid uri', - [NodeUri.CODE_INVALID], + [NodeUri.CODE_NOT_NODE_URI], ) def test_fail_bytes(self): @@ -47,3 +56,98 @@ def test_fail_wrong_type(self): ['udp://localhost:14265/node'], [f.Type.CODE_WRONG_TYPE], ) + + +# noinspection SpellCheckingInspection +class TrytesTestCase(BaseFilterTestCase): + filter_type = Trytes + + def test_pass_none(self): + """ + ``None`` always passes this filter. + + Use ``Required | Trytes`` to reject null values. + """ + self.assertFilterPasses(None) + + def test_pass_ascii(self): + """The incoming value is ASCII.""" + trytes = b'RBTC9D9DCDQAEASBYBCCKBFA' + + filter_ = self._filter(trytes) + + self.assertFilterPasses(filter_, trytes) + self.assertIsInstance(filter_.cleaned_data, TryteString) + + def test_pass_bytearray(self): + """The incoming value is a bytearray.""" + trytes = bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA') + + filter_ = self._filter(trytes) + + self.assertFilterPasses(filter_, trytes) + self.assertIsInstance(filter_.cleaned_data, TryteString) + + def test_pass_tryte_string(self): + """The incoming value is a TryteString.""" + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + + filter_ = self._filter(trytes) + + self.assertFilterPasses(filter_, trytes) + self.assertIsInstance(filter_.cleaned_data, TryteString) + + def test_pass_alternate_result_type(self): + """Configuring the filter to return a specific type.""" + input_trytes = b'RBTC9D9DCDQAEASBYBCCKBFA' + + result_trytes = ( + b'RBTC9D9DCDQAEASBYBCCKBFA9999999999999999' + b'99999999999999999999999999999999999999999' + ) + + filter_ = self._filter(input_trytes, result_type=TransactionId) + + self.assertFilterPasses(filter_, result_trytes) + self.assertIsInstance(filter_.cleaned_data, TransactionId) + + def test_fail_not_trytes(self): + """ + The incoming value contains an invalid character. + + Note: Internally, the filter uses `TryteString`, which has its own + unit tests. We won't duplicate them here; a simple smoke check + should suffice. + + :ref:`test.types_test.TryteStringTestCase` + """ + self.assertFilterErrors( + # Everyone knows there's no such thing as "8"! + b'RBTC9D9DCDQAEASBYBCCKBFA8', + [Trytes.CODE_NOT_TRYTES], + ) + + def test_fail_alternate_result_type(self): + """ + The incoming value is a valid tryte sequence, but the filter is + configured for a specific type with stricter validation. + """ + trytes = ( + # Ooh, just a little bit too long there. + b'RBTC9D9DCDQAEASBYBCCKBFA99999999999999999' + b'99999999999999999999999999999999999999999' + ) + + self.assertFilterErrors( + self._filter(trytes, result_type=TransactionId), + [Trytes.CODE_WRONG_FORMAT], + ) + + def test_fail_wrong_type(self): + """The incoming value has an incompatible type.""" + self.assertFilterErrors( + # Use ``FilterRepeater(Trytes)`` to validate a sequence of tryte + # representations. + [TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA')], + [f.Type.CODE_WRONG_TYPE], + ) From ff61edc38a2c8373fbb4fadba4c1c13ee8327199 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 16:08:37 -0500 Subject: [PATCH 044/239] Integrated filters into `attachToTangle`. --- iota/commands/attach_to_tangle.py | 78 ++++------ test/commands/__init__.py | 43 ++++++ test/commands/add_neighbors_test.py | 31 ++-- test/commands/attach_to_tangle_test.py | 190 ++++++++++--------------- 4 files changed, 156 insertions(+), 186 deletions(-) diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index 2aed909..519a547 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -2,19 +2,18 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, Sequence +import filters as f -from six import binary_type, string_types - -from iota.commands import BaseCommand -from iota.types import TransactionId, TryteString +from iota.commands import FilterCommand +from iota.filters import Trytes +from iota.types import TransactionId __all__ = [ 'AttachToTangleCommand', ] -class AttachToTangleCommand(BaseCommand): +class AttachToTangleCommand(FilterCommand): """ Executes `attachToTangle` command. @@ -22,55 +21,30 @@ class AttachToTangleCommand(BaseCommand): """ command = 'attachToTangle' - def _prepare_request(self, params): - # Required parameters. - trunk_transaction = params['trunk_transaction'] - branch_transaction = params['branch_transaction'] - trytes = params['trytes'] - - # Optional parameters. - min_weight_magnitude = params.get('min_weight_magnitude', 18) - - if type(min_weight_magnitude) is not int: - raise TypeError( - 'min_weight_magnitude has wrong type ' - '(expected int, actual {type}).'.format( - type = type(min_weight_magnitude).__name__, - ), - ) + def get_request_filter(self): + return f.FilterMapper( + { + 'trunk_transaction': f.Required | Trytes(result_type=TransactionId), + 'branch_transaction': f.Required | Trytes(result_type=TransactionId), - if min_weight_magnitude < 18: - raise ValueError( - 'min_weight_magnitude is too small ' - '(expected >= 18, actual {value}).'.format( - value = min_weight_magnitude, - ), - ) + 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), - if isinstance(trytes, Generator): - # :see: https://youtrack.jetbrains.com/issue/PY-20709 - # noinspection PyTypeChecker - trytes = list(trytes) + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + }, - # Technically, we only need `trytes` to be an Iterable, but some - # types (such as TryteString) are Iterable yet not acceptable - # here. - if isinstance(trytes, string_types) or not isinstance(trytes, Sequence): - raise TypeError( - 'trytes has wrong type (expected Sequence, actual {type}).'.format( - type = type(trytes).__name__, - ), - ) + allow_extra_keys = False, - if not trytes: - raise ValueError('trytes must not be empty.') + allow_missing_keys = { + 'min_weight_magnitude', + }, + ) - return { - 'trunkTransaction': binary_type(TransactionId(trunk_transaction)), - 'branchTransaction': binary_type(TransactionId(branch_transaction)), - 'minWeightMagnitude': min_weight_magnitude, - 'trytes': [binary_type(TryteString(t)) for t in trytes], - } + def get_response_filter(self): + return f.FilterMapper( + { + 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), + }, - def _prepare_response(self, response): - self._convert_to_tryte_strings(response, ('trytes',)) + allow_extra_keys = True, + allow_missing_keys = True, + ) diff --git a/test/commands/__init__.py b/test/commands/__init__.py index 3f3d02d..4aa5665 100644 --- a/test/commands/__init__.py +++ b/test/commands/__init__.py @@ -1,3 +1,46 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ unicode_literals + +from pprint import pformat +from typing import Optional +from unittest import TestCase + +from iota.commands import FilterCommand, FilterError +from test import MockAdapter + + +class BaseCommandTestCase(TestCase): + command_type = None + + def setUp(self): + super(BaseCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = self.command_type(self.adapter) # type: FilterCommand + + def assertCommandSuccess(self, expected_response, request=None): + # type: (dict, Optional[dict]) -> None + """ + Sends the command to the adapter and expects a successful result. + """ + request = request or {} # type: dict + response = self.command(**request) + + self.assertDictEqual(response, expected_response) + + +class BaseFilterCommandTestCase(BaseCommandTestCase): + def assertCommandSuccess(self, expected_response, request=None): + # type: (dict, Optional[dict]) -> None + """ + Sends the command to the adapter and expects a successful result. + """ + try: + super(BaseFilterCommandTestCase, self)\ + .assertCommandSuccess(expected_response, request) + except FilterError as e: + self.fail('{exc}\n\n{errors}'.format( + exc = e, + errors = pformat(e.context['filter_errors']), + )) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index 0f29517..b50ab78 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -2,19 +2,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from unittest import TestCase - from iota.commands import FilterError from iota.commands.add_neighbors import AddNeighborsCommand -from test import MockAdapter - +from test.commands import BaseFilterCommandTestCase -class AddNeighborsCommandTestCase(TestCase): - def setUp(self): - super(AddNeighborsCommandTestCase, self).setUp() - self.adapter = MockAdapter() - self.command = AddNeighborsCommand(self.adapter) +class AddNeighborsCommandTestCase(BaseFilterCommandTestCase): + command_type = AddNeighborsCommand def test_happy_path(self): """Successful invocation of `addNeighbors`.""" @@ -27,21 +21,12 @@ def test_happy_path(self): neighbors = ['udp://node1.iotatoken.com:14265/', 'http://localhost:14265/'] - response = self.command(uris=neighbors) - - self.assertDictEqual(response, expected_response) - - self.assertListEqual( - self.adapter.requests, - - [( - { - 'command': 'addNeighbors', - 'uris': neighbors, - }, + self.assertCommandSuccess( + expected_response = expected_response, - {}, - )] + request = { + 'uris': neighbors, + }, ) def test_uris_error_invalid(self): diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index fc0cb0e..388fa95 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -2,31 +2,22 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from unittest import TestCase - -from six import binary_type - +from iota.commands import FilterError from iota.commands.attach_to_tangle import AttachToTangleCommand from iota.types import TransactionId, TryteString -from test import MockAdapter +from test.commands import BaseFilterCommandTestCase # noinspection SpellCheckingInspection -class AttachToTangleCommandTestCase(TestCase): - def setUp(self): - super(AttachToTangleCommandTestCase, self).setUp() - - self.adapter = MockAdapter() - self.command = AttachToTangleCommand(self.adapter) +class AttachToTangleCommandTestCase(BaseFilterCommandTestCase): + command_type = AttachToTangleCommand def test_happy_path(self): """Successful invocation of `attachToTangle`.""" - expected_response = { + self.adapter.response = { 'trytes': ['TRYTEVALUEHERE'] } - self.adapter.response = expected_response - trunk_transaction =\ TransactionId( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' @@ -42,36 +33,17 @@ def test_happy_path(self): min_weight_magnitude = 20 trytes = [TryteString(b'TRYTVALUEHERE')] - response = self.command( - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - min_weight_magnitude = min_weight_magnitude, - trytes = trytes, - ) - - self.assertDictEqual(response, expected_response) - - self.assertListEqual( - list(map(type, response['trytes'])), - [TryteString], - ) + self.assertCommandSuccess( + expected_response = { + 'trytes': [TryteString(b'TRYTEVALUEHERE')] + }, - self.assertListEqual( - self.adapter.requests, - [( - { - 'command': 'attachToTangle', - 'minWeightMagnitude': min_weight_magnitude, - - # We can't send TryteString objects across the wire, so - # trytes were converted into ASCII for transport. - 'trunkTransaction': binary_type(trunk_transaction), - 'branchTransaction': binary_type(branch_transaction), - 'trytes': [binary_type(trytes[0])], - }, - - {}, - )] + request = { + 'trunk_transaction': trunk_transaction, + 'branch_transaction': branch_transaction, + 'min_weight_magnitude': min_weight_magnitude, + 'trytes': trytes, + }, ) def test_compatible_types(self): @@ -79,62 +51,41 @@ def test_compatible_types(self): Calling `attachToTangle` with parameters that can be converted into the correct types. """ - self.command( - # Any value that can be converted into a TransactionId is valid - # here. - trunk_transaction =\ - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + self.adapter.response = { + 'trytes': ['TRYTEVALUEHERE', 'TRANSACTIONIDHERE'] + } - branch_transaction =\ - TryteString( + self.assertCommandSuccess( + expected_response = { + 'trytes': [ + TryteString(b'TRYTEVALUEHERE'), + TryteString(b'TRANSACTIONIDHERE'), + ], + }, + + request = { + # Any value that can be converted into a TransactionId is valid + # here. + 'trunk_transaction': b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', - ), - - # This still has to be an int, however. - min_weight_magnitude = 30, - - # Just to be extra tricky, let's see what happens if `trytes` is - # a generator. - trytes = ( - t for t in [ - # `trytes` can contain any value that can be converted into a - # TryteString. - b'TRYTEVALUEHERE', - - # This is probably wrong, but maybe not. - TransactionId(b'TRANSACTIONIDHERE'), - ] - ), - ) - - # Not interested in the response, but we should check to make sure - # that the incoming values were converted correctly. - request = self.adapter.requests[0][0] - self.assertDictEqual( - request, - - { - 'command': 'attachToTangle', - 'minWeightMagnitude': 30, - - 'trunkTransaction': + 'branch_transaction': + TryteString( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - , + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + ), - 'branchTransaction': - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - , + # This still has to be an int, however. + 'min_weight_magnitude': 30, 'trytes': [ - b'TRYTEVALUEHERE', + # `trytes` can contain any value that can be converted into a + # TryteString. + b'TRYTEVALUEHERE', - b'TRANSACTIONIDHERE99999999999999999999999' - b'99999999999999999999999999999999999999999', + # This is probably wrong, but maybe not. + TransactionId(b'TRANSACTIONIDHERE'), ], }, ) @@ -153,7 +104,16 @@ def test_error_trunk_transaction_invalid(self): trytes = [TryteString(b'TRYTEVALUEHERE')] - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): + self.command( + # Bytes are allowed, but not strings. + trunk_transaction = 'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT', + + branch_transaction = branch_transaction, + trytes = trytes, + ) + + with self.assertRaises(FilterError): self.command( # Now you're not making any sense. trunk_transaction = None, @@ -162,7 +122,7 @@ def test_error_trunk_transaction_invalid(self): trytes = trytes, ) - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): self.command( # Are you even listening to me? trunk_transaction = 42, @@ -185,7 +145,16 @@ def test_error_branch_transaction_invalid(self): trytes = [TryteString(b'TRYTEVALUEHERE')] - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): + self.command( + # Bytes are allowed, but not strings. + branch_transaction = 'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT', + + trunk_transaction = trunk_transaction, + trytes = trytes, + ) + + with self.assertRaises(FilterError): self.command( # Now you're not making any sense. branch_transaction = None, @@ -194,7 +163,7 @@ def test_error_branch_transaction_invalid(self): trytes = trytes, ) - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): self.command( # Are you even listening to me? branch_transaction = 42, @@ -223,7 +192,7 @@ def test_error_min_weight_magnitude_invalid(self): trytes = [TryteString(b'TRYTVALUEHERE')] - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): self.command( # Nice try, but it's gotta be an int. min_weight_magnitude = 18.0, @@ -233,7 +202,7 @@ def test_error_min_weight_magnitude_invalid(self): trytes = trytes, ) - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): self.command( # Oh, come on. You know what I meant! min_weight_magnitude = True, @@ -243,7 +212,7 @@ def test_error_min_weight_magnitude_invalid(self): trytes = trytes, ) - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): self.command( # I swear you're doing this on purpose just to annoy me. min_weight_magnitude = 'eighteen', @@ -253,19 +222,9 @@ def test_error_min_weight_magnitude_invalid(self): trytes = trytes, ) - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): self.command( - # This parameter is required. - min_weight_magnitude = None, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(ValueError): - self.command( - # Minimum value for this parameter is 18. + # Better, but the minimum value for this parameter is 18. min_weight_magnitude = 17, trunk_transaction = trunk_transaction, @@ -291,7 +250,7 @@ def test_error_trytes_invalid(self): b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' ) - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): self.command( # It's gotta be a list. trytes = TryteString(b'TRYTEVALUEHERE'), @@ -300,7 +259,7 @@ def test_error_trytes_invalid(self): branch_transaction = branch_transaction, ) - with self.assertRaises(ValueError): + with self.assertRaises(FilterError): self.command( # Ok, you got the list part down, but you have to put something # inside it. @@ -309,3 +268,12 @@ def test_error_trytes_invalid(self): trunk_transaction = trunk_transaction, branch_transaction = branch_transaction, ) + + with self.assertRaises(FilterError): + self.command( + # I hate you so much. + trytes = [42], + + trunk_transaction = trunk_transaction, + branch_transaction = branch_transaction, + ) From c746aa61d5a3d28ef78e208fb9bb79816466e924 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 16:13:58 -0500 Subject: [PATCH 045/239] Integrated filters into `broadcastTransactions`. --- iota/commands/broadcast_transactions.py | 41 +++------- test/commands/broadcast_transactions_test.py | 85 ++++++-------------- 2 files changed, 38 insertions(+), 88 deletions(-) diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index 488cb19..1fb4b77 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -2,15 +2,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, Sequence +import filters as f -from six import binary_type, string_types +from iota.commands import FilterCommand +from iota.filters import Trytes -from iota.commands import BaseCommand -from iota.types import TryteString - -class BroadcastTransactionsCommand(BaseCommand): +class BroadcastTransactionsCommand(FilterCommand): """ Executes `broadcastTransactions` command. @@ -18,28 +16,15 @@ class BroadcastTransactionsCommand(BaseCommand): """ command = 'broadcastTransactions' - def _prepare_request(self, request): - # Required parameters. - trytes = request['trytes'] - - if isinstance(trytes, Generator): - # :see: https://youtrack.jetbrains.com/issue/PY-20709 - # noinspection PyTypeChecker - trytes = list(trytes) - - if isinstance(trytes, string_types) or not isinstance(trytes, Sequence): - raise TypeError( - 'trytes has wrong type (expected Sequence, actual {type}).'.format( - type = type(trytes).__name__, - ), - ) - - if not trytes: - raise ValueError('trytes must not be empty.') + def get_request_filter(self): + return f.FilterMapper( + { + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + }, - return { - 'trytes': [binary_type(TryteString(t)) for t in trytes], - } + allow_extra_keys = False, + allow_missing_keys = False, + ) - def _prepare_response(self, response): + def get_response_filter(self): pass diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index 9c6e3c8..84e9032 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -2,51 +2,28 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from unittest import TestCase - +from iota.commands import FilterError from iota.commands.broadcast_transactions import BroadcastTransactionsCommand from iota.types import TryteString -from test import MockAdapter +from test.commands import BaseFilterCommandTestCase # noinspection SpellCheckingInspection -class BroadcastTransactionsCommandTestCase(TestCase): - def setUp(self): - super(BroadcastTransactionsCommandTestCase, self).setUp() - - self.adapter = MockAdapter() - self.command = BroadcastTransactionsCommand(self.adapter) +class BroadcastTransactionsCommandTestCase(BaseFilterCommandTestCase): + command_type = BroadcastTransactionsCommand def test_happy_path(self): """Successful invocation of `broadcastTransactions`.""" - expected_response = {} - - self.adapter.response = expected_response - - response = self.command( - trytes = [ - # These values tend to get rather long, but for purposes of - # this test, we don't have to get too realistic. - TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH'), - ], - ) - - self.assertDictEqual(response, expected_response) - - self.assertListEqual( - self.adapter.requests, - - [( - { - 'command': 'broadcastTransactions', - - 'trytes': [ - b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', - ], - }, - - {}, - )] + self.assertCommandSuccess( + expected_response = {}, + + request = { + 'trytes': [ + # These values tend to get rather long, but for purposes of + # this test, we don't have to get too realistic. + TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'), + ], + }, ) def test_compatible_types(self): @@ -54,43 +31,31 @@ def test_compatible_types(self): Invoking `broadcastTransactions` with parameters that can be converted into the correct types. """ - self.command( - trytes = [ - b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', - ], - ) - - self.assertListEqual( - self.adapter.requests, - - [( - { - 'command': 'broadcastTransactions', - - 'trytes': [ - b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', - ], - }, - - {}, - )] + self.assertCommandSuccess( + expected_response = {}, + + request = { + 'trytes': [ + b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', + ], + } ) def test_error_trytes_invalid(self): """ Attempting to call `broadcastTransactions` but `trytes` is invalid. """ - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): # This won't work; `trytes` has to be an array. self.command( trytes = TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQF'), ) - with self.assertRaises(TypeError): + with self.assertRaises(FilterError): # Seriously, you haven't figured this out yet? self.command(trytes=['not a valid tryte string', 42]) - with self.assertRaises(ValueError): + with self.assertRaises(FilterError): # Got everything set up, but nothing to broadcast? # Welcome to your first YouTube channel! self.command(trytes=[]) From fd765e302ff7a03164a7e2f2558f9857449e52ee Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 16:29:09 -0500 Subject: [PATCH 046/239] Integrated filters into `getNodeInfo`, minor fixes. --- iota/commands/__init__.py | 67 ++++++++++---------- iota/commands/broadcast_transactions.py | 9 ++- iota/commands/get_node_info.py | 32 +++++++--- test/commands/broadcast_transactions_test.py | 30 +++++++-- test/commands/get_node_info_test.py | 65 +++++++++++-------- 5 files changed, 128 insertions(+), 75 deletions(-) diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 01cc2bf..82b3604 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -7,13 +7,12 @@ from inspect import isabstract as is_abstract from pkgutil import walk_packages from types import ModuleType -from typing import Any, Callable, Dict, Iterable, Optional, Text, Union +from typing import Dict, Optional, Text, Union from filters import BaseFilter, FilterRunner -from six import with_metaclass, text_type, string_types +from six import with_metaclass, string_types from iota.adapter import BaseAdapter -from iota.types import TryteString __all__ = [ 'BaseCommand', @@ -125,36 +124,14 @@ def _prepare_response(self, response): 'Not implemented in {cls}.'.format(cls=type(self).__name__), ) - @staticmethod - def _convert_response_values(response, keys, converter): - # type: (dict, Iterable[Text], Callable[Any, Any]) -> None - """ - Converts non-null response values at the specified keys to the - specified type. - """ - for k in keys: - value = response.get(k) - if value is not None: - response[k] = converter(value) - - def _convert_to_tryte_strings(self, response, keys, type_=TryteString): - # type: (dict, Iterable[Text], type) -> None - """ - Converts non-null response values at the specified keys to - TryteStrings. - """ - def converter(value): - if isinstance(value, text_type): - return type_(value.encode('ascii')) - - elif isinstance(value, Iterable): - return list(map(converter, value)) - - self._convert_response_values(response, keys, converter) - class CustomCommand(BaseCommand): - """Used to execute experimental/undocumented commands.""" + """ + Sends an arbitrary command to the node, with no request/response + validation. + + Useful for executing experimental/undocumented commands. + """ def __init__(self, adapter, command): # type: (BaseAdapter, Text) -> None super(CustomCommand, self).__init__(adapter) @@ -187,12 +164,30 @@ class FilterCommand(with_metaclass(ABCMeta, BaseCommand)): @abstract_method def get_request_filter(self): # type: () -> Optional[BaseFilter] - """Returns the filter that should be applied to the request.""" + """ + Returns the filter that should be applied to the request (if any). + + Generally, this filter should be strict about validating/converting + the values in the request, to minimize the chance of an error + response from the node. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) @abstract_method def get_response_filter(self): # type: () -> Optional[BaseFilter] - """Returns the filter that should be applied to the response.""" + """ + Returns the filter that should be applied to the response (if any). + + Generally, this filter should be less concerned with validation and + more concerned with ensuring the response values have the correct + types, since we can't control what the node sends us. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) def _prepare_request(self, request): return self._apply_filter( @@ -210,6 +205,12 @@ def _prepare_response(self, response): @staticmethod def _apply_filter(value, filter_, failure_message): + # type: (dict, Optional[BaseFilter], Text) -> dict + """ + Applies a filter to a value. If the value does not pass the + filter, an exception will be raised with lots of contextual info + attached to it. + """ if filter_: runner = FilterRunner(filter_, value) diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index 1fb4b77..885d44a 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -27,4 +27,11 @@ def get_request_filter(self): ) def get_response_filter(self): - pass + return f.FilterMapper( + { + 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), + }, + + allow_extra_keys = True, + allow_missing_keys = True, + ) diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index 5b6e88a..7dd1879 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -2,14 +2,17 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from iota.commands import BaseCommand +import filters as f + +from iota.commands import FilterCommand +from iota.filters import Trytes __all__ = [ 'GetNodeInfoCommand', ] -class GetNodeInfoCommand(BaseCommand): +class GetNodeInfoCommand(FilterCommand): """ Executes `getNodeInfo` command. @@ -17,13 +20,26 @@ class GetNodeInfoCommand(BaseCommand): """ command = 'getNodeInfo' - def _prepare_request(self, request): - pass + def get_request_filter(self): + # `getNodeInfo` does not accept any parameters. + # Using a filter here just to enforce that the request is empty. + return f.FilterMapper( + { + }, - def _prepare_response(self, response): - self._convert_to_tryte_strings( - response = response, - keys = ('latestMilestone', 'latestSolidSubtangleMilestone'), + allow_extra_keys = False, + allow_missing_keys = False, ) + def get_response_filter(self): + return f.FilterMapper( + { + 'latestMilestone': f.ByteString(encoding='ascii') | Trytes, + + 'latestSolidSubtangleMilestone': + f.ByteString(encoding='ascii') | Trytes, + }, + allow_extra_keys = True, + allow_missing_keys = True, + ) diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index 84e9032..71e7927 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -14,15 +14,23 @@ class BroadcastTransactionsCommandTestCase(BaseFilterCommandTestCase): def test_happy_path(self): """Successful invocation of `broadcastTransactions`.""" + self.adapter.response = { + 'trytes': ['BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'], + } + + trytes = [ + # These values tend to get rather long, but for purposes of this + # test, we don't have to get too realistic. + TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'), + ] + self.assertCommandSuccess( - expected_response = {}, + expected_response = { + 'trytes': trytes, + }, request = { - 'trytes': [ - # These values tend to get rather long, but for purposes of - # this test, we don't have to get too realistic. - TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'), - ], + 'trytes': trytes, }, ) @@ -31,8 +39,16 @@ def test_compatible_types(self): Invoking `broadcastTransactions` with parameters that can be converted into the correct types. """ + self.adapter.response = { + 'trytes': ['BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'], + } + self.assertCommandSuccess( - expected_response = {}, + expected_response = { + 'trytes': [ + TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'), + ], + }, request = { 'trytes': [ diff --git a/test/commands/get_node_info_test.py b/test/commands/get_node_info_test.py index e575134..6e7b756 100644 --- a/test/commands/get_node_info_test.py +++ b/test/commands/get_node_info_test.py @@ -2,24 +2,18 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from unittest import TestCase - from iota.commands.get_node_info import GetNodeInfoCommand from iota.types import TryteString -from test import MockAdapter +from test.commands import BaseFilterCommandTestCase # noinspection SpellCheckingInspection -class GetNodeInfoCommandTestCase(TestCase): - def setUp(self): - super(GetNodeInfoCommandTestCase, self).setUp() - - self.adapter = MockAdapter() - self.command = GetNodeInfoCommand(self.adapter) +class GetNodeInfoCommandTestCase(BaseFilterCommandTestCase): + command_type = GetNodeInfoCommand def test_happy_path(self): """Successful invocation of `getNodeInfo`.""" - expected_response = { + self.adapter.response = { 'appName': 'IRI', 'appVersion': '1.0.8.nu', 'duration': 1, @@ -27,31 +21,50 @@ def test_happy_path(self): 'jreFreeMemory': 91707424, 'jreMaxMemory': 1908932608, 'jreTotalMemory': 122683392, - 'latestMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', 'latestMilestoneIndex': 107, - 'latestSolidSubtangleMilestone': 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJRFKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', 'latestSolidSubtangleMilestoneIndex': 107, 'neighbors': 2, 'packetsQueueSize': 0, 'time': 1477037811737, 'tips': 3, - 'transactionsToRequest': 0 - } + 'transactionsToRequest': 0, - self.adapter.response = expected_response + 'latestMilestone': + 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJR' + 'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', - response = self.command() - - self.assertDictEqual(response, expected_response) + 'latestSolidSubtangleMilestone': + 'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJR' + 'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', + } - self.assertIsInstance(response['latestMilestone'], TryteString) + self.assertCommandSuccess( + expected_response = { + 'appName': 'IRI', + 'appVersion': '1.0.8.nu', + 'duration': 1, + 'jreAvailableProcessors': 4, + 'jreFreeMemory': 91707424, + 'jreMaxMemory': 1908932608, + 'jreTotalMemory': 122683392, + 'latestMilestoneIndex': 107, + 'latestSolidSubtangleMilestoneIndex': 107, + 'neighbors': 2, + 'packetsQueueSize': 0, + 'time': 1477037811737, + 'tips': 3, + 'transactionsToRequest': 0, - self.assertIsInstance( - response['latestSolidSubtangleMilestone'], - TryteString, - ) + 'latestMilestone': + TryteString( + b'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJR' + b'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', + ), - self.assertListEqual( - self.adapter.requests, - [({'command': 'getNodeInfo'}, {})], + 'latestSolidSubtangleMilestone': + TryteString( + b'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJR' + b'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999' + ), + } ) From e102cc4fc565da7a4148f836facfe3aacda9024b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 16:51:05 -0500 Subject: [PATCH 047/239] `addNeighbors` unit tests are now less insane. --- iota/commands/__init__.py | 32 +++++++-- iota/commands/add_neighbors.py | 18 ++--- test/commands/add_neighbors_test.py | 101 ++++++++++++++++++++-------- 3 files changed, 108 insertions(+), 43 deletions(-) diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 82b3604..717e1d9 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -9,7 +9,7 @@ from types import ModuleType from typing import Dict, Optional, Text, Union -from filters import BaseFilter, FilterRunner +import filters as f from six import with_metaclass, string_types from iota.adapter import BaseAdapter @@ -106,6 +106,8 @@ def _prepare_request(self, request): Note: the `command` parameter will be injected later; it is not necessary for this method to include it. + + :param request: Guaranteed to be a dict, but it might be empty. """ raise NotImplementedError( 'Not implemented in {cls}.'.format(cls=type(self).__name__), @@ -119,6 +121,8 @@ def _prepare_response(self, response): If this method returns a dict, it will replace the response entirely. + + :param response: Guaranteed to be a dict, but it might be empty. """ raise NotImplementedError( 'Not implemented in {cls}.'.format(cls=type(self).__name__), @@ -151,7 +155,7 @@ class FilterError(ValueError): failed one or more filters. """ def __init__(self, message, filter_runner): - # type: (Text, FilterRunner) -> None + # type: (Text, f.FilterRunner) -> None super(FilterError, self).__init__(message) self.context = { @@ -159,11 +163,27 @@ def __init__(self, message, filter_runner): } +class RequestFilter(f.FilterMapper): + """Template for filter applied to API requests.""" + def __init__(self, filter_map): + # Be more strict about missing/extra keys for requests, since they + # tend to come from code that the developer has control over. + super(RequestFilter, self).__init__(filter_map, False, False) + + +class ResponseFilter(f.FilterMapper): + """Template for filter applied to API responses.""" + def __init__(self, filter_map): + # Be a little looser about missing/extra keys for responses, since + # we can't control what the node sends us back. + super(ResponseFilter, self).__init__(filter_map, True, True) + + class FilterCommand(with_metaclass(ABCMeta, BaseCommand)): """Uses filters to manipulate request/response values.""" @abstract_method def get_request_filter(self): - # type: () -> Optional[BaseFilter] + # type: () -> Optional[RequestFilter] """ Returns the filter that should be applied to the request (if any). @@ -177,7 +197,7 @@ def get_request_filter(self): @abstract_method def get_response_filter(self): - # type: () -> Optional[BaseFilter] + # type: () -> Optional[ResponseFilter] """ Returns the filter that should be applied to the response (if any). @@ -205,14 +225,14 @@ def _prepare_response(self, response): @staticmethod def _apply_filter(value, filter_, failure_message): - # type: (dict, Optional[BaseFilter], Text) -> dict + # type: (dict, Optional[f.BaseFilter], Text) -> dict """ Applies a filter to a value. If the value does not pass the filter, an exception will be raised with lots of contextual info attached to it. """ if filter_: - runner = FilterRunner(filter_, value) + runner = f.FilterRunner(filter_, value) if runner.is_valid(): return runner.cleaned_data diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index 1281022..9309b98 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -4,7 +4,7 @@ import filters as f -from iota.commands import FilterCommand +from iota.commands import FilterCommand, RequestFilter from iota.filters import NodeUri @@ -17,14 +17,14 @@ class AddNeighborsCommand(FilterCommand): command = 'addNeighbors' def get_request_filter(self): - return f.FilterMapper( - { - 'uris': f.Required | f.FilterRepeater(NodeUri), - }, - - allow_extra_keys = False, - allow_missing_keys = False, - ) + return AddNeighborsRequestFilter def get_response_filter(self): pass + + +class AddNeighborsRequestFilter(RequestFilter): + def __init__(self): + super(AddNeighborsRequestFilter, self).__init__({ + 'uris': f.Required | f.Array | f.FilterRepeater(f.Required | NodeUri), + }) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index b50ab78..a57cc4c 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -2,43 +2,88 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from iota.commands import FilterError -from iota.commands.add_neighbors import AddNeighborsCommand -from test.commands import BaseFilterCommandTestCase +import filters as f +from filters.test import BaseFilterTestCase +from iota.commands.add_neighbors import AddNeighborsRequestFilter +from iota.filters import NodeUri -class AddNeighborsCommandTestCase(BaseFilterCommandTestCase): - command_type = AddNeighborsCommand - def test_happy_path(self): - """Successful invocation of `addNeighbors`.""" - expected_response = { - 'addedNeighbors': 0, - 'duration': 2, - } +class AddNeighborsRequestFilterTestCase(BaseFilterTestCase): + filter_type = AddNeighborsRequestFilter - self.adapter.response = expected_response + def test_pass_happy_path(self): + """The incoming request is valid.""" + self.assertFilterPasses({ + 'uris': [ + 'udp://node1.iotatoken.com', + 'http://localhost:14265/', + ], + }) - neighbors = ['udp://node1.iotatoken.com:14265/', 'http://localhost:14265/'] + def test_fail_neighbors_wrong_type(self): + """`neighbors` is not an array.""" + self.assertFilterErrors( + { + # Nope; it's gotta be an array, even if you only want to add + # a single neighbor. + 'uris': 'http://localhost:8080/' + }, + + { + 'uris': [f.Type.CODE_WRONG_TYPE] + }, - self.assertCommandSuccess( - expected_response = expected_response, + self.skip_value_check, + ) - request = { - 'uris': neighbors, + def test_neighbors_empty(self): + """`neighbors` is an array, but it's empty.""" + self.assertFilterErrors( + { + # Insert "Forever Alone" meme here. + 'uris': [], }, + + { + 'uris': [f.Required.CODE_EMPTY], + }, + + self.skip_value_check, ) - def test_uris_error_invalid(self): - """Attempting to call `addNeighbors`, but `uris` is invalid.""" - with self.assertRaises(FilterError): - # It's gotta be an array. - self.command(uris='http://localhost:8080/') + def test_neighbors_contents_invalid(self): + """ + `neighbors` is an array, but it contains invalid values. + """ + self.assertFilterErrors( + { + # When I said it has to be an array before, I meant an array of + # strings! + 'uris': [ + '', + False, + None, + b'http://localhost:8080/', + 'not a valid uri', - with self.assertRaises(FilterError): - # I meant an array of strings! - self.command(uris=[42, 'http://localhost:8080/']) - with self.assertRaises(FilterError): - # Insert "Forever Alone" meme here. - self.command(uris=[]) + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + 'udp://localhost', + + 2130706433, + ], + }, + + { + 'uris.0': [f.Required.CODE_EMPTY], + 'uris.1': [f.Type.CODE_WRONG_TYPE], + 'uris.2': [f.Required.CODE_EMPTY], + 'uris.3': [f.Type.CODE_WRONG_TYPE], + 'uris.4': [NodeUri.CODE_NOT_NODE_URI], + 'uris.6': [f.Type.CODE_WRONG_TYPE], + }, + + self.skip_value_check, + ) From f477563a9808f5b7a1822eb79b36a391e4806d05 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 17:42:26 -0500 Subject: [PATCH 048/239] Made `attachToTangle` tests less insane. --- iota/commands/__init__.py | 38 +- iota/commands/add_neighbors.py | 2 +- iota/commands/attach_to_tangle.py | 27 +- test/commands/add_neighbors_test.py | 15 +- test/commands/attach_to_tangle_test.py | 581 +++++++++++++++---------- 5 files changed, 395 insertions(+), 268 deletions(-) diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 717e1d9..7c0828e 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -7,7 +7,7 @@ from inspect import isabstract as is_abstract from pkgutil import walk_packages from types import ModuleType -from typing import Dict, Optional, Text, Union +from typing import Dict, Mapping, Optional, Text, Union import filters as f from six import with_metaclass, string_types @@ -163,20 +163,36 @@ def __init__(self, message, filter_runner): } -class RequestFilter(f.FilterMapper): +class RequestFilter(f.FilterChain): """Template for filter applied to API requests.""" - def __init__(self, filter_map): - # Be more strict about missing/extra keys for requests, since they - # tend to come from code that the developer has control over. - super(RequestFilter, self).__init__(filter_map, False, False) + # Be more strict about missing/extra keys for requests, since they + # tend to come from code that the developer has control over. + def __init__( + self, + filter_map, + allow_missing_keys = False, + allow_extra_keys = False, + ): + super(RequestFilter, self).__init__( + f.Type(Mapping) + | f.FilterMapper(filter_map, allow_missing_keys, allow_extra_keys) + ) -class ResponseFilter(f.FilterMapper): +class ResponseFilter(f.FilterChain): """Template for filter applied to API responses.""" - def __init__(self, filter_map): - # Be a little looser about missing/extra keys for responses, since - # we can't control what the node sends us back. - super(ResponseFilter, self).__init__(filter_map, True, True) + # Be a little looser about missing/extra keys for responses, since we + # can't control what the node sends us back. + def __init__( + self, + filter_map, + allow_missing_keys = True, + allow_extra_keys = True, + ): + super(ResponseFilter, self).__init__( + f.Type(Mapping) + | f.FilterMapper(filter_map, allow_missing_keys, allow_extra_keys) + ) class FilterCommand(with_metaclass(ABCMeta, BaseCommand)): diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index 9309b98..b83f212 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -17,7 +17,7 @@ class AddNeighborsCommand(FilterCommand): command = 'addNeighbors' def get_request_filter(self): - return AddNeighborsRequestFilter + return AddNeighborsRequestFilter() def get_response_filter(self): pass diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index 519a547..9471599 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -4,7 +4,7 @@ import filters as f -from iota.commands import FilterCommand +from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes from iota.types import TransactionId @@ -22,7 +22,15 @@ class AttachToTangleCommand(FilterCommand): command = 'attachToTangle' def get_request_filter(self): - return f.FilterMapper( + return AttachToTangleRequestFilter() + + def get_response_filter(self): + return AttachToTangleResponseFilter() + + +class AttachToTangleRequestFilter(RequestFilter): + def __init__(self): + super(AttachToTangleRequestFilter, self).__init__( { 'trunk_transaction': f.Required | Trytes(result_type=TransactionId), 'branch_transaction': f.Required | Trytes(result_type=TransactionId), @@ -32,19 +40,14 @@ def get_request_filter(self): 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), }, - allow_extra_keys = False, - allow_missing_keys = { 'min_weight_magnitude', }, ) - def get_response_filter(self): - return f.FilterMapper( - { - 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), - }, - allow_extra_keys = True, - allow_missing_keys = True, - ) +class AttachToTangleResponseFilter(ResponseFilter): + def __init__(self): + super(AttachToTangleResponseFilter, self).__init__({ + 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), + }) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index a57cc4c..f7740fb 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -12,7 +12,7 @@ class AddNeighborsRequestFilterTestCase(BaseFilterTestCase): filter_type = AddNeighborsRequestFilter - def test_pass_happy_path(self): + def test_pass_valid_request(self): """The incoming request is valid.""" self.assertFilterPasses({ 'uris': [ @@ -21,6 +21,18 @@ def test_pass_happy_path(self): ], }) + def test_fail_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'uris': [f.FilterMapper.CODE_MISSING_KEY], + }, + + self.skip_value_check, + ) + def test_fail_neighbors_wrong_type(self): """`neighbors` is not an array.""" self.assertFilterErrors( @@ -67,7 +79,6 @@ def test_neighbors_contents_invalid(self): b'http://localhost:8080/', 'not a valid uri', - # This is actually valid; I just added it to make sure the # filter isn't cheating! 'udp://localhost', diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 388fa95..a0d3976 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -2,278 +2,375 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from iota.commands import FilterError -from iota.commands.attach_to_tangle import AttachToTangleCommand +import filters as f +from filters.test import BaseFilterTestCase +from six import binary_type, text_type + +from iota.commands.attach_to_tangle import AttachToTangleRequestFilter, \ + AttachToTangleResponseFilter +from iota.filters import Trytes from iota.types import TransactionId, TryteString -from test.commands import BaseFilterCommandTestCase -# noinspection SpellCheckingInspection -class AttachToTangleCommandTestCase(BaseFilterCommandTestCase): - command_type = AttachToTangleCommand +class AttachToTangleRequestFilterTestCase(BaseFilterTestCase): + filter_type = AttachToTangleRequestFilter + + # noinspection SpellCheckingInspection + def setUp(self): + super(AttachToTangleRequestFilterTestCase, self).setUp() + + # Define a few valid values here that we can reuse across multiple + # tests. + self.txn_id = ( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) - def test_happy_path(self): - """Successful invocation of `attachToTangle`.""" - self.adapter.response = { - 'trytes': ['TRYTEVALUEHERE'] + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_pass_valid_request(self): + """The incoming request is valid.""" + self.assertFilterPasses({ + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + 'min_weight_magnitude': 20, + + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }) + + def test_pass_min_weight_magnitude_missing(self): + """`min_weight_magnitude` is optional.""" + request = { + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + + 'trytes': [ + TryteString(self.trytes1) + ], + + # If not provided, this value is set to the minimum (18). + # 'min_weight_magnitude': 20, } - trunk_transaction =\ - TransactionId( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ) + filter_ = self._filter(request) + + expected_value = request.copy() + expected_value['min_weight_magnitude'] = 18 + + self.assertFilterPasses(filter_, expected_value) + + # noinspection SpellCheckingInspection + def test_pass_compatible_types(self): + """Incoming values can be converted into the expected types.""" + self.assertFilterPasses( + { + # Any value that can be converted into a TransactionId is valid + # here. + 'trunk_transaction': binary_type(self.txn_id), + 'branch_transaction': bytearray(self.txn_id), - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) + 'trytes': [ + # `trytes` can contain any value that can be converted into a + # TryteString. + binary_type(self.trytes1), - min_weight_magnitude = 20 - trytes = [TryteString(b'TRYTVALUEHERE')] + # This is probably wrong, but technically it's valid. + TransactionId( + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA', + ), + ], - self.assertCommandSuccess( - expected_response = { - 'trytes': [TryteString(b'TRYTEVALUEHERE')] + # This still has to be an int, however. + 'min_weight_magnitude': 30, }, - request = { - 'trunk_transaction': trunk_transaction, - 'branch_transaction': branch_transaction, - 'min_weight_magnitude': min_weight_magnitude, - 'trytes': trytes, + # After running through the filter, all of the values have been + # converted to the correct types. + { + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + 'min_weight_magnitude': 30, + + 'trytes': [ + TryteString(self.trytes1), + + TryteString( + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHD' + b'WCTCEAKDCDFD9DSCSA99999999999999999999999', + ), + ], + } + ) + + def test_error_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'trunk_transaction': [f.FilterMapper.CODE_MISSING_KEY], + 'branch_transaction': [f.FilterMapper.CODE_MISSING_KEY], + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], }, + + self.skip_value_check, ) - def test_compatible_types(self): - """ - Calling `attachToTangle` with parameters that can be converted into - the correct types. - """ - self.adapter.response = { - 'trytes': ['TRYTEVALUEHERE', 'TRANSACTIONIDHERE'] - } + def test_error_trunk_transaction_null(self): + """`trunk_transaction` is null.""" + self.assertFilterErrors( + { + 'trunk_transaction': None, + + 'branch_transaction': TransactionId(self.txn_id), + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'trunk_transaction': [f.Required.CODE_EMPTY], + }, + + self.skip_value_check, + ) + + def test_error_trunk_transaction_wrong_type(self): + """`trunk_transaction` can't be converted to a TryteString.""" + self.assertFilterErrors( + { + # Strings are not valid tryte sequences. + 'trunk_transaction': text_type(self.txn_id, 'ascii'), + + 'branch_transaction': TransactionId(self.txn_id), + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'trunk_transaction': [f.Type.CODE_WRONG_TYPE], + }, + + self.skip_value_check, + ) + + def test_error_branch_transaction_null(self): + """`branch_transaction` is null.""" + self.assertFilterErrors( + { + 'branch_transaction': None, + + 'trunk_transaction': TransactionId(self.txn_id), + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'branch_transaction': [f.Required.CODE_EMPTY], + }, + + self.skip_value_check, + ) + + def test_error_branch_transaction_wrong_type(self): + """`branch_transaction` can't be converted to a TryteString.""" + self.assertFilterErrors( + { + # Strings are not valid tryte sequences. + 'branch_transaction': text_type(self.txn_id, 'ascii'), + + 'trunk_transaction': TransactionId(self.txn_id), + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'branch_transaction': [f.Type.CODE_WRONG_TYPE], + }, + + self.skip_value_check, + ) + + def test_min_weight_magnitude_float(self): + """`min_weight_magnitude` is a float.""" + self.assertFilterErrors( + { + # I don't care if the fpart is empty; it's still not an int! + 'min_weight_magnitude': 20.0, + + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), - self.assertCommandSuccess( - expected_response = { 'trytes': [ - TryteString(b'TRYTEVALUEHERE'), - TryteString(b'TRANSACTIONIDHERE'), + TryteString(self.trytes1) ], }, - request = { - # Any value that can be converted into a TransactionId is valid - # here. - 'trunk_transaction': - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, - 'branch_transaction': - TryteString( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', - ), + self.skip_value_check, + ) - # This still has to be an int, however. - 'min_weight_magnitude': 30, + def test_min_weight_magnitude_string(self): + """`min_weight_magnitude` is a string.""" + self.assertFilterErrors( + { + # For want of an int cast, the transaction was lost. + 'min_weight_magnitude': '20', + + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), 'trytes': [ - # `trytes` can contain any value that can be converted into a - # TryteString. - b'TRYTEVALUEHERE', + TryteString(self.trytes1) + ], + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, - # This is probably wrong, but maybe not. - TransactionId(b'TRANSACTIONIDHERE'), + self.skip_value_check, + ) + + def test_min_weight_magnitude_too_small(self): + """`min_weight_magnitude` is less than 18.""" + self.assertFilterErrors( + { + 'min_weight_magnitude': 17, + + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + + 'trytes': [ + TryteString(self.trytes1) ], }, + + { + 'min_weight_magnitude': [f.Min.CODE_TOO_SMALL], + }, + + self.skip_value_check, ) - # noinspection PyTypeChecker - def test_error_trunk_transaction_invalid(self): - """ - Attempting to call `attachToTangle`, but the `trunkTransaction` - parameter is not valid. - """ - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - trytes = [TryteString(b'TRYTEVALUEHERE')] - - with self.assertRaises(FilterError): - self.command( - # Bytes are allowed, but not strings. - trunk_transaction = 'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT', - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(FilterError): - self.command( - # Now you're not making any sense. - trunk_transaction = None, - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(FilterError): - self.command( - # Are you even listening to me? - trunk_transaction = 42, - - branch_transaction = branch_transaction, - trytes = trytes, - ) - - # noinspection PyTypeChecker,PyUnresolvedReferences - def test_error_branch_transaction_invalid(self): - """ - Attempting to call `attachToTangle`, but the `branchTransaction` - parameter is not valid. - """ - trunk_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - trytes = [TryteString(b'TRYTEVALUEHERE')] - - with self.assertRaises(FilterError): - self.command( - # Bytes are allowed, but not strings. - branch_transaction = 'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT', - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - with self.assertRaises(FilterError): - self.command( - # Now you're not making any sense. - branch_transaction = None, - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - with self.assertRaises(FilterError): - self.command( - # Are you even listening to me? - branch_transaction = 42, - - trunk_transaction = trunk_transaction, - trytes = trytes, - ) - - # noinspection PyTypeChecker - def test_error_min_weight_magnitude_invalid(self): - """ - Attempting to call `attachToTangle`, but the `minWeightMagnitude` - parameter is not valid. - """ - trunk_transaction =\ - TransactionId( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ) - - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - trytes = [TryteString(b'TRYTVALUEHERE')] - - with self.assertRaises(FilterError): - self.command( - # Nice try, but it's gotta be an int. - min_weight_magnitude = 18.0, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(FilterError): - self.command( - # Oh, come on. You know what I meant! - min_weight_magnitude = True, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(FilterError): - self.command( - # I swear you're doing this on purpose just to annoy me. - min_weight_magnitude = 'eighteen', - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - with self.assertRaises(FilterError): - self.command( - # Better, but the minimum value for this parameter is 18. - min_weight_magnitude = 17, - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - trytes = trytes, - ) - - # noinspection PyTypeChecker - def test_error_trytes_invalid(self): - """ - Attempting to call `attachToTangle`, but the `trytes` parameter is - not valid. - """ - trunk_transaction =\ - TransactionId( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ) - - branch_transaction =\ - TransactionId( - b'P9KFSJVGSPLXAEBJSHWFZLGP9GGJTIO9YITDEHAT' - b'DTGAFLPLBZ9FOFWWTKMAZXZHFGQHUOXLXUALY9999' - ) - - with self.assertRaises(FilterError): - self.command( - # It's gotta be a list. - trytes = TryteString(b'TRYTEVALUEHERE'), - - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - ) - - with self.assertRaises(FilterError): - self.command( + def test_error_trytes_wrong_type(self): + """`trytes` is not an array.""" + self.assertFilterErrors( + { + # You have to specify an array, even if you only want to attach + # a single tryte sequence. + 'trytes': TryteString(self.trytes1), + + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + }, + + { + 'trytes': [f.Type.CODE_WRONG_TYPE], + }, + + self.skip_value_check, + ) + + def test_error_trytes_empty(self): + """`trytes` is an array, but it's empty.""" + self.assertFilterErrors( + { # Ok, you got the list part down, but you have to put something # inside it. - trytes = [], + 'trytes': [], - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - ) + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + + self.skip_value_check, + ) + + def test_error_trytes_contents_invalid(self): + """`trytes` is an array, but it contains invalid values.""" + self.assertFilterErrors( + { + 'trytes': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + ], + + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + }, + + { + 'trytes.0': [f.NotEmpty.CODE_EMPTY], + 'trytes.1': [f.Type.CODE_WRONG_TYPE], + 'trytes.2': [f.Type.CODE_WRONG_TYPE], + 'trytes.3': [f.Required.CODE_EMPTY], + 'trytes.4': [Trytes.CODE_NOT_TRYTES], + 'trytes.6': [f.Type.CODE_WRONG_TYPE], + }, + + self.skip_value_check, + ) - with self.assertRaises(FilterError): - self.command( - # I hate you so much. - trytes = [42], - trunk_transaction = trunk_transaction, - branch_transaction = branch_transaction, - ) +class AttachToTangleResponseFilterTestCase(BaseFilterTestCase): + filter_type = AttachToTangleResponseFilter + + # noinspection SpellCheckingInspection + def setUp(self): + super(AttachToTangleResponseFilterTestCase, self).setUp() + + # Define a few valid values here that we can reuse across multiple + # tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_pass_happy_path(self): + """The incoming response contains valid values.""" + self.assertFilterPasses( + # Responses from the node arrive as strings. + { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }, + + # The filter converts them into TryteStrings. + { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ) + + def test_pass_correct_types(self): + """ + The incoming response already contains correct types. + + This scenario is highly unusual, but who's complaining? + """ + self.assertFilterPasses({ + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ] + }) From fe48a535d990a06f85b8143b7efdb9074af2afb1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 18:30:44 -0500 Subject: [PATCH 049/239] Made `broadcastTransactions` tests less insane. --- iota/commands/broadcast_transactions.py | 34 +-- test/commands/add_neighbors_test.py | 22 +- test/commands/attach_to_tangle_test.py | 129 ++++++----- test/commands/broadcast_transactions_test.py | 225 ++++++++++++++----- 4 files changed, 265 insertions(+), 145 deletions(-) diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index 885d44a..a693f56 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -4,7 +4,7 @@ import filters as f -from iota.commands import FilterCommand +from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes @@ -17,21 +17,21 @@ class BroadcastTransactionsCommand(FilterCommand): command = 'broadcastTransactions' def get_request_filter(self): - return f.FilterMapper( - { - 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), - }, - - allow_extra_keys = False, - allow_missing_keys = False, - ) + return BroadcastTransactionsRequestFilter() def get_response_filter(self): - return f.FilterMapper( - { - 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), - }, - - allow_extra_keys = True, - allow_missing_keys = True, - ) + return BroadcastTransactionsResponseFilter() + + +class BroadcastTransactionsRequestFilter(RequestFilter): + def __init__(self): + super(BroadcastTransactionsRequestFilter, self).__init__({ + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + }) + + +class BroadcastTransactionsResponseFilter(ResponseFilter): + def __init__(self): + super(BroadcastTransactionsResponseFilter, self).__init__({ + 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), + }) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index f7740fb..f7dfc38 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -11,15 +11,21 @@ class AddNeighborsRequestFilterTestCase(BaseFilterTestCase): filter_type = AddNeighborsRequestFilter + skip_value_check = True def test_pass_valid_request(self): """The incoming request is valid.""" - self.assertFilterPasses({ + request = { 'uris': [ 'udp://node1.iotatoken.com', 'http://localhost:14265/', ], - }) + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_fail_empty(self): """The incoming request is empty.""" @@ -29,8 +35,6 @@ def test_fail_empty(self): { 'uris': [f.FilterMapper.CODE_MISSING_KEY], }, - - self.skip_value_check, ) def test_fail_neighbors_wrong_type(self): @@ -45,11 +49,9 @@ def test_fail_neighbors_wrong_type(self): { 'uris': [f.Type.CODE_WRONG_TYPE] }, - - self.skip_value_check, ) - def test_neighbors_empty(self): + def test_fail_neighbors_empty(self): """`neighbors` is an array, but it's empty.""" self.assertFilterErrors( { @@ -60,11 +62,9 @@ def test_neighbors_empty(self): { 'uris': [f.Required.CODE_EMPTY], }, - - self.skip_value_check, ) - def test_neighbors_contents_invalid(self): + def test_fail_neighbors_contents_invalid(self): """ `neighbors` is an array, but it contains invalid values. """ @@ -95,6 +95,4 @@ def test_neighbors_contents_invalid(self): 'uris.4': [NodeUri.CODE_NOT_NODE_URI], 'uris.6': [f.Type.CODE_WRONG_TYPE], }, - - self.skip_value_check, ) diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index a0d3976..2d739bf 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -14,6 +14,7 @@ class AttachToTangleRequestFilterTestCase(BaseFilterTestCase): filter_type = AttachToTangleRequestFilter + skip_value_check = True # noinspection SpellCheckingInspection def setUp(self): @@ -32,7 +33,7 @@ def setUp(self): def test_pass_valid_request(self): """The incoming request is valid.""" - self.assertFilterPasses({ + request = { 'trunk_transaction': TransactionId(self.txn_id), 'branch_transaction': TransactionId(self.txn_id), 'min_weight_magnitude': 20, @@ -41,7 +42,12 @@ def test_pass_valid_request(self): TryteString(self.trytes1), TryteString(self.trytes2), ], - }) + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_min_weight_magnitude_missing(self): """`min_weight_magnitude` is optional.""" @@ -58,36 +64,39 @@ def test_pass_min_weight_magnitude_missing(self): } filter_ = self._filter(request) + self.assertFilterPasses(filter_) expected_value = request.copy() expected_value['min_weight_magnitude'] = 18 - - self.assertFilterPasses(filter_, expected_value) + self.assertDictEqual(filter_.cleaned_data, expected_value) # noinspection SpellCheckingInspection def test_pass_compatible_types(self): """Incoming values can be converted into the expected types.""" - self.assertFilterPasses( - { - # Any value that can be converted into a TransactionId is valid - # here. - 'trunk_transaction': binary_type(self.txn_id), - 'branch_transaction': bytearray(self.txn_id), + filter_ = self._filter({ + # Any value that can be converted into a TransactionId is valid + # here. + 'trunk_transaction': binary_type(self.txn_id), + 'branch_transaction': bytearray(self.txn_id), - 'trytes': [ - # `trytes` can contain any value that can be converted into a - # TryteString. - binary_type(self.trytes1), + 'trytes': [ + # `trytes` can contain any value that can be converted into a + # TryteString. + binary_type(self.trytes1), + + # This is probably wrong, but technically it's valid. + TransactionId( + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA', + ), + ], - # This is probably wrong, but technically it's valid. - TransactionId( - b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA', - ), - ], + # This still has to be an int, however. + 'min_weight_magnitude': 30, + }) - # This still has to be an int, however. - 'min_weight_magnitude': 30, - }, + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, # After running through the filter, all of the values have been # converted to the correct types. @@ -107,7 +116,7 @@ def test_pass_compatible_types(self): } ) - def test_error_empty(self): + def test_fail_empty(self): """The incoming request is empty.""" self.assertFilterErrors( {}, @@ -117,11 +126,9 @@ def test_error_empty(self): 'branch_transaction': [f.FilterMapper.CODE_MISSING_KEY], 'trytes': [f.FilterMapper.CODE_MISSING_KEY], }, - - self.skip_value_check, ) - def test_error_trunk_transaction_null(self): + def test_fail_trunk_transaction_null(self): """`trunk_transaction` is null.""" self.assertFilterErrors( { @@ -134,11 +141,9 @@ def test_error_trunk_transaction_null(self): { 'trunk_transaction': [f.Required.CODE_EMPTY], }, - - self.skip_value_check, ) - def test_error_trunk_transaction_wrong_type(self): + def test_fail_trunk_transaction_wrong_type(self): """`trunk_transaction` can't be converted to a TryteString.""" self.assertFilterErrors( { @@ -152,11 +157,9 @@ def test_error_trunk_transaction_wrong_type(self): { 'trunk_transaction': [f.Type.CODE_WRONG_TYPE], }, - - self.skip_value_check, ) - def test_error_branch_transaction_null(self): + def test_fail_branch_transaction_null(self): """`branch_transaction` is null.""" self.assertFilterErrors( { @@ -169,11 +172,9 @@ def test_error_branch_transaction_null(self): { 'branch_transaction': [f.Required.CODE_EMPTY], }, - - self.skip_value_check, ) - def test_error_branch_transaction_wrong_type(self): + def test_fail_branch_transaction_wrong_type(self): """`branch_transaction` can't be converted to a TryteString.""" self.assertFilterErrors( { @@ -187,11 +188,9 @@ def test_error_branch_transaction_wrong_type(self): { 'branch_transaction': [f.Type.CODE_WRONG_TYPE], }, - - self.skip_value_check, ) - def test_min_weight_magnitude_float(self): + def test_fail_min_weight_magnitude_float(self): """`min_weight_magnitude` is a float.""" self.assertFilterErrors( { @@ -209,11 +208,9 @@ def test_min_weight_magnitude_float(self): { 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], }, - - self.skip_value_check, ) - def test_min_weight_magnitude_string(self): + def test_fail_min_weight_magnitude_string(self): """`min_weight_magnitude` is a string.""" self.assertFilterErrors( { @@ -231,11 +228,9 @@ def test_min_weight_magnitude_string(self): { 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], }, - - self.skip_value_check, ) - def test_min_weight_magnitude_too_small(self): + def test_fail_min_weight_magnitude_too_small(self): """`min_weight_magnitude` is less than 18.""" self.assertFilterErrors( { @@ -252,11 +247,9 @@ def test_min_weight_magnitude_too_small(self): { 'min_weight_magnitude': [f.Min.CODE_TOO_SMALL], }, - - self.skip_value_check, ) - def test_error_trytes_wrong_type(self): + def test_fail_trytes_wrong_type(self): """`trytes` is not an array.""" self.assertFilterErrors( { @@ -271,11 +264,9 @@ def test_error_trytes_wrong_type(self): { 'trytes': [f.Type.CODE_WRONG_TYPE], }, - - self.skip_value_check, ) - def test_error_trytes_empty(self): + def test_fail_trytes_empty(self): """`trytes` is an array, but it's empty.""" self.assertFilterErrors( { @@ -290,11 +281,9 @@ def test_error_trytes_empty(self): { 'trytes': [f.Required.CODE_EMPTY], }, - - self.skip_value_check, ) - def test_error_trytes_contents_invalid(self): + def test_fail_trytes_contents_invalid(self): """`trytes` is an array, but it contains invalid values.""" self.assertFilterErrors( { @@ -324,13 +313,12 @@ def test_error_trytes_contents_invalid(self): 'trytes.4': [Trytes.CODE_NOT_TRYTES], 'trytes.6': [f.Type.CODE_WRONG_TYPE], }, - - self.skip_value_check, ) class AttachToTangleResponseFilterTestCase(BaseFilterTestCase): filter_type = AttachToTangleResponseFilter + skip_value_check = True # noinspection SpellCheckingInspection def setUp(self): @@ -344,16 +332,20 @@ def setUp(self): def test_pass_happy_path(self): """The incoming response contains valid values.""" - self.assertFilterPasses( - # Responses from the node arrive as strings. - { - 'trytes': [ - text_type(self.trytes1, 'ascii'), - text_type(self.trytes2, 'ascii'), - ], - }, + # Responses from the node arrive as strings. + filter_ = self._filter({ + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + self.assertFilterPasses(filter_) + + # The filter converts them into TryteStrings. + self.assertDictEqual( + filter_.cleaned_data, - # The filter converts them into TryteStrings. { 'trytes': [ TryteString(self.trytes1), @@ -368,9 +360,14 @@ def test_pass_correct_types(self): This scenario is highly unusual, but who's complaining? """ - self.assertFilterPasses({ + response = { 'trytes': [ TryteString(self.trytes1), TryteString(self.trytes2), ] - }) + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, response) diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index 71e7927..4de0383 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -2,76 +2,201 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from iota.commands import FilterError -from iota.commands.broadcast_transactions import BroadcastTransactionsCommand +import filters as f +from filters.test import BaseFilterTestCase +from six import binary_type, text_type + +from iota.commands.broadcast_transactions import \ + BroadcastTransactionsRequestFilter, BroadcastTransactionsResponseFilter +from iota.filters import Trytes from iota.types import TryteString -from test.commands import BaseFilterCommandTestCase -# noinspection SpellCheckingInspection -class BroadcastTransactionsCommandTestCase(BaseFilterCommandTestCase): - command_type = BroadcastTransactionsCommand +class BroadcastTransactionsRequestFilterTestCase(BaseFilterTestCase): + filter_type = BroadcastTransactionsRequestFilter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(BroadcastTransactionsRequestFilterTestCase, self).setUp() + + # Define a few valid values that we can reuse across tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' - def test_happy_path(self): - """Successful invocation of `broadcastTransactions`.""" - self.adapter.response = { - 'trytes': ['BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'], + def test_pass_happy_path(self): + """The incoming request is valid.""" + request = { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], } - trytes = [ - # These values tend to get rather long, but for purposes of this - # test, we don't have to get too realistic. - TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'), - ] + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The incoming request contains values that can be converted into the + expected types. + """ + # Any values that can be converted into TryteStrings are accepted. + filter_ = self._filter({ + 'trytes': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + # The values are converted into TryteStrings so that they can be + # sent to the node. + { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ) + + def test_fail_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) - self.assertCommandSuccess( - expected_response = { - 'trytes': trytes, + def test_fail_trytes_null(self): + """`trytes` is null.""" + self.assertFilterErrors( + { + 'trytes': None, }, - request = { - 'trytes': trytes, + { + 'trytes': [f.Required.CODE_EMPTY], }, ) - def test_compatible_types(self): - """ - Invoking `broadcastTransactions` with parameters that can be - converted into the correct types. - """ - self.adapter.response = { - 'trytes': ['BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'], - } + def test_fail_trytes_wrong_type(self): + """`trytes` is not an array.""" + self.assertFilterErrors( + { + # `trytes` has to be an array, even if there's only one + # TryteString. + 'trytes': TryteString(self.trytes1), + }, + + { + 'trytes': [f.Type.CODE_WRONG_TYPE], + }, + ) - self.assertCommandSuccess( - expected_response = { + def test_fail_trytes_empty(self): + """`trytes` is an array, but it's empty.""" + self.assertFilterErrors( + { + 'trytes': [], + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_trytes_contents_invalid(self): + """`trytes` is an array, but it contains invalid values.""" + self.assertFilterErrors( + { 'trytes': [ - TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXH'), + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, ], }, - request = { + { + 'trytes.0': [f.NotEmpty.CODE_EMPTY], + 'trytes.1': [f.Type.CODE_WRONG_TYPE], + 'trytes.2': [f.Type.CODE_WRONG_TYPE], + 'trytes.3': [f.Required.CODE_EMPTY], + 'trytes.4': [Trytes.CODE_NOT_TRYTES], + 'trytes.6': [f.Type.CODE_WRONG_TYPE], + }, + ) + + +class BroadcastTransactionsResponseFilterTestCase(BaseFilterTestCase): + filter_type = BroadcastTransactionsResponseFilter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(BroadcastTransactionsResponseFilterTestCase, self).setUp() + + # Define a few valid values here that we can reuse across multiple + # tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_pass_happy_path(self): + """The incoming response contains valid values.""" + # Responses from the node arrive as strings. + filter_ = self._filter({ + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + self.assertFilterPasses(filter_) + + # The filter converts them into TryteStrings. + self.assertDictEqual( + filter_.cleaned_data, + + { 'trytes': [ - b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQFCUSDXZHOFH', + TryteString(self.trytes1), + TryteString(self.trytes2), ], - } + }, ) - def test_error_trytes_invalid(self): + def test_pass_correct_types(self): """ - Attempting to call `broadcastTransactions` but `trytes` is invalid. + The incoming response already contains correct types. + + This scenario is highly unusual, but who's complaining? """ - with self.assertRaises(FilterError): - # This won't work; `trytes` has to be an array. - self.command( - trytes = TryteString(b'BYSWEAUTWXHXZ9YBZISEK9LUHWGMHXCGEVNZHRLUWQF'), - ) - - with self.assertRaises(FilterError): - # Seriously, you haven't figured this out yet? - self.command(trytes=['not a valid tryte string', 42]) - - with self.assertRaises(FilterError): - # Got everything set up, but nothing to broadcast? - # Welcome to your first YouTube channel! - self.command(trytes=[]) + response = { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ] + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, response) + From 3605b19c59dac57bba3ec27defc6d782ef86e7e1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 18:45:01 -0500 Subject: [PATCH 050/239] Made `getNodeInfoTests` less insane. --- iota/commands/add_neighbors.py | 4 ++ iota/commands/broadcast_transactions.py | 4 ++ iota/commands/get_node_info.py | 35 ++++++------ test/commands/add_neighbors_test.py | 15 +++++ test/commands/attach_to_tangle_test.py | 36 ++++++------ test/commands/broadcast_transactions_test.py | 15 +++++ test/commands/get_node_info_test.py | 59 ++++++++++++++++---- 7 files changed, 120 insertions(+), 48 deletions(-) diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index b83f212..4f9608d 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -7,6 +7,10 @@ from iota.commands import FilterCommand, RequestFilter from iota.filters import NodeUri +__all__ = [ + 'AddNeighborsCommand', +] + class AddNeighborsCommand(FilterCommand): """ diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index a693f56..aeba8e0 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -7,6 +7,10 @@ from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes +__all__ = [ + 'BroadcastTransactionsCommand', +] + class BroadcastTransactionsCommand(FilterCommand): """ diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index 7dd1879..73c4977 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -4,7 +4,7 @@ import filters as f -from iota.commands import FilterCommand +from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes __all__ = [ @@ -21,25 +21,24 @@ class GetNodeInfoCommand(FilterCommand): command = 'getNodeInfo' def get_request_filter(self): + return GetNodeInfoRequestFilter() + + def get_response_filter(self): + return GetNodeInfoResponseFilter() + + +class GetNodeInfoRequestFilter(RequestFilter): + def __init__(self): # `getNodeInfo` does not accept any parameters. # Using a filter here just to enforce that the request is empty. - return f.FilterMapper( - { - }, - - allow_extra_keys = False, - allow_missing_keys = False, - ) + super(GetNodeInfoRequestFilter, self).__init__({}) - def get_response_filter(self): - return f.FilterMapper( - { - 'latestMilestone': f.ByteString(encoding='ascii') | Trytes, - 'latestSolidSubtangleMilestone': - f.ByteString(encoding='ascii') | Trytes, - }, +class GetNodeInfoResponseFilter(ResponseFilter): + def __init__(self): + super(GetNodeInfoResponseFilter, self).__init__({ + 'latestMilestone': f.ByteString(encoding='ascii') | Trytes, - allow_extra_keys = True, - allow_missing_keys = True, - ) + 'latestSolidSubtangleMilestone': + f.ByteString(encoding='ascii') | Trytes, + }) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index f7dfc38..169576e 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -37,6 +37,21 @@ def test_fail_empty(self): }, ) + def test_fail_unexpected_parameters(self): + """The incoming request contains unexpected parameters.""" + self.assertFilterErrors( + { + 'uris': ['udp://localhost'], + + # I've never seen that before in my life, officer. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + def test_fail_neighbors_wrong_type(self): """`neighbors` is not an array.""" self.assertFilterErrors( diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 2d739bf..4f69bfa 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -128,6 +128,24 @@ def test_fail_empty(self): }, ) + def test_fail_unexpected_parameters(self): + """The incoming request contains unexpected parameters.""" + self.assertFilterErrors( + { + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + 'min_weight_magnitude': 20, + 'trytes': [TryteString(self.trytes1)], + + # Hey, how'd that get in there? + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + def test_fail_trunk_transaction_null(self): """`trunk_transaction` is null.""" self.assertFilterErrors( @@ -353,21 +371,3 @@ def test_pass_happy_path(self): ], }, ) - - def test_pass_correct_types(self): - """ - The incoming response already contains correct types. - - This scenario is highly unusual, but who's complaining? - """ - response = { - 'trytes': [ - TryteString(self.trytes1), - TryteString(self.trytes2), - ] - } - - filter_ = self._filter(response) - - self.assertFilterPasses(filter_) - self.assertDictEqual(filter_.cleaned_data, response) diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index 4de0383..8b259b9 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -76,6 +76,21 @@ def test_fail_empty(self): }, ) + def test_fail_unexpected_parameters(self): + """The incoming value contains unexpected parameters.""" + self.assertFilterErrors( + { + 'trytes': [TryteString(self.trytes1)], + + # Alright buddy, let's see some ID. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + def test_fail_trytes_null(self): """`trytes` is null.""" self.assertFilterErrors( diff --git a/test/commands/get_node_info_test.py b/test/commands/get_node_info_test.py index 6e7b756..d07d00c 100644 --- a/test/commands/get_node_info_test.py +++ b/test/commands/get_node_info_test.py @@ -2,18 +2,48 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from iota.commands.get_node_info import GetNodeInfoCommand +import filters as f +from filters.test import BaseFilterTestCase + +from iota.commands.get_node_info import GetNodeInfoRequestFilter, GetNodeInfoResponseFilter from iota.types import TryteString -from test.commands import BaseFilterCommandTestCase -# noinspection SpellCheckingInspection -class GetNodeInfoCommandTestCase(BaseFilterCommandTestCase): - command_type = GetNodeInfoCommand +class GetNodeInfoRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetNodeInfoRequestFilter + skip_value_check = True + + def test_pass_empty(self): + """The incoming response is (correctly) empty.""" + request = {} + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_fail_unexpected_parameters(self): + """The incoming response contains unexpected parameters.""" + self.assertFilterErrors( + { + # All you had to do was nothing! How did you screw that up?! + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + +class GetNodeInfoResponseFilterTestCase(BaseFilterTestCase): + filter_type = GetNodeInfoResponseFilter + skip_value_check = True - def test_happy_path(self): - """Successful invocation of `getNodeInfo`.""" - self.adapter.response = { + # noinspection SpellCheckingInspection + def test_pass_happy_path(self): + """The incoming response contains valid values.""" + response = { 'appName': 'IRI', 'appVersion': '1.0.8.nu', 'duration': 1, @@ -38,8 +68,13 @@ def test_happy_path(self): 'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', } - self.assertCommandSuccess( - expected_response = { + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { 'appName': 'IRI', 'appVersion': '1.0.8.nu', 'duration': 1, @@ -64,7 +99,7 @@ def test_happy_path(self): 'latestSolidSubtangleMilestone': TryteString( b'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJR' - b'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999' + b'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', ), - } + }, ) From 10d140acea5ac0ba84cdfc5f257c26d02d7ac3ab Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 3 Dec 2016 19:36:21 -0500 Subject: [PATCH 051/239] Created skeleton for `findTransactions`. --- iota/api.py | 7 +- iota/commands/find_transactions.py | 40 +++++++++++ test/commands/__init__.py | 43 ------------ test/commands/find_transactions_test.py | 92 +++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 44 deletions(-) create mode 100644 iota/commands/find_transactions.py create mode 100644 test/commands/find_transactions_test.py diff --git a/iota/api.py b/iota/api.py index cbe17c7..e5025bf 100644 --- a/iota/api.py +++ b/iota/api.py @@ -123,7 +123,12 @@ def find_transactions( :see: https://iota.readme.io/docs/findtransactions """ - raise NotImplementedError('Not implemented yet.') + return self.findTransactions( + bundles = bundles, + addresses = addresses, + tags = tags, + approvees = approvees, + ) def get_balances(self, addresses, threshold=100): # type: (Iterable[Text], int) -> dict diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py new file mode 100644 index 0000000..5ef9fad --- /dev/null +++ b/iota/commands/find_transactions.py @@ -0,0 +1,40 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter, ResponseFilter + +__all__ = [ + 'FindTransactionsCommand', +] + + +class FindTransactionsCommand(FilterCommand): + """ + Executes `findTransactions` command. + + :see: iota.IotaApi.find_transactions + """ + def get_request_filter(self): + return FindTransactionsRequestFilter() + + def get_response_filter(self): + return FindTransactionsResponseFilter() + + +class FindTransactionsRequestFilter(RequestFilter): + def __init__(self): + super(FindTransactionsRequestFilter, self).__init__( + { + }, + + # Technically, all of the parameters for this command are + # optional, so long as at least one of them is present and not + # empty. + allow_missing_keys = True, + ) + + +class FindTransactionsResponseFilter(ResponseFilter): + def __init__(self): + super(FindTransactionsResponseFilter, self).__init__({}) diff --git a/test/commands/__init__.py b/test/commands/__init__.py index 4aa5665..3f3d02d 100644 --- a/test/commands/__init__.py +++ b/test/commands/__init__.py @@ -1,46 +1,3 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ unicode_literals - -from pprint import pformat -from typing import Optional -from unittest import TestCase - -from iota.commands import FilterCommand, FilterError -from test import MockAdapter - - -class BaseCommandTestCase(TestCase): - command_type = None - - def setUp(self): - super(BaseCommandTestCase, self).setUp() - - self.adapter = MockAdapter() - self.command = self.command_type(self.adapter) # type: FilterCommand - - def assertCommandSuccess(self, expected_response, request=None): - # type: (dict, Optional[dict]) -> None - """ - Sends the command to the adapter and expects a successful result. - """ - request = request or {} # type: dict - response = self.command(**request) - - self.assertDictEqual(response, expected_response) - - -class BaseFilterCommandTestCase(BaseCommandTestCase): - def assertCommandSuccess(self, expected_response, request=None): - # type: (dict, Optional[dict]) -> None - """ - Sends the command to the adapter and expects a successful result. - """ - try: - super(BaseFilterCommandTestCase, self)\ - .assertCommandSuccess(expected_response, request) - except FilterError as e: - self.fail('{exc}\n\n{errors}'.format( - exc = e, - errors = pformat(e.context['filter_errors']), - )) diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py new file mode 100644 index 0000000..29e10cd --- /dev/null +++ b/test/commands/find_transactions_test.py @@ -0,0 +1,92 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from filters.test import BaseFilterTestCase + +from iota.commands.find_transactions import FindTransactionsRequestFilter + + +class FindTransactionsRequestFilterTestCase(BaseFilterTestCase): + filter_type = FindTransactionsRequestFilter + skip_value_check = True + + def test_pass_all_parameters(self): + """The request contains valid values for all parameters.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_bundles_only(self): + """The request only includes bundles.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_addresses_only(self): + """The request only includes addresses.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_tags_only(self): + """The request only includes tags.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_approvees_only(self): + """The request only includes approvees.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_empty(self): + """The request does not contain any parameters.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_all_parameters_empty(self): + """The request contains all parameters, but every one is empty.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """The request contains unexpected parameters.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_bundles_wrong_type(self): + """`bundles` is not an array.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_bundles_contents_invalid(self): + """`bundles` is an array, but it contains invalid values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_addresses_wrong_type(self): + """`addresses` is not an array.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_addresses_contents_invalid(self): + """`addresses` is an array, but it contains invalid values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_tags_wrong_type(self): + """`tags` is not an array.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_tags_contents_invalid(self): + """`tags` is an array, but it contains invalid values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_approvees_wrong_type(self): + """`approvees` is not an array.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_approvees_contents_invalid(self): + """`approvees` is an array, but it contains invalid values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') From 087cc941f8443c63b51fe5497ea460fca79c236e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 19:16:47 -0500 Subject: [PATCH 052/239] Added support for exception responses. --- iota/adapter.py | 9 +++++---- test/adapter_test.py | 29 ++++++++++++++++++++++++++--- 2 files changed, 31 insertions(+), 7 deletions(-) diff --git a/iota/adapter.py b/iota/adapter.py index 0ac3938..8f07d87 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -188,10 +188,11 @@ def send_request(self, payload, **kwargs): raise BadApiResponse('Non-JSON response from node: ' + raw_content) try: - # Response always has 200 status, even for errors, so the only way - # to check for success is to inspect the response body. - # :see: https://github.com/iotaledger/iri/issues/9 - error = decoded.get('error') + # Response always has 200 status, even for errors/exceptions, so the + # only way to check for success is to inspect the response body. + # :see:`https://github.com/iotaledger/iri/issues/9` + # :see:`https://github.com/iotaledger/iri/issues/12` + error = decoded.get('exception') or decoded.get('error') except AttributeError: raise BadApiResponse('Invalid response from node: ' + raw_content) diff --git a/test/adapter_test.py b/test/adapter_test.py index 3eb9a44..3ba06a2 100644 --- a/test/adapter_test.py +++ b/test/adapter_test.py @@ -89,7 +89,7 @@ def test_configure_path(self): def test_configure_custom_path_default_port(self): """ Configuring HttpAdapter to use a custom path but implicitly use - default port. + default port. """ adapter = HttpAdapter.configure('http://iotatoken.com/node') @@ -138,7 +138,7 @@ def test_configure_error_non_numeric_port(self): def test_success_response(self): """ Simulates sending a command to the node and getting a success - response. + response. """ adapter = HttpAdapter('localhost') @@ -158,7 +158,7 @@ def test_success_response(self): def test_error_response(self): """ Simulates sending a command to the node and getting an error - response. + response. """ adapter = HttpAdapter('localhost') @@ -178,6 +178,29 @@ def test_error_response(self): self.assertEqual(text(context.exception), expected_result) + def test_exception_response(self): + """ + Simulates sending a command to the node and getting an exception + response. + """ + adapter = HttpAdapter('localhost') + + expected_result = 'java.lang.ArrayIndexOutOfBoundsException: 4' + + mocked_response = self._create_response(json.dumps({ + 'exception': 'java.lang.ArrayIndexOutOfBoundsException: 4', + 'duration': 16 + })) + + mocked_sender = Mock(return_value=mocked_response) + + # noinspection PyUnresolvedReferences + with patch.object(adapter, '_send_http_request', mocked_sender): + with self.assertRaises(BadApiResponse) as context: + adapter.send_request({'command': 'helloWorld'}) + + self.assertEqual(text(context.exception), expected_result) + def test_empty_response(self): """The response is empty.""" adapter = HttpAdapter('localhost') From f59a340cbb846466fa59182171c7f2ff25d36409 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 19:29:52 -0500 Subject: [PATCH 053/239] Happy path for `findTransactions` request. --- iota/api.py | 9 ++-- iota/commands/find_transactions.py | 4 ++ iota/types.py | 61 +++++++++++++++++++------ test/commands/find_transactions_test.py | 39 +++++++++++++++- test/types_test.py | 44 +++++++++++++++++- 5 files changed, 135 insertions(+), 22 deletions(-) diff --git a/iota/api.py b/iota/api.py index e5025bf..3c52319 100644 --- a/iota/api.py +++ b/iota/api.py @@ -6,7 +6,7 @@ from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry -from iota.types import TransactionId, TryteString +from iota.types import Address, Tag, TransactionId, TryteString __all__ = [ 'IotaApi', @@ -104,7 +104,7 @@ def find_transactions( tags = None, approvees = None, ): - # type: (Optional[Iterable[Text]], Optional[Iterable[Text]], Optional[Iterable[Text]], Optional[Iterable[Text]]) -> dict + # type: (Optional[Iterable[TransactionId]], Optional[Iterable[Address]], Optional[Iterable[Tag]], Optional[Iterable[TransactionId]]) -> dict """ Find the transactions which match the specified input and return. @@ -115,11 +115,10 @@ def find_transactions( Using multiple of these input fields returns the intersection of the values. - :param bundles: List of bundle hashes. The hashes will be extended - to 81 trytes if necessary. + :param bundles: List of transaction IDs. :param addresses: List of addresses. :param tags: List of tags. Each tag must be 27 trytes. - :param approvees: List of approvee transaction hashes. + :param approvees: List of approvee transaction IDs. :see: https://iota.readme.io/docs/findtransactions """ diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index 5ef9fad..f0ccc20 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -26,6 +26,10 @@ class FindTransactionsRequestFilter(RequestFilter): def __init__(self): super(FindTransactionsRequestFilter, self).__init__( { + 'addresses': None, + 'approvees': None, + 'bundles': None, + 'tags': None, }, # Technically, all of the parameters for this command are diff --git a/iota/types.py b/iota/types.py index 6c1f526..ee27d8b 100644 --- a/iota/types.py +++ b/iota/types.py @@ -10,20 +10,23 @@ from iota import TrytesCodec +TrytesCompatible = Union[binary_type, bytearray, 'TryteString'] + + class TryteString(object): """ A string representation of a sequence of trytes. A trit can be thought of as the ternary version of a bit. It can - have one of three values: 1, 0 or unknown. + have one of three values: 1, 0 or unknown. A tryte can be thought of as the ternary version of a byte. It is a - sequence of 3 trits. + sequence of 3 trits. A tryte string is similar in concept to Python's byte string, except - it has a more limited alphabet. Byte strings are limited to ASCII - (256 possible values), while the tryte string alphabet only has - 27 characters (one for each possible tryte configuration). + it has a more limited alphabet. Byte strings are limited to ASCII + (256 possible values), while the tryte string alphabet only has 27 + characters (one for each possible tryte configuration). """ @classmethod def from_bytes(cls, bytes_): @@ -32,15 +35,15 @@ def from_bytes(cls, bytes_): return cls(encode(bytes_, 'trytes')) def __init__(self, trytes, pad=None): - # type: (Union[binary_type, bytearray, TryteString], int) -> None + # type: (TrytesCompatible, int) -> None """ :param trytes: Byte string or bytearray. :param pad: Ensure at least this many trytes. - If there are too few, additional Tryte([-1, -1, -1]) values - will be appended to the TryteString. + If there are too few, additional ``Tryte([-1, -1, -1])`` values + will be appended to the TryteString. Note: If the TryteString is too long, it will _not_ be - truncated! + truncated! """ super(TryteString, self).__init__() @@ -87,7 +90,7 @@ def __bytes__(self): Converts the TryteString into a string representation. Note that this method will NOT convert the trytes back into bytes; - use `as_bytes` for that. + use :py:method:`as_bytes` for that. """ return binary_type(self.trytes) @@ -118,7 +121,7 @@ def as_bytes(self, errors='strict'): return decode(self.trytes, 'trytes', errors) def __eq__(self, other): - # type: (Union[TryteString, binary_type, bytearray]) -> bool + # type: (TrytesCompatible) -> bool if isinstance(other, TryteString): return self.trytes == other.trytes elif isinstance(other, (binary_type, bytearray)): @@ -134,14 +137,44 @@ def __eq__(self, other): # :bc: In Python 2 this must be defined explicitly. def __ne__(self, other): - # type: (Union[TryteString, binary_type, bytearray]) -> bool + # type: (TrytesCompatible) -> bool return not (self == other) +class Address(TryteString): + """ + A TryteString that acts as an address, with support for generating + and validating checksums. + """ + LEN = 81 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Address, self).__init__(trytes, pad=self.LEN) + + if len(self.trytes) > self.LEN: + raise ValueError('Addresses must be 81 trytes long.') + + +class Tag(TryteString): + """A TryteString that acts as a transaction tag.""" + LEN = 27 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Tag, self).__init__(trytes, pad=self.LEN) + + if len(self.trytes) > self.LEN: + raise ValueError('Tags must be 27 trytes long.') + + class TransactionId(TryteString): """A TryteString that acts as a transaction ID.""" + LEN = 81 + def __init__(self, trytes): - super(TransactionId, self).__init__(trytes, pad=81) + # type: (TrytesCompatible) -> None + super(TransactionId, self).__init__(trytes, pad=self.LEN) - if len(self.trytes) > 81: + if len(self.trytes) > self.LEN: raise ValueError('TransactionIds must be 81 trytes long.') diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index 29e10cd..cbfa02a 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -5,16 +5,51 @@ from filters.test import BaseFilterTestCase from iota.commands.find_transactions import FindTransactionsRequestFilter +from iota.types import Address, Tag, TransactionId class FindTransactionsRequestFilterTestCase(BaseFilterTestCase): filter_type = FindTransactionsRequestFilter skip_value_check = True + # noinspection SpellCheckingInspection + def setUp(self): + super(FindTransactionsRequestFilterTestCase, self).setUp() + + # Define a few valid values that we can reuse across tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + self.trytes3 = b'999999999999999999999999999' + def test_pass_all_parameters(self): """The request contains valid values for all parameters.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'bundles': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + + 'addresses': [ + Address(self.trytes1), + Address(self.trytes2), + ], + + 'tags': [ + Tag(self.trytes1), + Tag(self.trytes3), + ], + + 'approvees': [ + TransactionId(self.trytes1), + TransactionId(self.trytes3), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_bundles_only(self): """The request only includes bundles.""" diff --git a/test/types_test.py b/test/types_test.py index e7d5ff2..c6249d2 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -7,7 +7,7 @@ from six import binary_type from iota import TrytesDecodeError -from iota.types import TransactionId, TryteString +from iota.types import Address, Tag, TransactionId, TryteString # noinspection SpellCheckingInspection @@ -227,6 +227,48 @@ def test_as_bytes_non_ascii_errors_replace(self): b'??\xd2\x80??\xc3??', ) + +# noinspection SpellCheckingInspection +class AddressTestCase(TestCase): + def test_init_automatic_pad(self): + """Addresses are automatically padded to 81 trytes.""" + txn = Address( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' + ) + + self.assertEqual( + txn.trytes, + + # Note the extra 9's added to the end. + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + def test_init_error_too_long(self): + """Attempting to create an address longer than 81 trytes.""" + with self.assertRaises(ValueError): + Address( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' + ) + + +# noinspection SpellCheckingInspection +class TagTestCase(TestCase): + def test_init_automatic_pad(self): + """Tags are automatically padded to 27 trytes.""" + tag = Tag(b'COLOREDCOINS') + + self.assertEqual(tag.trytes, b'COLOREDCOINS999999999999999') + + def test_init_error_too_long(self): + """Attempting to create a tag longer than 27 trytes.""" + with self.assertRaises(ValueError): + # 28 chars = no va. + Tag(b'COLOREDCOINS9999999999999999') + + # noinspection SpellCheckingInspection class TransactionIdTestCase(TestCase): def test_init_automatic_pad(self): From 4bc3fc4c207614edf4aba8c716dc042592ac4909 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 19:34:34 -0500 Subject: [PATCH 054/239] Type conversion for `findTransactions` request. --- iota/commands/find_transactions.py | 27 ++++++++++-- test/commands/find_transactions_test.py | 57 ++++++++++++++++++++++++- 2 files changed, 79 insertions(+), 5 deletions(-) diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index f0ccc20..bb7f694 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -2,7 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f + from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.filters import Trytes +from iota.types import Address, Tag, TransactionId __all__ = [ 'FindTransactionsCommand', @@ -26,10 +30,25 @@ class FindTransactionsRequestFilter(RequestFilter): def __init__(self): super(FindTransactionsRequestFilter, self).__init__( { - 'addresses': None, - 'approvees': None, - 'bundles': None, - 'tags': None, + 'addresses': ( + f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=Address)) + ), + + 'approvees': ( + f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + ), + + 'bundles': ( + f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + ), + + 'tags': ( + f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=Tag)) + ), }, # Technically, all of the parameters for this command are diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index cbfa02a..22c8496 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -3,6 +3,7 @@ unicode_literals from filters.test import BaseFilterTestCase +from six import binary_type from iota.commands.find_transactions import FindTransactionsRequestFilter from iota.types import Address, Tag, TransactionId @@ -25,7 +26,7 @@ def setUp(self): def test_pass_all_parameters(self): """The request contains valid values for all parameters.""" request = { - 'bundles': [ + 'bundles': [ TransactionId(self.trytes1), TransactionId(self.trytes2), ], @@ -51,6 +52,60 @@ def test_pass_all_parameters(self): self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, request) + def test_pass_compatible_types(self): + """ + The request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + 'bundles': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + + 'addresses': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + + 'tags': [ + binary_type(self.trytes1), + bytearray(self.trytes3), + ], + + 'approvees': [ + binary_type(self.trytes1), + bytearray(self.trytes3), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'bundles': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + + 'addresses': [ + Address(self.trytes1), + Address(self.trytes2), + ], + + 'tags': [ + Tag(self.trytes1), + Tag(self.trytes3), + ], + + 'approvees': [ + TransactionId(self.trytes1), + TransactionId(self.trytes3), + ], + }, + ) + def test_pass_bundles_only(self): """The request only includes bundles.""" # :todo: Implement test. From 25d1384ceec7d7e885906950312541d119224e04 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 20:15:30 -0500 Subject: [PATCH 055/239] Defaults for `findTransactions` params. --- iota/commands/find_transactions.py | 4 + test/commands/find_transactions_test.py | 104 ++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 8 deletions(-) diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index bb7f694..f4a4a91 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -33,21 +33,25 @@ def __init__(self): 'addresses': ( f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Address)) + | f.Optional(default=[]) ), 'approvees': ( f.Array | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + | f.Optional(default=[]) ), 'bundles': ( f.Array | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + | f.Optional(default=[]) ), 'tags': ( f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Tag)) + | f.Optional(default=[]) ), }, diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index 22c8496..8fd0a84 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -108,23 +108,111 @@ def test_pass_compatible_types(self): def test_pass_bundles_only(self): """The request only includes bundles.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'bundles': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'bundles': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + + 'addresses': [], + 'approvees': [], + 'tags': [], + }, + ) def test_pass_addresses_only(self): """The request only includes addresses.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'addresses': [ + Address(self.trytes1), + Address(self.trytes2), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'addresses': [ + Address(self.trytes1), + Address(self.trytes2), + ], + + 'approvees': [], + 'bundles': [], + 'tags': [], + }, + ) def test_pass_tags_only(self): """The request only includes tags.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'tags': [ + Tag(self.trytes1), + Tag(self.trytes3), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'tags': [ + Tag(self.trytes1), + Tag(self.trytes3), + ], + + 'addresses': [], + 'approvees': [], + 'bundles': [], + }, + ) def test_pass_approvees_only(self): """The request only includes approvees.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'approvees': [ + TransactionId(self.trytes1), + TransactionId(self.trytes3), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'approvees': [ + TransactionId(self.trytes1), + TransactionId(self.trytes3), + ], + + 'addresses': [], + 'bundles': [], + 'tags': [], + }, + ) def test_fail_empty(self): """The request does not contain any parameters.""" From 571b30b71229e7cb1e8092a1f8e64dc1a2b92e11 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 20:19:27 -0500 Subject: [PATCH 056/239] `findTransactions` requires at least 1 search term. --- iota/commands/find_transactions.py | 23 +++++++++++++++++++++++ test/commands/find_transactions_test.py | 9 +++++++-- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index f4a4a91..38dcdbb 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -27,6 +27,12 @@ def get_response_filter(self): class FindTransactionsRequestFilter(RequestFilter): + CODE_NO_SEARCH_VALUES = 'no_search_values' + + templates = { + CODE_NO_SEARCH_VALUES: 'No search values specified.', + } + def __init__(self): super(FindTransactionsRequestFilter, self).__init__( { @@ -61,6 +67,23 @@ def __init__(self): allow_missing_keys = True, ) + def _apply(self, value): + value = super(FindTransactionsRequestFilter, self)._apply(value) # type: dict + + if self._has_errors: + return value + + # At least one search term is required. + if not any(( + value['addresses'], + value['approvees'], + value['bundles'], + value['tags'], + )): + return self._invalid_value(value, self.CODE_NO_SEARCH_VALUES) + + return value + class FindTransactionsResponseFilter(ResponseFilter): def __init__(self): diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index 8fd0a84..eee7fad 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -216,8 +216,13 @@ def test_pass_approvees_only(self): def test_fail_empty(self): """The request does not contain any parameters.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + {}, + + { + '': [FindTransactionsRequestFilter.CODE_NO_SEARCH_VALUES], + }, + ) def test_fail_all_parameters_empty(self): """The request contains all parameters, but every one is empty.""" From 92d127c18fdf448546108c32ab21d9ba813ceaf2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 20:24:10 -0500 Subject: [PATCH 057/239] Fleshed out some unit tests. --- test/commands/find_transactions_test.py | 32 +++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index eee7fad..38b07ba 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f from filters.test import BaseFilterTestCase from six import binary_type @@ -226,13 +227,36 @@ def test_fail_empty(self): def test_fail_all_parameters_empty(self): """The request contains all parameters, but every one is empty.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'addresses': [], + 'approvees': [], + 'bundles': [], + 'tags': [], + }, + + { + '': [FindTransactionsRequestFilter.CODE_NO_SEARCH_VALUES], + }, + ) def test_fail_unexpected_parameters(self): """The request contains unexpected parameters.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'addresses': [Address(self.trytes1)], + 'approvees': [TransactionId(self.trytes1)], + 'bundles': [TransactionId(self.trytes1)], + 'tags': [Tag(self.trytes1)], + + # Hey, you're not allowed in he-argh! + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_bundles_wrong_type(self): """`bundles` is not an array.""" From 4e38f11983ccafe11f0d88be47ef302235f5907e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 20:29:24 -0500 Subject: [PATCH 058/239] Implemented remaining tests for `findTransactions` request. --- test/commands/find_transactions_test.py | 169 +++++++++++++++++++++--- 1 file changed, 151 insertions(+), 18 deletions(-) diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index 38b07ba..683bf2e 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -4,10 +4,11 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type +from six import binary_type, text_type from iota.commands.find_transactions import FindTransactionsRequestFilter -from iota.types import Address, Tag, TransactionId +from iota.filters import Trytes +from iota.types import Address, Tag, TransactionId, TryteString class FindTransactionsRequestFilterTestCase(BaseFilterTestCase): @@ -260,40 +261,172 @@ def test_fail_unexpected_parameters(self): def test_fail_bundles_wrong_type(self): """`bundles` is not an array.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'bundles': TransactionId(self.trytes1), + }, + + { + 'bundles': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_bundles_contents_invalid(self): """`bundles` is an array, but it contains invalid values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'bundles': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'bundles.0': [f.Required.CODE_EMPTY], + 'bundles.1': [f.Type.CODE_WRONG_TYPE], + 'bundles.2': [f.Type.CODE_WRONG_TYPE], + 'bundles.3': [f.Required.CODE_EMPTY], + 'bundles.4': [Trytes.CODE_NOT_TRYTES], + 'bundles.6': [f.Type.CODE_WRONG_TYPE], + 'bundles.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) def test_fail_addresses_wrong_type(self): """`addresses` is not an array.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'addresses': Address(self.trytes1), + }, + + { + 'addresses': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_addresses_contents_invalid(self): """`addresses` is an array, but it contains invalid values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'addresses': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'addresses.0': [f.Required.CODE_EMPTY], + 'addresses.1': [f.Type.CODE_WRONG_TYPE], + 'addresses.2': [f.Type.CODE_WRONG_TYPE], + 'addresses.3': [f.Required.CODE_EMPTY], + 'addresses.4': [Trytes.CODE_NOT_TRYTES], + 'addresses.6': [f.Type.CODE_WRONG_TYPE], + 'addresses.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) def test_fail_tags_wrong_type(self): """`tags` is not an array.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'tags': Tag(self.trytes1), + }, + + { + 'tags': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_tags_contents_invalid(self): """`tags` is an array, but it contains invalid values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'tags': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes1), + + 2130706433, + b'9' * 28, + ], + }, + + { + 'tags.0': [f.Required.CODE_EMPTY], + 'tags.1': [f.Type.CODE_WRONG_TYPE], + 'tags.2': [f.Type.CODE_WRONG_TYPE], + 'tags.3': [f.Required.CODE_EMPTY], + 'tags.4': [Trytes.CODE_NOT_TRYTES], + 'tags.6': [f.Type.CODE_WRONG_TYPE], + 'tags.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) def test_fail_approvees_wrong_type(self): """`approvees` is not an array.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'approvees': TransactionId(self.trytes1), + }, + + { + 'approvees': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_approvees_contents_invalid(self): """`approvees` is an array, but it contains invalid values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'approvees': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'approvees.0': [f.Required.CODE_EMPTY], + 'approvees.1': [f.Type.CODE_WRONG_TYPE], + 'approvees.2': [f.Type.CODE_WRONG_TYPE], + 'approvees.3': [f.Required.CODE_EMPTY], + 'approvees.4': [Trytes.CODE_NOT_TRYTES], + 'approvees.6': [f.Type.CODE_WRONG_TYPE], + 'approvees.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) From 7e88900b2a0bc47a7c393250c41217512e3ebb5c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 20:36:48 -0500 Subject: [PATCH 059/239] Implemented `findTransactions` response filter. --- iota/commands/find_transactions.py | 4 +- test/commands/attach_to_tangle_test.py | 8 ++- test/commands/find_transactions_test.py | 66 ++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 4 deletions(-) diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index 38dcdbb..a2f15a4 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -87,4 +87,6 @@ def _apply(self, value): class FindTransactionsResponseFilter(ResponseFilter): def __init__(self): - super(FindTransactionsResponseFilter, self).__init__({}) + super(FindTransactionsResponseFilter, self).__init__({ + 'hashes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), + }) diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 4f69bfa..48fa190 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -350,24 +350,28 @@ def setUp(self): def test_pass_happy_path(self): """The incoming response contains valid values.""" - # Responses from the node arrive as strings. filter_ = self._filter({ + # Trytes arrive from the node as strings. 'trytes': [ text_type(self.trytes1, 'ascii'), text_type(self.trytes2, 'ascii'), ], + + 'duration': 42, }) self.assertFilterPasses(filter_) - # The filter converts them into TryteStrings. self.assertDictEqual( filter_.cleaned_data, { + # The filter converts them into TryteStrings. 'trytes': [ TryteString(self.trytes1), TryteString(self.trytes2), ], + + 'duration': 42, }, ) diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index 683bf2e..31fc0d7 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -6,7 +6,8 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type -from iota.commands.find_transactions import FindTransactionsRequestFilter +from iota.commands.find_transactions import FindTransactionsRequestFilter, \ + FindTransactionsResponseFilter from iota.filters import Trytes from iota.types import Address, Tag, TransactionId, TryteString @@ -430,3 +431,66 @@ def test_fail_approvees_contents_invalid(self): 'approvees.7': [Trytes.CODE_WRONG_FORMAT], }, ) + + +class FindTransactionsResponseFilterTestCase(BaseFilterTestCase): + filter_type = FindTransactionsResponseFilter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(FindTransactionsResponseFilterTestCase, self).setUp() + + # Define a few valid values here that we can reuse across multiple + # tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_no_results(self): + """The incoming response contains no hashes.""" + response = { + 'hashes': [], + 'duration': 42, + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, response) + + # noinspection SpellCheckingInspection + def test_search_results(self): + """The incoming response contains lots of hashes.""" + filter_ = self._filter({ + 'hashes': [ + 'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFW' + 'YWZRE9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA', + + 'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' + 'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', + ], + + 'duration': 42, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'hashes': [ + Address( + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFW' + b'YWZRE9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA', + ), + + Address( + b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' + b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', + ), + ], + + 'duration': 42, + }, + ) From 0d28fc19080ba18ed3f94188421bd9ddb69ec699 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 21:03:22 -0500 Subject: [PATCH 060/239] Added address validation to `getBalances`. --- iota/api.py | 7 +- iota/commands/add_neighbors.py | 2 +- iota/commands/attach_to_tangle.py | 2 +- iota/commands/broadcast_transactions.py | 2 +- iota/commands/find_transactions.py | 2 +- iota/commands/get_balances.py | 52 ++++++ iota/commands/get_node_info.py | 2 +- test/commands/get_balances_test.py | 216 ++++++++++++++++++++++++ 8 files changed, 278 insertions(+), 7 deletions(-) create mode 100644 iota/commands/get_balances.py create mode 100644 test/commands/get_balances_test.py diff --git a/iota/api.py b/iota/api.py index 3c52319..cf1e157 100644 --- a/iota/api.py +++ b/iota/api.py @@ -130,7 +130,7 @@ def find_transactions( ) def get_balances(self, addresses, threshold=100): - # type: (Iterable[Text], int) -> dict + # type: (Iterable[Address], int) -> dict """ Similar to `get_inclusion_states`. Returns the confirmed balance which a list of addresses have at the latest confirmed milestone. @@ -146,7 +146,10 @@ def get_balances(self, addresses, threshold=100): :see: https://iota.readme.io/docs/getbalances """ - raise NotImplementedError('Not implemented yet.') + return self.getBalances( + addresses = addresses, + threshold = threshold, + ) def get_inclusion_states(self, transactions, tips): # type: (Iterable[Text], Iterable[Text]) -> dict diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index 4f9608d..0b71b63 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -16,7 +16,7 @@ class AddNeighborsCommand(FilterCommand): """ Executes `addNeighbors` command. - :see: iota.IotaApi.add_neighbors + See :py:method:`iota.api.IotaApi.add_neighbors`. """ command = 'addNeighbors' diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index 9471599..f9f697d 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -17,7 +17,7 @@ class AttachToTangleCommand(FilterCommand): """ Executes `attachToTangle` command. - :see: iota.IotaApi.attach_to_tangle + See :py:method:`iota.api.IotaApi.attach_to_tangle`. """ command = 'attachToTangle' diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index aeba8e0..19d1554 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -16,7 +16,7 @@ class BroadcastTransactionsCommand(FilterCommand): """ Executes `broadcastTransactions` command. - :see: iota.IotaApi.broadcast_transactions + See :py:method:`iota.api.IotaApi.broadcast_transactions`. """ command = 'broadcastTransactions' diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index a2f15a4..4a14a2b 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -17,7 +17,7 @@ class FindTransactionsCommand(FilterCommand): """ Executes `findTransactions` command. - :see: iota.IotaApi.find_transactions + See :py:method:`iota.api.IotaApi.find_transactions`. """ def get_request_filter(self): return FindTransactionsRequestFilter() diff --git a/iota/commands/get_balances.py b/iota/commands/get_balances.py new file mode 100644 index 0000000..8152270 --- /dev/null +++ b/iota/commands/get_balances.py @@ -0,0 +1,52 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f + +from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.filters import Trytes +from iota.types import Address + +__all__ = [ + 'GetBalancesCommand', +] + + +class GetBalancesCommand(FilterCommand): + """ + Executes `getBalances` command. + + See :py:method:`iota.api.IotaApi.get_balances`. + """ + def get_request_filter(self): + return GetBalancesRequestFilter() + + def get_response_filter(self): + return GetBalancesResponseFilter() + + +class GetBalancesRequestFilter(RequestFilter): + def __init__(self): + super(GetBalancesRequestFilter, self).__init__( + { + 'addresses': ( + f.Required + | f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=Address)) + ), + + 'threshold': f.Optional(default=100), + }, + + allow_missing_keys = { + 'threshold', + }, + ) + + +class GetBalancesResponseFilter(ResponseFilter): + def __init__(self): + super(GetBalancesResponseFilter, self).__init__({ + + }) diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index 73c4977..64add79 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -16,7 +16,7 @@ class GetNodeInfoCommand(FilterCommand): """ Executes `getNodeInfo` command. - :see: iota.IotaApi.get_node_info + See :py:method:`iota.api.IotaApi.get_node_info`. """ command = 'getNodeInfo' diff --git a/test/commands/get_balances_test.py b/test/commands/get_balances_test.py new file mode 100644 index 0000000..624692b --- /dev/null +++ b/test/commands/get_balances_test.py @@ -0,0 +1,216 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase +from six import binary_type, text_type + +from iota.commands.get_balances import GetBalancesRequestFilter, \ + GetBalancesResponseFilter +from iota.filters import Trytes +from iota.types import Address, TryteString + + +class GetBalancesRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetBalancesRequestFilter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(GetBalancesRequestFilterTestCase, self).setUp() + + # Define a few valid values that we can reuse across tests. + self.trytes1 = ( + b'ORLSCIMM9ZONOUSPYYWLOEMXQZLYEHCBEDQSHZ' + b'OGOPZCZCDZYTDPGEEUXWUZ9FQYCT9OGS9PICOOX' + ) + + self.trytes2 = ( + b'HHKUSTHZPUPONLCHXUGFYEHATTMFOSSHEUHYS' + b'ZUKBODYHZM99IR9KOXLZXVUOJM9LQKCQJBWMTY' + ) + + def test_pass_happy_path(self): + """Typical invocation of `getBalances`.""" + request = { + 'addresses': [ + Address(self.trytes1), + Address(self.trytes2), + ], + + 'threshold': 80, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The incoming request contains values that can be converted to the + expected types. + """ + request = { + 'addresses': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + + 'threshold': 80, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'addresses': [ + Address(self.trytes1), + Address(self.trytes2), + ], + + 'threshold': 80, + }, + ) + + def test_pass_threshold_optional(self): + """ + The incoming request does not contain a `threshold` value, so the + default value is assumed. + """ + request = { + 'addresses': [Address(self.trytes1)], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'addresses': [Address(self.trytes1)], + 'threshold': 100, + }, + ) + + def test_fail_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'addresses': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """The incoming request contains unexpected parameters.""" + self.assertFilterErrors( + { + 'addresses': [Address(self.trytes1)], + + # I've had a perfectly wonderful evening. + # But this wasn't it. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_addresses_wrong_type(self): + """`addresses` is not an array.""" + self.assertFilterErrors( + { + 'addresses': Address(self.trytes1), + }, + + { + 'addresses': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_addresses_empty(self): + """`addresses` is an array, but it's empty.""" + self.assertFilterErrors( + { + 'addresses': [], + }, + + { + 'addresses': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_addresses_contents_invalid(self): + """`addresses` is an array, but it contains invalid values.""" + self.assertFilterErrors( + { + 'addresses': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'addresses.0': [f.Required.CODE_EMPTY], + 'addresses.1': [f.Type.CODE_WRONG_TYPE], + 'addresses.2': [f.Type.CODE_WRONG_TYPE], + 'addresses.3': [f.Required.CODE_EMPTY], + 'addresses.4': [Trytes.CODE_NOT_TRYTES], + 'addresses.6': [f.Type.CODE_WRONG_TYPE], + 'addresses.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) + + def test_fail_threshold_float(self): + """`threshold` is a float.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_threshold_string(self): + """`threshold` is a string.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_threshold_too_small(self): + """`threshold` is less than 0.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_threshold_too_big(self): + """`threshold` is greater than 100.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + +class GetBalancesResponseFilterTestCase(BaseFilterTestCase): + filter_type = GetBalancesResponseFilter + + def test_no_balances(self): + """Incoming response contains no balances.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_all_balances(self): + """ + Incoming response contains balances for all requested addresses. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') From 2f27060608e8ccd8b5b0460ae80c6c49569b1577 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 21:10:05 -0500 Subject: [PATCH 061/239] Added some missing tests. --- test/commands/add_neighbors_test.py | 24 ++++++++++++++++++------ test/commands/attach_to_tangle_test.py | 15 +++++++++++++++ 2 files changed, 33 insertions(+), 6 deletions(-) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index 169576e..c22afa5 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -52,8 +52,20 @@ def test_fail_unexpected_parameters(self): }, ) - def test_fail_neighbors_wrong_type(self): - """`neighbors` is not an array.""" + def test_fail_neighbors_null(self): + """`uris` is null.""" + self.assertFilterErrors( + { + 'uris': None, + }, + + { + 'uris': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_uris_wrong_type(self): + """`uris` is not an array.""" self.assertFilterErrors( { # Nope; it's gotta be an array, even if you only want to add @@ -66,8 +78,8 @@ def test_fail_neighbors_wrong_type(self): }, ) - def test_fail_neighbors_empty(self): - """`neighbors` is an array, but it's empty.""" + def test_fail_uris_empty(self): + """`uris` is an array, but it's empty.""" self.assertFilterErrors( { # Insert "Forever Alone" meme here. @@ -79,9 +91,9 @@ def test_fail_neighbors_empty(self): }, ) - def test_fail_neighbors_contents_invalid(self): + def test_fail_uris_contents_invalid(self): """ - `neighbors` is an array, but it contains invalid values. + `uris` is an array, but it contains invalid values. """ self.assertFilterErrors( { diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 48fa190..2b4d798 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -267,6 +267,21 @@ def test_fail_min_weight_magnitude_too_small(self): }, ) + def test_fail_trytes_null(self): + """`trytes` is null.""" + self.assertFilterErrors( + { + 'trytes': None, + + 'trunk_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionId(self.txn_id), + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + def test_fail_trytes_wrong_type(self): """`trytes` is not an array.""" self.assertFilterErrors( From 8175737a3ba7ced0d06167e104ffacd916ac9f6e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 21:13:28 -0500 Subject: [PATCH 062/239] Added threshold validation to `getBalances`. --- iota/commands/get_balances.py | 7 +++- test/commands/get_balances_test.py | 53 +++++++++++++++++++++++++----- 2 files changed, 51 insertions(+), 9 deletions(-) diff --git a/iota/commands/get_balances.py b/iota/commands/get_balances.py index 8152270..5855860 100644 --- a/iota/commands/get_balances.py +++ b/iota/commands/get_balances.py @@ -36,7 +36,12 @@ def __init__(self): | f.FilterRepeater(f.Required | Trytes(result_type=Address)) ), - 'threshold': f.Optional(default=100), + 'threshold': ( + f.Type(int) + | f.Min(0) + | f.Max(100) + | f.Optional(default=100) + ), }, allow_missing_keys = { diff --git a/test/commands/get_balances_test.py b/test/commands/get_balances_test.py index 624692b..8d78d5e 100644 --- a/test/commands/get_balances_test.py +++ b/test/commands/get_balances_test.py @@ -181,23 +181,60 @@ def test_fail_addresses_contents_invalid(self): def test_fail_threshold_float(self): """`threshold` is a float.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Even with an empty fpart, floats are not accepted. + 'threshold': 86.0, + + 'addresses': [Address(self.trytes1)], + }, + + { + 'threshold': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_threshold_string(self): """`threshold` is a string.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'threshold': '86', + + 'addresses': [Address(self.trytes1)], + }, + + { + 'threshold': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_threshold_too_small(self): """`threshold` is less than 0.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'threshold': -1, + + 'addresses': [Address(self.trytes1)], + }, + + { + 'threshold': [f.Min.CODE_TOO_SMALL], + }, + ) def test_fail_threshold_too_big(self): """`threshold` is greater than 100.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'threshold': 101, + + 'addresses': [Address(self.trytes1)], + }, + + { + 'threshold': [f.Max.CODE_TOO_BIG], + }, + ) class GetBalancesResponseFilterTestCase(BaseFilterTestCase): From aac70f1e3cba06e071656fc07648406434fdbc69 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 21:17:29 -0500 Subject: [PATCH 063/239] Impl'd `getBalances` response filter. --- iota/commands/get_balances.py | 3 +- test/commands/get_balances_test.py | 59 ++++++++++++++++++++++++++++-- 2 files changed, 57 insertions(+), 5 deletions(-) diff --git a/iota/commands/get_balances.py b/iota/commands/get_balances.py index 5855860..589da0b 100644 --- a/iota/commands/get_balances.py +++ b/iota/commands/get_balances.py @@ -53,5 +53,6 @@ def __init__(self): class GetBalancesResponseFilter(ResponseFilter): def __init__(self): super(GetBalancesResponseFilter, self).__init__({ - + 'milestone': + f.ByteString(encoding='ascii') | Trytes(result_type=Address), }) diff --git a/test/commands/get_balances_test.py b/test/commands/get_balances_test.py index 8d78d5e..3e10feb 100644 --- a/test/commands/get_balances_test.py +++ b/test/commands/get_balances_test.py @@ -239,15 +239,66 @@ def test_fail_threshold_too_big(self): class GetBalancesResponseFilterTestCase(BaseFilterTestCase): filter_type = GetBalancesResponseFilter + skip_value_check = True + # noinspection SpellCheckingInspection def test_no_balances(self): """Incoming response contains no balances.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'balances': [], + 'duration': 42, + 'milestoneIndex': 128, + + 'milestone': + 'INRTUYSZCWBHGFGGXXPWRWBZACYAFGVRRP9VYEQJ' + 'OHYD9URMELKWAFYFMNTSP9MCHLXRGAFMBOZPZ9999', + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'balances': [], + 'duration': 42, + 'milestoneIndex': 128, + + 'milestone': + Address( + b'INRTUYSZCWBHGFGGXXPWRWBZACYAFGVRRP9VYEQJ' + b'OHYD9URMELKWAFYFMNTSP9MCHLXRGAFMBOZPZ9999', + ) + } + ) + # noinspection SpellCheckingInspection def test_all_balances(self): """ Incoming response contains balances for all requested addresses. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'balances': [114544444, 8175737], + 'duration': 42, + 'milestoneIndex': 128, + + 'milestone': + 'INRTUYSZCWBHGFGGXXPWRWBZACYAFGVRRP9VYEQJ' + 'OHYD9URMELKWAFYFMNTSP9MCHLXRGAFMBOZPZ9999', + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'balances': [114544444, 8175737], + 'duration': 42, + 'milestoneIndex': 128, + + 'milestone': + Address( + b'INRTUYSZCWBHGFGGXXPWRWBZACYAFGVRRP9VYEQJ' + b'OHYD9URMELKWAFYFMNTSP9MCHLXRGAFMBOZPZ9999', + ) + } + ) From b70d70e0ab03fe79b8b0a984c6dd7fb010718ae4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 5 Dec 2016 21:22:11 -0500 Subject: [PATCH 064/239] Stubbed out documentation files. --- .gitignore | 3 + docs/Makefile | 20 ++++++ docs/conf.py | 161 +++++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 20 ++++++ docs/make.bat | 36 +++++++++++ 5 files changed, 240 insertions(+) create mode 100644 docs/Makefile create mode 100644 docs/conf.py create mode 100644 docs/index.rst create mode 100644 docs/make.bat diff --git a/.gitignore b/.gitignore index 56ad972..5e15aa4 100644 --- a/.gitignore +++ b/.gitignore @@ -11,6 +11,9 @@ PyOTA.egg-info/* # :see: https://tox.readthedocs.io/en/latest/ .tox/* +# Generated documentation files. +docs/_build + # # Note: For environment- or IDE-specific metadata (e.g., .DS_Store, .idea, etc. # you can add these to your own "global" .gitignore file. diff --git a/docs/Makefile b/docs/Makefile new file mode 100644 index 0000000..cbab78c --- /dev/null +++ b/docs/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +SPHINXPROJ = PyOTA +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) \ No newline at end of file diff --git a/docs/conf.py b/docs/conf.py new file mode 100644 index 0000000..3be81fd --- /dev/null +++ b/docs/conf.py @@ -0,0 +1,161 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +# +# PyOTA documentation build configuration file, created by +# sphinx-quickstart on Mon Dec 5 21:20:31 2016. +# +# This file is execfile()d with the current directory set to its +# containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +# import os +# import sys +# sys.path.insert(0, os.path.abspath('.')) + + +# -- General configuration ------------------------------------------------ + +# If your documentation needs a minimal Sphinx version, state it here. +# +# needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', +] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix(es) of source filenames. +# You can specify multiple suffix as a list of string: +# +# source_suffix = ['.rst', '.md'] +source_suffix = '.rst' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = 'PyOTA' +copyright = '2016, IOTA Foundation' +author = 'Phoenix Zerin' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +from iota import __version__ +# The short X.Y version. +version = __version__ +# The full version, including alpha/beta/rc tags. +release = __version__ + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +# +# This is also used if you do content translation via gettext catalogs. +# Usually you set "language" from the command line for these cases. +language = None + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This patterns also effect to html_static_path and html_extra_path +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# If true, `todo` and `todoList` produce output, else they produce nothing. +todo_include_todos = False + + +# -- Options for HTML output ---------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'alabaster' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +# +# html_theme_options = {} + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + + +# -- Options for HTMLHelp output ------------------------------------------ + +# Output file base name for HTML help builder. +htmlhelp_basename = 'PyOTAdoc' + + +# -- Options for LaTeX output --------------------------------------------- + +latex_elements = { + # The paper size ('letterpaper' or 'a4paper'). + # + # 'papersize': 'letterpaper', + + # The font size ('10pt', '11pt' or '12pt'). + # + # 'pointsize': '10pt', + + # Additional stuff for the LaTeX preamble. + # + # 'preamble': '', + + # Latex figure (float) alignment + # + # 'figure_align': 'htbp', +} + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, +# author, documentclass [howto, manual, or own class]). +latex_documents = [ + (master_doc, 'PyOTA.tex', 'PyOTA Documentation', + 'Phoenix Zerin', 'manual'), +] + + +# -- Options for manual page output --------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + (master_doc, 'pyota', 'PyOTA Documentation', + [author], 1) +] + + +# -- Options for Texinfo output ------------------------------------------- + +# Grouping the document tree into Texinfo files. List of tuples +# (source start file, target name, title, author, +# dir menu entry, description, category) +texinfo_documents = [ + (master_doc, 'PyOTA', 'PyOTA Documentation', + author, 'PyOTA', 'One line description of project.', + 'Miscellaneous'), +] + + + diff --git a/docs/index.rst b/docs/index.rst new file mode 100644 index 0000000..3db97a5 --- /dev/null +++ b/docs/index.rst @@ -0,0 +1,20 @@ +.. PyOTA documentation master file, created by + sphinx-quickstart on Mon Dec 5 21:20:31 2016. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PyOTA's documentation! +================================= + +.. toctree:: + :maxdepth: 2 + :caption: Contents: + + + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/docs/make.bat b/docs/make.bat new file mode 100644 index 0000000..6675022 --- /dev/null +++ b/docs/make.bat @@ -0,0 +1,36 @@ +@ECHO OFF + +pushd %~dp0 + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set SOURCEDIR=. +set BUILDDIR=_build +set SPHINXPROJ=PyOTA + +if "%1" == "" goto help + +%SPHINXBUILD% >NUL 2>NUL +if errorlevel 9009 ( + echo. + echo.The 'sphinx-build' command was not found. Make sure you have Sphinx + echo.installed, then set the SPHINXBUILD environment variable to point + echo.to the full path of the 'sphinx-build' executable. Alternatively you + echo.may add the Sphinx directory to PATH. + echo. + echo.If you don't have Sphinx installed, grab it from + echo.http://sphinx-doc.org/ + exit /b 1 +) + +%SPHINXBUILD% -M %1 %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% +goto end + +:help +%SPHINXBUILD% -M help %SOURCEDIR% %BUILDDIR% %SPHINXOPTS% + +:end +popd From 83aef743a46707e4138ff9f9c6a361abab3541e1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 18:26:53 -0500 Subject: [PATCH 065/239] Filled in missing attribute. --- iota/commands/find_transactions.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index 4a14a2b..019ae45 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -19,6 +19,8 @@ class FindTransactionsCommand(FilterCommand): See :py:method:`iota.api.IotaApi.find_transactions`. """ + command = 'findTransactions' + def get_request_filter(self): return FindTransactionsRequestFilter() From f9adc9bc512f1d6c23647c0677300332a6950c52 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 18:47:41 -0500 Subject: [PATCH 066/239] Stubbed out `getInclusionStates`. --- iota/api.py | 7 +- iota/commands/get_inclusion_states.py | 32 +++++ test/commands/get_inclusion_states_test.py | 152 +++++++++++++++++++++ test/types_test.py | 4 +- 4 files changed, 191 insertions(+), 4 deletions(-) create mode 100644 iota/commands/get_inclusion_states.py create mode 100644 test/commands/get_inclusion_states_test.py diff --git a/iota/api.py b/iota/api.py index cf1e157..0612a6a 100644 --- a/iota/api.py +++ b/iota/api.py @@ -152,7 +152,7 @@ def get_balances(self, addresses, threshold=100): ) def get_inclusion_states(self, transactions, tips): - # type: (Iterable[Text], Iterable[Text]) -> dict + # type: (Iterable[TransactionId], Iterable[TransactionId]) -> dict """ Get the inclusion states of a set of transactions. This is for determining if a transaction was accepted and confirmed by the @@ -166,7 +166,10 @@ def get_inclusion_states(self, transactions, tips): :see: https://iota.readme.io/docs/getinclusionstates """ - raise NotImplementedError('Not implemented yet.') + return self.getInclusionStates( + transactions = transactions, + tips = tips, + ) def get_neighbors(self): # type: () -> dict diff --git a/iota/commands/get_inclusion_states.py b/iota/commands/get_inclusion_states.py new file mode 100644 index 0000000..f3a9b51 --- /dev/null +++ b/iota/commands/get_inclusion_states.py @@ -0,0 +1,32 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter + +__all__ = [ + 'GetInclusionStatesCommand', +] + + +class GetInclusionStatesCommand(FilterCommand): + """ + Executes ``getInclusionStates`` command. + + See :py:method:`iota.api.IotaApi.get_inclusion_states`. + """ + command = 'getInclusionStates' + + def get_request_filter(self): + pass + + def get_response_filter(self): + pass + + +class GetInclusionStatesRequestFilter(RequestFilter): + def __init__(self): + super(GetInclusionStatesRequestFilter, self).__init__({ + 'transactions': None, + 'tips': None, + }) diff --git a/test/commands/get_inclusion_states_test.py b/test/commands/get_inclusion_states_test.py new file mode 100644 index 0000000..be4f884 --- /dev/null +++ b/test/commands/get_inclusion_states_test.py @@ -0,0 +1,152 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase +from six import binary_type + +from iota.commands.get_inclusion_states import GetInclusionStatesRequestFilter +from iota.types import TransactionId + + +class GetInclusionStatesRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetInclusionStatesRequestFilter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(GetInclusionStatesRequestFilterTestCase, self).setUp() + + self.trytes1 = ( + b'QHBYXQWRAHQJZEIARWSQGZJTAIITOZRMBFICIPAV' + b'D9YRJMXFXBDPFDTRAHHHP9YPDUVTNOFWZGFGWMYHE' + ) + + self.trytes2 = ( + b'ZIJGAJ9AADLRPWNCYNNHUHRRAC9QOUDATEDQUMTN' + b'OTABUVRPTSTFQDGZKFYUUIE9ZEBIVCCXXXLKX9999' + ) + + def test_pass_happy_path(self): + """Typical `getInclusionStates` request.""" + request = { + 'transactions': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + + 'tips': [ + # These values would normally be different from + # ``transactions``, but for purposes of this unit test, we just + # need to make sure the format is correct. + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The request contains values that can be converted to expected + types. + """ + filter_ = self._filter({ + 'transactions': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + + 'tips': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'transactions': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + + 'tips': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + }, + ) + + def test_fail_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'transactions': [f.FilterMapper.CODE_MISSING_KEY], + 'tips': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """The incoming request contains unexpected parameters.""" + self.assertFilterErrors( + { + 'transactions': [TransactionId(self.trytes1)], + 'tips': [TransactionId(self.trytes2)], + + # I bring scientists, you bring a rock star. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_transactions_null(self): + """`transactions` is null.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transactions_wrong_type(self): + """`transactions` is not an array.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transactions_empty(self): + """`transactions` is an array, but it is empty.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transactions_contents_invalid(self): + """`transactions` is an array, but it contains invalid values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_tips_null(self): + """`tips` is null""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_tips_wrong_type(self): + """`tips` is not an array.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_tips_empty(self): + """`tips` is an array, but it is empty.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_tips_contents_invalid(self): + """`tips` is an array, but it contains invalid values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') diff --git a/test/types_test.py b/test/types_test.py index c6249d2..62e6c2b 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -232,13 +232,13 @@ def test_as_bytes_non_ascii_errors_replace(self): class AddressTestCase(TestCase): def test_init_automatic_pad(self): """Addresses are automatically padded to 81 trytes.""" - txn = Address( + addy = Address( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' ) self.assertEqual( - txn.trytes, + addy.trytes, # Note the extra 9's added to the end. b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' From 9a04ed1f496473c17a7fe50a965cf0a6760b8958 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 18:51:32 -0500 Subject: [PATCH 067/239] Added validation to `transactions`. --- iota/commands/get_inclusion_states.py | 4 +- test/commands/get_inclusion_states_test.py | 78 +++++++++++++++++++--- 2 files changed, 71 insertions(+), 11 deletions(-) diff --git a/iota/commands/get_inclusion_states.py b/iota/commands/get_inclusion_states.py index f3a9b51..1c8738a 100644 --- a/iota/commands/get_inclusion_states.py +++ b/iota/commands/get_inclusion_states.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f + from iota.commands import FilterCommand, RequestFilter __all__ = [ @@ -27,6 +29,6 @@ def get_response_filter(self): class GetInclusionStatesRequestFilter(RequestFilter): def __init__(self): super(GetInclusionStatesRequestFilter, self).__init__({ - 'transactions': None, + 'transactions': f.Required | f.Array, 'tips': None, }) diff --git a/test/commands/get_inclusion_states_test.py b/test/commands/get_inclusion_states_test.py index be4f884..b23e8ed 100644 --- a/test/commands/get_inclusion_states_test.py +++ b/test/commands/get_inclusion_states_test.py @@ -4,10 +4,11 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type +from six import binary_type, text_type from iota.commands.get_inclusion_states import GetInclusionStatesRequestFilter -from iota.types import TransactionId +from iota.filters import Trytes +from iota.types import TransactionId, TryteString class GetInclusionStatesRequestFilterTestCase(BaseFilterTestCase): @@ -113,23 +114,80 @@ def test_fail_unexpected_parameters(self): def test_fail_transactions_null(self): """`transactions` is null.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transactions': None, + + 'tips': [TransactionId(self.trytes2)], + }, + + { + 'transactions': [f.Required.CODE_EMPTY], + }, + ) def test_fail_transactions_wrong_type(self): """`transactions` is not an array.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Has to be an array, even if we're only querying for one + # transaction. + 'transactions': TransactionId(self.trytes1), + + 'tips': [TransactionId(self.trytes2)], + }, + + { + 'transactions': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_transactions_empty(self): """`transactions` is an array, but it is empty.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transactions': [], + + 'tips': [TransactionId(self.trytes2)], + }, + + { + 'transactions': [f.Required.CODE_EMPTY], + }, + ) def test_fail_transactions_contents_invalid(self): """`transactions` is an array, but it contains invalid values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.failureException( + { + 'transactions': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes1), + + 2130706433, + b'9' * 82, + ], + + 'tips': [TransactionId(self.trytes2)], + }, + + { + 'transactions.0': [f.Required.CODE_EMPTY], + 'transactions.1': [f.Type.CODE_WRONG_TYPE], + 'transactions.2': [f.Type.CODE_WRONG_TYPE], + 'transactions.3': [f.Required.CODE_EMPTY], + 'transactions.4': [Trytes.CODE_NOT_TRYTES], + 'transactions.6': [f.Type.CODE_WRONG_TYPE], + 'transactions.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) def test_fail_tips_null(self): """`tips` is null""" From 689371a7cd50c96511be646caf0233a313ab78f2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 18:55:24 -0500 Subject: [PATCH 068/239] Finished validation for `getInclusionStates` request. --- iota/commands/get_inclusion_states.py | 15 ++++- test/commands/get_inclusion_states_test.py | 73 +++++++++++++++++++--- 2 files changed, 77 insertions(+), 11 deletions(-) diff --git a/iota/commands/get_inclusion_states.py b/iota/commands/get_inclusion_states.py index 1c8738a..46b9688 100644 --- a/iota/commands/get_inclusion_states.py +++ b/iota/commands/get_inclusion_states.py @@ -5,6 +5,8 @@ import filters as f from iota.commands import FilterCommand, RequestFilter +from iota.filters import Trytes +from iota.types import TransactionId __all__ = [ 'GetInclusionStatesCommand', @@ -29,6 +31,15 @@ def get_response_filter(self): class GetInclusionStatesRequestFilter(RequestFilter): def __init__(self): super(GetInclusionStatesRequestFilter, self).__init__({ - 'transactions': f.Required | f.Array, - 'tips': None, + 'transactions': ( + f.Required + | f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + ), + + 'tips': ( + f.Required + | f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + ), }) diff --git a/test/commands/get_inclusion_states_test.py b/test/commands/get_inclusion_states_test.py index b23e8ed..4202a4d 100644 --- a/test/commands/get_inclusion_states_test.py +++ b/test/commands/get_inclusion_states_test.py @@ -158,7 +158,7 @@ def test_fail_transactions_empty(self): def test_fail_transactions_contents_invalid(self): """`transactions` is an array, but it contains invalid values.""" - self.failureException( + self.assertFilterErrors( { 'transactions': [ b'', @@ -191,20 +191,75 @@ def test_fail_transactions_contents_invalid(self): def test_fail_tips_null(self): """`tips` is null""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'tips': None, + + 'transactions': [TransactionId(self.trytes1)], + }, + + { + 'tips': [f.Required.CODE_EMPTY], + }, + ) def test_fail_tips_wrong_type(self): """`tips` is not an array.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'tips': TransactionId(self.trytes2), + + 'transactions': [TransactionId(self.trytes1)], + }, + + { + 'tips': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_tips_empty(self): """`tips` is an array, but it is empty.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'tips': [], + + 'transactions': [TransactionId(self.trytes1)], + }, + + { + 'tips': [f.Required.CODE_EMPTY], + }, + ) def test_fail_tips_contents_invalid(self): """`tips` is an array, but it contains invalid values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'tips': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes1), + + 2130706433, + b'9' * 82, + ], + + 'transactions': [TransactionId(self.trytes1)], + }, + + { + 'tips.0': [f.Required.CODE_EMPTY], + 'tips.1': [f.Type.CODE_WRONG_TYPE], + 'tips.2': [f.Type.CODE_WRONG_TYPE], + 'tips.3': [f.Required.CODE_EMPTY], + 'tips.4': [Trytes.CODE_NOT_TRYTES], + 'tips.6': [f.Type.CODE_WRONG_TYPE], + 'tips.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) From 7235429fdf01e8f55a86d73bec170269bf210f20 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:02:16 -0500 Subject: [PATCH 069/239] Added `BaseCommand.reset()`. --- iota/commands/__init__.py | 9 +++++++++ test/api_test.py | 27 +++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 7c0828e..e5c2803 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -95,6 +95,15 @@ def __call__(self, **kwargs): return self.response + def reset(self): + # type: () -> None + """ + Resets the command, allowing it to be called again. + """ + self.called = False + self.request = None # type: dict + self.response = None # type: dict + @abstract_method def _prepare_request(self, request): # type: (dict) -> Optional[dict] diff --git a/test/api_test.py b/test/api_test.py index ab586ba..ad668d5 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -59,6 +59,33 @@ def test_call_error_already_called(self): self.assertDictEqual(self.command.request, {'command': 'helloWorld'}) + def test_call_reset(self): + """Resetting a command allows it to be called more than once.""" + self.adapter.response = {'message': 'Hello, IOTA!'} + self.command() + + self.command.reset() + + self.assertFalse(self.command.called) + self.assertIsNone(self.command.request) + self.assertIsNone(self.command.response) + + expected_response = {'message': 'Welcome back!'} + self.adapter.response = expected_response + response = self.command(foo='bar') + + self.assertDictEqual(response, expected_response) + self.assertDictEqual(self.command.response, expected_response) + + self.assertDictEqual( + self.command.request, + + { + 'command': 'helloWorld', + 'foo': 'bar', + }, + ) + class IotaApiTestCase(TestCase): def test_init_with_uri(self): From 91f20b8c8ba18bf35b59ee04d9efbad46ec7fc5b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:08:40 -0500 Subject: [PATCH 070/239] Added validation to `getTips` response. --- iota/commands/get_inclusion_states.py | 2 +- iota/commands/get_tips.py | 37 ++++++++++++++ test/commands/get_tips_test.py | 71 +++++++++++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) create mode 100644 iota/commands/get_tips.py create mode 100644 test/commands/get_tips_test.py diff --git a/iota/commands/get_inclusion_states.py b/iota/commands/get_inclusion_states.py index 46b9688..d7b5bcf 100644 --- a/iota/commands/get_inclusion_states.py +++ b/iota/commands/get_inclusion_states.py @@ -22,7 +22,7 @@ class GetInclusionStatesCommand(FilterCommand): command = 'getInclusionStates' def get_request_filter(self): - pass + return GetInclusionStatesRequestFilter() def get_response_filter(self): pass diff --git a/iota/commands/get_tips.py b/iota/commands/get_tips.py new file mode 100644 index 0000000..94ca1be --- /dev/null +++ b/iota/commands/get_tips.py @@ -0,0 +1,37 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f + +from iota.commands import FilterCommand, ResponseFilter +from iota.filters import Trytes +from iota.types import Address + + +class GetTipsCommand(FilterCommand): + """ + Executes ``getTips`` command. + + See :py:method:`iota.api.IotaApi.get_tips`. + """ + command = 'getTips' + + def get_request_filter(self): + pass + + def get_response_filter(self): + return GetTipsResponseFilter() + + +class GetTipsResponseFilter(ResponseFilter): + def __init__(self): + super(GetTipsResponseFilter, self).__init__({ + 'hashes': ( + f.Array + | f.FilterRepeater( + f.ByteString(encoding='ascii') + | Trytes(result_type=Address) + ) + ), + }) diff --git a/test/commands/get_tips_test.py b/test/commands/get_tips_test.py new file mode 100644 index 0000000..1ae2e9d --- /dev/null +++ b/test/commands/get_tips_test.py @@ -0,0 +1,71 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from filters.test import BaseFilterTestCase + +from iota.commands.get_tips import GetTipsResponseFilter +from iota.types import Address + + +class GetTipsResponseFilterTestCase(BaseFilterTestCase): + filter_type = GetTipsResponseFilter + skip_value_check = True + + # noinspection SpellCheckingInspection + def test_pass_lots_of_hashes(self): + """The response contains lots of hashes.""" + response = { + 'hashes': [ + 'YVXJOEOP9JEPRQUVBPJMB9MGIB9OMTIJJLIUYPM9' + 'YBIWXPZ9PQCCGXYSLKQWKHBRVA9AKKKXXMXF99999', + + 'ZUMARCWKZOZRMJM9EEYJQCGXLHWXPRTMNWPBRCAG' + 'SGQNRHKGRUCIYQDAEUUEBRDBNBYHAQSSFZZQW9999', + + 'QLQECHDVQBMXKD9YYLBMGQLLIQ9PSOVDRLYCLLFM' + 'S9O99XIKCUHWAFWSTARYNCPAVIQIBTVJROOYZ9999', + ], + + 'duration': 4 + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'hashes': [ + Address( + b'YVXJOEOP9JEPRQUVBPJMB9MGIB9OMTIJJLIUYPM9' + b'YBIWXPZ9PQCCGXYSLKQWKHBRVA9AKKKXXMXF99999' + ), + + Address( + b'ZUMARCWKZOZRMJM9EEYJQCGXLHWXPRTMNWPBRCAG' + b'SGQNRHKGRUCIYQDAEUUEBRDBNBYHAQSSFZZQW9999' + ), + + Address( + b'QLQECHDVQBMXKD9YYLBMGQLLIQ9PSOVDRLYCLLFM' + b'S9O99XIKCUHWAFWSTARYNCPAVIQIBTVJROOYZ9999' + ), + ], + + 'duration': 4, + } + ) + + def test_pass_no_hashes(self): + """The response doesn't contain any hashes.""" + response = { + 'hashes': [], + 'duration': 4, + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, response) From 8c6a9bcaa1c0838136a5450200d81567ee320263 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:16:49 -0500 Subject: [PATCH 071/239] Stubbed out `getTransactionsToApprove`. --- iota/commands/get_transactions_to_approve.py | 34 ++++++++++++ .../get_transactions_to_approve_test.py | 55 +++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 iota/commands/get_transactions_to_approve.py create mode 100644 test/commands/get_transactions_to_approve_test.py diff --git a/iota/commands/get_transactions_to_approve.py b/iota/commands/get_transactions_to_approve.py new file mode 100644 index 0000000..1d81057 --- /dev/null +++ b/iota/commands/get_transactions_to_approve.py @@ -0,0 +1,34 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter, ResponseFilter + + +class GetTransactionsToApproveCommand(FilterCommand): + """ + Executes ``getTransactionsToApprove`` command. + + See :py:method:`iota.api.IotaApi.get_transactions_to_approve`. + """ + command = 'getTransactionsToApprove' + + def get_request_filter(self): + return GetTransactionsToApproveRequestFilter() + + def get_response_filter(self): + return GetTransactionsToApproveResponseFilter() + + +class GetTransactionsToApproveRequestFilter(RequestFilter): + def __init__(self): + super(GetTransactionsToApproveRequestFilter, self).__init__({ + + }) + + +class GetTransactionsToApproveResponseFilter(ResponseFilter): + def __init__(self): + super(GetTransactionsToApproveResponseFilter, self).__init__({ + + }) diff --git a/test/commands/get_transactions_to_approve_test.py b/test/commands/get_transactions_to_approve_test.py new file mode 100644 index 0000000..58ad25e --- /dev/null +++ b/test/commands/get_transactions_to_approve_test.py @@ -0,0 +1,55 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from filters.test import BaseFilterTestCase + +from iota.commands.get_transactions_to_approve import \ + GetTransactionsToApproveRequestFilter, GetTransactionsToApproveResponseFilter + + +class GetTransactionsToApproveRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetTransactionsToApproveRequestFilter + skip_value_check = True + + def test_pass_happy_path(self): + """Typical `getTransactionsToApprove` request.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_empty(self): + """ + Request is empty, so default values are used for all parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """Request contains unexpected parameters.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_float(self): + """`depth` is a float.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_string(self): + """`depth` is a string.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_too_small(self): + """`depth` is less than 1.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + +class GetTransactionsToApproveResponseFilterTestCase(BaseFilterTestCase): + filter_type = GetTransactionsToApproveResponseFilter + skip_value_check = True + + def test_pass_happy_path(self): + """Typical `getTransactionsToApprove` response.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') From 3e1817cb665ef56e1539a16530ee469d7ebec38f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:21:12 -0500 Subject: [PATCH 072/239] Added validation to `getTransactionsToApprove` request. --- iota/commands/get_transactions_to_approve.py | 4 +- .../get_transactions_to_approve_test.py | 73 +++++++++++++++---- 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/iota/commands/get_transactions_to_approve.py b/iota/commands/get_transactions_to_approve.py index 1d81057..c8c631f 100644 --- a/iota/commands/get_transactions_to_approve.py +++ b/iota/commands/get_transactions_to_approve.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f + from iota.commands import FilterCommand, RequestFilter, ResponseFilter @@ -23,7 +25,7 @@ def get_response_filter(self): class GetTransactionsToApproveRequestFilter(RequestFilter): def __init__(self): super(GetTransactionsToApproveRequestFilter, self).__init__({ - + 'depth': f.Type(int) | f.Min(1), }) diff --git a/test/commands/get_transactions_to_approve_test.py b/test/commands/get_transactions_to_approve_test.py index 58ad25e..07b4384 100644 --- a/test/commands/get_transactions_to_approve_test.py +++ b/test/commands/get_transactions_to_approve_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f from filters.test import BaseFilterTestCase from iota.commands.get_transactions_to_approve import \ @@ -14,35 +15,75 @@ class GetTransactionsToApproveRequestFilterTestCase(BaseFilterTestCase): def test_pass_happy_path(self): """Typical `getTransactionsToApprove` request.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'depth': 100, + } - def test_pass_empty(self): - """ - Request is empty, so default values are used for all parameters. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_fail_empty(self): + """Request is empty.""" + self.assertFilterErrors( + {}, + + { + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) def test_fail_unexpected_parameters(self): """Request contains unexpected parameters.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': 100, + + # I knew I should have taken that left turn at Albuquerque. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_depth_float(self): """`depth` is a float.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': 100.0, + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_depth_string(self): """`depth` is a string.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': '100', + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_depth_too_small(self): """`depth` is less than 1.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': 0, + }, + + { + 'depth': [f.Min.CODE_TOO_SMALL], + }, + ) class GetTransactionsToApproveResponseFilterTestCase(BaseFilterTestCase): From 4e8c2f91a0733fcf8eb238ba7832f88185c8a4d5 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:24:28 -0500 Subject: [PATCH 073/239] Added type conversion to `getTransactionsToApprove` response. --- iota/commands/get_transactions_to_approve.py | 12 +++++- .../get_transactions_to_approve_test.py | 38 ++++++++++++++++++- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/iota/commands/get_transactions_to_approve.py b/iota/commands/get_transactions_to_approve.py index c8c631f..381e2d9 100644 --- a/iota/commands/get_transactions_to_approve.py +++ b/iota/commands/get_transactions_to_approve.py @@ -5,6 +5,8 @@ import filters as f from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.filters import Trytes +from iota.types import TransactionId class GetTransactionsToApproveCommand(FilterCommand): @@ -32,5 +34,13 @@ def __init__(self): class GetTransactionsToApproveResponseFilter(ResponseFilter): def __init__(self): super(GetTransactionsToApproveResponseFilter, self).__init__({ - + 'branchTransaction': ( + f.ByteString(encoding='ascii') + | Trytes(result_type=TransactionId) + ), + + 'trunkTransaction': ( + f.ByteString(encoding='ascii') + | Trytes(result_type=TransactionId) + ), }) diff --git a/test/commands/get_transactions_to_approve_test.py b/test/commands/get_transactions_to_approve_test.py index 07b4384..b31d500 100644 --- a/test/commands/get_transactions_to_approve_test.py +++ b/test/commands/get_transactions_to_approve_test.py @@ -7,6 +7,7 @@ from iota.commands.get_transactions_to_approve import \ GetTransactionsToApproveRequestFilter, GetTransactionsToApproveResponseFilter +from iota.types import TransactionId class GetTransactionsToApproveRequestFilterTestCase(BaseFilterTestCase): @@ -90,7 +91,40 @@ class GetTransactionsToApproveResponseFilterTestCase(BaseFilterTestCase): filter_type = GetTransactionsToApproveResponseFilter skip_value_check = True + # noinspection SpellCheckingInspection def test_pass_happy_path(self): """Typical `getTransactionsToApprove` response.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + response = { + 'trunkTransaction': + 'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' + 'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999', + + 'branchTransaction': + 'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' + 'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999', + + 'duration': 936, + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'trunkTransaction': + TransactionId( + b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' + b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' + ), + + 'branchTransaction': + TransactionId( + b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' + b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' + ), + + 'duration': 936, + }, + ) From 767b5a42a072268b47b8832d05bb727848a9cc88 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:31:42 -0500 Subject: [PATCH 074/239] Enforce no parameters for `getTips` request. --- iota/commands/get_tips.py | 9 ++++++++- test/commands/get_tips_test.py | 30 +++++++++++++++++++++++++++++- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/iota/commands/get_tips.py b/iota/commands/get_tips.py index 94ca1be..ea00985 100644 --- a/iota/commands/get_tips.py +++ b/iota/commands/get_tips.py @@ -4,7 +4,7 @@ import filters as f -from iota.commands import FilterCommand, ResponseFilter +from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes from iota.types import Address @@ -24,6 +24,13 @@ def get_response_filter(self): return GetTipsResponseFilter() +class GetTipsRequestFilter(RequestFilter): + def __init__(self): + # `getTips` doesn't accept any parameters. + # Using a filter here just to enforce that the request is empty. + super(GetTipsRequestFilter, self).__init__({}) + + class GetTipsResponseFilter(ResponseFilter): def __init__(self): super(GetTipsResponseFilter, self).__init__({ diff --git a/test/commands/get_tips_test.py b/test/commands/get_tips_test.py index 1ae2e9d..da53f74 100644 --- a/test/commands/get_tips_test.py +++ b/test/commands/get_tips_test.py @@ -2,12 +2,40 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f from filters.test import BaseFilterTestCase -from iota.commands.get_tips import GetTipsResponseFilter +from iota.commands.get_tips import GetTipsRequestFilter, GetTipsResponseFilter from iota.types import Address +class GetTipsRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetTipsRequestFilter + skip_value_check = True + + def test_pass_empty(self): + """The incoming response is (correctly) empty.""" + request = {} + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_fail_unexpected_parameters(self): + """The incoming response contains unexpected parameters.""" + self.assertFilterErrors( + { + # All you had to do was nothing! How did you screw that up?! + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + class GetTipsResponseFilterTestCase(BaseFilterTestCase): filter_type = GetTipsResponseFilter skip_value_check = True From 7a2c779cfbf10ccc5893bb4cdd8a3e5cd7a73558 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:32:08 -0500 Subject: [PATCH 075/239] Wired up `getTips` request filter. --- iota/commands/get_tips.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/commands/get_tips.py b/iota/commands/get_tips.py index ea00985..e2baf9f 100644 --- a/iota/commands/get_tips.py +++ b/iota/commands/get_tips.py @@ -18,7 +18,7 @@ class GetTipsCommand(FilterCommand): command = 'getTips' def get_request_filter(self): - pass + return GetTipsRequestFilter() def get_response_filter(self): return GetTipsResponseFilter() From c03ff2b0e2a1fcb1d6633f9948c17699e92e6882 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:40:41 -0500 Subject: [PATCH 076/239] Fix error when sending trytes via HTTP. --- iota/adapter.py | 7 ++++++- iota/json.py | 15 +++++++++++++++ iota/types.py | 9 +++++++++ 3 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 iota/json.py diff --git a/iota/adapter.py b/iota/adapter.py index 8f07d87..e0323a0 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -12,6 +12,7 @@ from six import with_metaclass from iota import DEFAULT_PORT +from iota.json import JsonEncoder __all__ = [ 'BadApiResponse', @@ -210,4 +211,8 @@ def _send_http_request(self, payload, **kwargs): tests. """ kwargs.setdefault('timeout', get_default_timeout()) - return requests.post(self.node_url, json=payload, **kwargs) + + # Use a custom JSON encoder that knows how to convert Tryte values. + encoder = JsonEncoder() + + return requests.post(self.node_url, data=encoder.encode(payload), **kwargs) diff --git a/iota/json.py b/iota/json.py new file mode 100644 index 0000000..6336d95 --- /dev/null +++ b/iota/json.py @@ -0,0 +1,15 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + + +from json.encoder import JSONEncoder as BaseJsonEncoder + + +# noinspection PyClassHasNoInit +class JsonEncoder(BaseJsonEncoder): + """JSON encoder with support for custom types.""" + def default(self, o): + if hasattr(o, 'as_json'): + return o.as_json() + return super(JsonEncoder, self).default(o) diff --git a/iota/types.py b/iota/types.py index ee27d8b..0931ba0 100644 --- a/iota/types.py +++ b/iota/types.py @@ -120,6 +120,15 @@ def as_bytes(self, errors='strict'): # :bc: In Python 2, `decode` does not accept keyword arguments. return decode(self.trytes, 'trytes', errors) + def as_json(self): + # type: () -> Text + """ + Converts the TryteString into a JSON representation. + + See :py:class:`iota.json.JsonEncoder`. + """ + return self.trytes.decode('ascii') + def __eq__(self, other): # type: (TrytesCompatible) -> bool if isinstance(other, TryteString): From 9f671e8d72216e792bc78209a95969f64d2788c0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 6 Dec 2016 19:46:35 -0500 Subject: [PATCH 077/239] Added unit test for tryte->json conversion. --- iota/adapter.py | 14 +++++++------- test/adapter_test.py | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) diff --git a/iota/adapter.py b/iota/adapter.py index e0323a0..aff7f3f 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -176,7 +176,11 @@ def node_url(self): def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict - response = self._send_http_request(payload, **kwargs) + response = self._send_http_request( + # Use a custom JSON encoder that knows how to convert Tryte values. + payload = JsonEncoder().encode(payload), + **kwargs + ) raw_content = response.text if not raw_content: @@ -203,7 +207,7 @@ def send_request(self, payload, **kwargs): return decoded def _send_http_request(self, payload, **kwargs): - # type: (dict, dict) -> requests.Response + # type: (Text, dict) -> requests.Response """ Sends the actual HTTP request. @@ -211,8 +215,4 @@ def _send_http_request(self, payload, **kwargs): tests. """ kwargs.setdefault('timeout', get_default_timeout()) - - # Use a custom JSON encoder that knows how to convert Tryte values. - encoder = JsonEncoder() - - return requests.post(self.node_url, data=encoder.encode(payload), **kwargs) + return requests.post(self.node_url, data=payload, **kwargs) diff --git a/test/adapter_test.py b/test/adapter_test.py index 3ba06a2..0b2f04a 100644 --- a/test/adapter_test.py +++ b/test/adapter_test.py @@ -12,6 +12,7 @@ from iota import BadApiResponse, DEFAULT_PORT, InvalidUri from iota.adapter import HttpAdapter, resolve_adapter +from iota.types import TryteString class ResolveAdapterTestCase(TestCase): @@ -254,6 +255,40 @@ def test_non_object_response(self): 'Invalid response from node: ' + invalid_response, ) + # noinspection SpellCheckingInspection + def test_trytes_in_request(self): + """Sending a request that includes trytes.""" + adapter = HttpAdapter('localhost') + + # Response is not important for this test; we just need to make + # sure that the request is converted correctly. + mocked_sender = Mock(return_value=self._create_response('{}')) + + # noinspection PyUnresolvedReferences + with patch.object(adapter, '_send_http_request', mocked_sender): + adapter.send_request({ + 'command': 'helloWorld', + 'trytes': [ + TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA'), + + TryteString( + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA', + ), + ], + }) + + mocked_sender.assert_called_once_with( + payload = json.dumps({ + 'command': 'helloWorld', + + # Tryte sequences are converted to strings for transport. + 'trytes': [ + 'RBTC9D9DCDQAEASBYBCCKBFA', + 'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA', + ], + }), + ) + @staticmethod def _create_response(content): # type: (Text) -> requests.Response From 7eeb72f22f67ef659b24d2eb7d7d030eab6af7e1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 09:16:54 -0500 Subject: [PATCH 078/239] Improved command tests. Tests will now fail if the command class isn't wired up correctly. --- test/commands/add_neighbors_test.py | 5 +++-- test/commands/attach_to_tangle_test.py | 8 ++++---- test/commands/broadcast_transactions_test.py | 7 ++++--- test/commands/find_transactions_test.py | 7 ++++--- test/commands/get_balances_test.py | 8 ++++---- test/commands/get_inclusion_states_test.py | 5 +++-- test/commands/get_node_info_test.py | 7 ++++--- test/commands/get_tips_test.py | 7 ++++--- test/commands/get_transactions_to_approve_test.py | 9 ++++++--- 9 files changed, 36 insertions(+), 27 deletions(-) diff --git a/test/commands/add_neighbors_test.py b/test/commands/add_neighbors_test.py index c22afa5..da018fd 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/add_neighbors_test.py @@ -5,12 +5,13 @@ import filters as f from filters.test import BaseFilterTestCase -from iota.commands.add_neighbors import AddNeighborsRequestFilter +from iota.commands.add_neighbors import AddNeighborsCommand from iota.filters import NodeUri +from test import MockAdapter class AddNeighborsRequestFilterTestCase(BaseFilterTestCase): - filter_type = AddNeighborsRequestFilter + filter_type = AddNeighborsCommand(MockAdapter()).get_request_filter skip_value_check = True def test_pass_valid_request(self): diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 2b4d798..52cf181 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -6,14 +6,14 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type -from iota.commands.attach_to_tangle import AttachToTangleRequestFilter, \ - AttachToTangleResponseFilter +from iota.commands.attach_to_tangle import AttachToTangleCommand from iota.filters import Trytes from iota.types import TransactionId, TryteString +from test import MockAdapter class AttachToTangleRequestFilterTestCase(BaseFilterTestCase): - filter_type = AttachToTangleRequestFilter + filter_type = AttachToTangleCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection @@ -350,7 +350,7 @@ def test_fail_trytes_contents_invalid(self): class AttachToTangleResponseFilterTestCase(BaseFilterTestCase): - filter_type = AttachToTangleResponseFilter + filter_type = AttachToTangleCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index 8b259b9..d20aad6 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -7,13 +7,14 @@ from six import binary_type, text_type from iota.commands.broadcast_transactions import \ - BroadcastTransactionsRequestFilter, BroadcastTransactionsResponseFilter + BroadcastTransactionsCommand from iota.filters import Trytes from iota.types import TryteString +from test import MockAdapter class BroadcastTransactionsRequestFilterTestCase(BaseFilterTestCase): - filter_type = BroadcastTransactionsRequestFilter + filter_type = BroadcastTransactionsCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection @@ -160,7 +161,7 @@ def test_trytes_contents_invalid(self): class BroadcastTransactionsResponseFilterTestCase(BaseFilterTestCase): - filter_type = BroadcastTransactionsResponseFilter + filter_type = BroadcastTransactionsCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index 31fc0d7..d483491 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -7,13 +7,14 @@ from six import binary_type, text_type from iota.commands.find_transactions import FindTransactionsRequestFilter, \ - FindTransactionsResponseFilter + FindTransactionsCommand from iota.filters import Trytes from iota.types import Address, Tag, TransactionId, TryteString +from test import MockAdapter class FindTransactionsRequestFilterTestCase(BaseFilterTestCase): - filter_type = FindTransactionsRequestFilter + filter_type = FindTransactionsCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection @@ -434,7 +435,7 @@ def test_fail_approvees_contents_invalid(self): class FindTransactionsResponseFilterTestCase(BaseFilterTestCase): - filter_type = FindTransactionsResponseFilter + filter_type = FindTransactionsCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection diff --git a/test/commands/get_balances_test.py b/test/commands/get_balances_test.py index 3e10feb..d79d3dc 100644 --- a/test/commands/get_balances_test.py +++ b/test/commands/get_balances_test.py @@ -6,14 +6,14 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type -from iota.commands.get_balances import GetBalancesRequestFilter, \ - GetBalancesResponseFilter +from iota.commands.get_balances import GetBalancesCommand from iota.filters import Trytes from iota.types import Address, TryteString +from test import MockAdapter class GetBalancesRequestFilterTestCase(BaseFilterTestCase): - filter_type = GetBalancesRequestFilter + filter_type = GetBalancesCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection @@ -238,7 +238,7 @@ def test_fail_threshold_too_big(self): class GetBalancesResponseFilterTestCase(BaseFilterTestCase): - filter_type = GetBalancesResponseFilter + filter_type = GetBalancesCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection diff --git a/test/commands/get_inclusion_states_test.py b/test/commands/get_inclusion_states_test.py index 4202a4d..6a0cc42 100644 --- a/test/commands/get_inclusion_states_test.py +++ b/test/commands/get_inclusion_states_test.py @@ -6,13 +6,14 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type -from iota.commands.get_inclusion_states import GetInclusionStatesRequestFilter +from iota.commands.get_inclusion_states import GetInclusionStatesCommand from iota.filters import Trytes from iota.types import TransactionId, TryteString +from test import MockAdapter class GetInclusionStatesRequestFilterTestCase(BaseFilterTestCase): - filter_type = GetInclusionStatesRequestFilter + filter_type = GetInclusionStatesCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection diff --git a/test/commands/get_node_info_test.py b/test/commands/get_node_info_test.py index d07d00c..5d19313 100644 --- a/test/commands/get_node_info_test.py +++ b/test/commands/get_node_info_test.py @@ -5,12 +5,13 @@ import filters as f from filters.test import BaseFilterTestCase -from iota.commands.get_node_info import GetNodeInfoRequestFilter, GetNodeInfoResponseFilter +from iota.commands.get_node_info import GetNodeInfoCommand from iota.types import TryteString +from test import MockAdapter class GetNodeInfoRequestFilterTestCase(BaseFilterTestCase): - filter_type = GetNodeInfoRequestFilter + filter_type = GetNodeInfoCommand(MockAdapter()).get_request_filter skip_value_check = True def test_pass_empty(self): @@ -37,7 +38,7 @@ def test_fail_unexpected_parameters(self): class GetNodeInfoResponseFilterTestCase(BaseFilterTestCase): - filter_type = GetNodeInfoResponseFilter + filter_type = GetNodeInfoCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection diff --git a/test/commands/get_tips_test.py b/test/commands/get_tips_test.py index da53f74..0ca5370 100644 --- a/test/commands/get_tips_test.py +++ b/test/commands/get_tips_test.py @@ -5,12 +5,13 @@ import filters as f from filters.test import BaseFilterTestCase -from iota.commands.get_tips import GetTipsRequestFilter, GetTipsResponseFilter +from iota.commands.get_tips import GetTipsCommand from iota.types import Address +from test import MockAdapter class GetTipsRequestFilterTestCase(BaseFilterTestCase): - filter_type = GetTipsRequestFilter + filter_type = GetTipsCommand(MockAdapter()).get_request_filter skip_value_check = True def test_pass_empty(self): @@ -37,7 +38,7 @@ def test_fail_unexpected_parameters(self): class GetTipsResponseFilterTestCase(BaseFilterTestCase): - filter_type = GetTipsResponseFilter + filter_type = GetTipsCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection diff --git a/test/commands/get_transactions_to_approve_test.py b/test/commands/get_transactions_to_approve_test.py index b31d500..1a14f59 100644 --- a/test/commands/get_transactions_to_approve_test.py +++ b/test/commands/get_transactions_to_approve_test.py @@ -6,12 +6,14 @@ from filters.test import BaseFilterTestCase from iota.commands.get_transactions_to_approve import \ - GetTransactionsToApproveRequestFilter, GetTransactionsToApproveResponseFilter + GetTransactionsToApproveCommand from iota.types import TransactionId +from test import MockAdapter class GetTransactionsToApproveRequestFilterTestCase(BaseFilterTestCase): - filter_type = GetTransactionsToApproveRequestFilter + filter_type =\ + GetTransactionsToApproveCommand(MockAdapter()).get_request_filter skip_value_check = True def test_pass_happy_path(self): @@ -88,7 +90,8 @@ def test_fail_depth_too_small(self): class GetTransactionsToApproveResponseFilterTestCase(BaseFilterTestCase): - filter_type = GetTransactionsToApproveResponseFilter + filter_type =\ + GetTransactionsToApproveCommand(MockAdapter()).get_response_filter skip_value_check = True # noinspection SpellCheckingInspection From 439f16a0f5233a1bcb863bbc04fd0fda842bd22d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 09:41:43 -0500 Subject: [PATCH 079/239] Added validation to `getNeighbors` requests. --- iota/commands/get_neighbors.py | 27 +++++++++++++++++++++++ test/commands/get_neighbors_test.py | 34 +++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+) create mode 100644 iota/commands/get_neighbors.py create mode 100644 test/commands/get_neighbors_test.py diff --git a/iota/commands/get_neighbors.py b/iota/commands/get_neighbors.py new file mode 100644 index 0000000..ce177b8 --- /dev/null +++ b/iota/commands/get_neighbors.py @@ -0,0 +1,27 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter + + +class GetNeighborsCommand(FilterCommand): + """ + Executes ``getNeighbors`` command. + + See :py:method:`iota.api.IotaApi.get_neighbors`. + """ + command = 'getNeighbors' + + def get_request_filter(self): + return GetNeighborsRequestFilter() + + def get_response_filter(self): + pass + + +class GetNeighborsRequestFilter(RequestFilter): + def __init__(self): + # `getNeighbors` does not accept any parameters. + # Using a filter here just to enforce that the request is empty. + super(GetNeighborsRequestFilter, self).__init__({}) diff --git a/test/commands/get_neighbors_test.py b/test/commands/get_neighbors_test.py new file mode 100644 index 0000000..f4754ca --- /dev/null +++ b/test/commands/get_neighbors_test.py @@ -0,0 +1,34 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase + +from iota.commands.get_neighbors import GetNeighborsCommand +from test import MockAdapter + + +class GetNeighborsRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetNeighborsCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def test_pass_empty(self): + """The request is (correctly) empty.""" + filter_ = self._filter({}) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, {}) + + def test_fail_unexpected_parameters(self): + """The request contains unexpected parameters.""" + self.assertFilterErrors( + { + # Fool of a Took! + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) From adf56de81b80fb553e8e2d4e76e92289dae3e818 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 09:53:44 -0500 Subject: [PATCH 080/239] Added validation to `interruptAttachingToTangle` request. --- .../commands/interrupt_attaching_to_tangle.py | 27 ++++++++++++++ .../interrupt_attaching_to_tangle_test.py | 35 +++++++++++++++++++ 2 files changed, 62 insertions(+) create mode 100644 iota/commands/interrupt_attaching_to_tangle.py create mode 100644 test/commands/interrupt_attaching_to_tangle_test.py diff --git a/iota/commands/interrupt_attaching_to_tangle.py b/iota/commands/interrupt_attaching_to_tangle.py new file mode 100644 index 0000000..f059c65 --- /dev/null +++ b/iota/commands/interrupt_attaching_to_tangle.py @@ -0,0 +1,27 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter + + +class InterruptAttachingToTangleCommand(FilterCommand): + """ + Executes ``interruptAttachingToTangle`` command. + + See :py:method:`iota.api.IotaApi.interrupt_attaching_to_tangle`. + """ + command = 'interruptAttachingToTangle' + + def get_request_filter(self): + return InterruptAttachingToTangleRequestFilter() + + def get_response_filter(self): + pass + + +class InterruptAttachingToTangleRequestFilter(RequestFilter): + def __init__(self): + # `interruptAttachingToTangle` takes no parameters. + # Using a filter here just to enforce that the request is empty. + super(InterruptAttachingToTangleRequestFilter, self).__init__({}) diff --git a/test/commands/interrupt_attaching_to_tangle_test.py b/test/commands/interrupt_attaching_to_tangle_test.py new file mode 100644 index 0000000..8429fa3 --- /dev/null +++ b/test/commands/interrupt_attaching_to_tangle_test.py @@ -0,0 +1,35 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase + +from iota.commands.interrupt_attaching_to_tangle import \ + InterruptAttachingToTangleCommand +from test import MockAdapter + + +class InterruptAttachingToTangleRequestFilterTestCase(BaseFilterTestCase): + filter_type =\ + InterruptAttachingToTangleCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def test_pass_empty(self): + filter_ = self._filter({}) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, {}) + + def test_fail_unexpected_parameters(self): + """The request contains unexpected parameters.""" + self.assertFilterErrors( + { + # You're tearing me apart Lisa! + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) From 61ae8b97eb10a94357100d2aa78175fd3e43fba5 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 09:57:03 -0500 Subject: [PATCH 081/239] Added validation to `removeNeighbors` request. --- iota/commands/remove_neighbors.py | 30 ++++++ test/commands/remove_neighbors_test.py | 126 +++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 iota/commands/remove_neighbors.py create mode 100644 test/commands/remove_neighbors_test.py diff --git a/iota/commands/remove_neighbors.py b/iota/commands/remove_neighbors.py new file mode 100644 index 0000000..b28a3da --- /dev/null +++ b/iota/commands/remove_neighbors.py @@ -0,0 +1,30 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f + +from iota.commands import FilterCommand, RequestFilter +from iota.filters import NodeUri + + +class RemoveNeighborsCommand(FilterCommand): + """ + Executes ``removeNeighbors`` command. + + See :py:method:`iota.api.IotaApi.remove_neighbors`. + """ + command = 'removeNeighbors' + + def get_request_filter(self): + return RemoveNeighborsRequestFilter() + + def get_response_filter(self): + pass + + +class RemoveNeighborsRequestFilter(RequestFilter): + def __init__(self): + super(RemoveNeighborsRequestFilter, self).__init__({ + 'uris': f.Required | f.Array | f.FilterRepeater(f.Required | NodeUri), + }) diff --git a/test/commands/remove_neighbors_test.py b/test/commands/remove_neighbors_test.py new file mode 100644 index 0000000..dce7bb8 --- /dev/null +++ b/test/commands/remove_neighbors_test.py @@ -0,0 +1,126 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase + +from iota.commands.remove_neighbors import RemoveNeighborsCommand +from iota.filters import NodeUri +from test import MockAdapter + + +class RemoveNeighborsRequestFilterTestCase(BaseFilterTestCase): + filter_type = RemoveNeighborsCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def test_pass_valid_request(self): + """The incoming request is valid.""" + request = { + 'uris': [ + 'udp://node1.iotatoken.com', + 'http://localhost:14265/', + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_fail_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'uris': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """The incoming request contains unexpected parameters.""" + self.assertFilterErrors( + { + 'uris': ['udp://localhost'], + + # I've never seen that before in my life, officer. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_neighbors_null(self): + """`uris` is null.""" + self.assertFilterErrors( + { + 'uris': None, + }, + + { + 'uris': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_uris_wrong_type(self): + """`uris` is not an array.""" + self.assertFilterErrors( + { + # Nope; it's gotta be an array, even if you only want to add + # a single neighbor. + 'uris': 'http://localhost:8080/' + }, + + { + 'uris': [f.Type.CODE_WRONG_TYPE] + }, + ) + + def test_fail_uris_empty(self): + """`uris` is an array, but it's empty.""" + self.assertFilterErrors( + { + # Insert "Forever Alone" meme here. + 'uris': [], + }, + + { + 'uris': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_uris_contents_invalid(self): + """ + `uris` is an array, but it contains invalid values. + """ + self.assertFilterErrors( + { + # When I said it has to be an array before, I meant an array of + # strings! + 'uris': [ + '', + False, + None, + b'http://localhost:8080/', + 'not a valid uri', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + 'udp://localhost', + + 2130706433, + ], + }, + + { + 'uris.0': [f.Required.CODE_EMPTY], + 'uris.1': [f.Type.CODE_WRONG_TYPE], + 'uris.2': [f.Required.CODE_EMPTY], + 'uris.3': [f.Type.CODE_WRONG_TYPE], + 'uris.4': [NodeUri.CODE_NOT_NODE_URI], + 'uris.6': [f.Type.CODE_WRONG_TYPE], + }, + ) From fcce2f2b75d7ce9ea1140e84ba75a0e54bd38115 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 11:07:04 -0500 Subject: [PATCH 082/239] Stubbed out `getTrytes`. --- iota/api.py | 9 ++-- iota/commands/get_trytes.py | 34 +++++++++++++++ test/commands/get_trytes_test.py | 71 ++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+), 4 deletions(-) create mode 100644 iota/commands/get_trytes.py create mode 100644 test/commands/get_trytes_test.py diff --git a/iota/api.py b/iota/api.py index 0612a6a..1d6df70 100644 --- a/iota/api.py +++ b/iota/api.py @@ -223,12 +223,12 @@ def get_transactions_to_approve(self, depth): def get_trytes(self, hashes): # type: (Iterable[Text]) -> dict """ - Returns the raw transaction data (trytes) of a specific - transaction. + Returns the raw transaction data (trytes) of one or more + transactions. :see: https://iota.readme.io/docs/gettrytes """ - raise NotImplementedError('Not implemented yet.') + return self.getTrytes(hashes=hashes) def interrupt_attaching_to_tangle(self): # type: () -> dict @@ -245,7 +245,8 @@ def remove_neighbors(self, uris): Removes one or more neighbors from the node. Lasts until the node is restarted. - :param uris: Use format `udp://:`. + :param uris: + Use format ``udp://:``. Example: `remove_neighbors(['udp://example.com:14265'])` :see: https://iota.readme.io/docs/removeneighors diff --git a/iota/commands/get_trytes.py b/iota/commands/get_trytes.py new file mode 100644 index 0000000..13fdbae --- /dev/null +++ b/iota/commands/get_trytes.py @@ -0,0 +1,34 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter, ResponseFilter + + +class GetTrytesCommand(FilterCommand): + """ + Executes ``getTrytes`` command. + + See :py:method:`iota.api.IotaApi.get_trytes`. + """ + command = 'getTrytes' + + def get_request_filter(self): + return GetTrytesRequestFilter() + + def get_response_filter(self): + return GetTrytesResponseFilter() + + +class GetTrytesRequestFilter(RequestFilter): + def __init__(self): + super(GetTrytesRequestFilter, self).__init__({ + + }) + + +class GetTrytesResponseFilter(ResponseFilter): + def __init__(self): + super(GetTrytesResponseFilter, self).__init__({ + + }) diff --git a/test/commands/get_trytes_test.py b/test/commands/get_trytes_test.py new file mode 100644 index 0000000..20ec3fa --- /dev/null +++ b/test/commands/get_trytes_test.py @@ -0,0 +1,71 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from filters.test import BaseFilterTestCase + +from iota.commands.get_trytes import GetTrytesCommand +from test import MockAdapter + + +class GetTrytesRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetTrytesCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def test_pass_happy_path(self): + """The request is valid.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_compatible_types(self): + """ + The request contains values that can be converted to the expected + types. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_empty(self): + """The request is empty.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """The request contains unexpected parameters.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_null(self): + """`hashes` is null.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_wrong_type(self): + """`hashes` is not an array.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_empty(self): + """`hashes` is an array, but it is empty.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_contents_invalid(self): + """`hashes` is an array, but it contains invalid values.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + +class GetTrytesResponseFilter(BaseFilterTestCase): + filter_type = GetTrytesCommand(MockAdapter()).get_response_filter + skip_value_check = True + + def test_pass_transactions(self): + """The response contains data for multiple transactions.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_no_transactions(self): + """The response does not contain any transactions.""" + # :todo: Implement test. + self.skipTest('Not implemented yet.') From c6406efc76b95d0024ace38152284cb2d8dbd18d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 11:07:19 -0500 Subject: [PATCH 083/239] Cleaned up RST markup in IotaApi docs. --- iota/api.py | 135 +++++++++++++++++++++++++++++++--------------------- 1 file changed, 81 insertions(+), 54 deletions(-) diff --git a/iota/api.py b/iota/api.py index 1d6df70..07c26e1 100644 --- a/iota/api.py +++ b/iota/api.py @@ -17,7 +17,8 @@ class IotaApi(object): """ API to send HTTP requests for communicating with an IOTA node. - :see: https://iota.readme.io/docs/getting-started + References: + - https://iota.readme.io/docs/getting-started """ def __init__(self, adapter): # type: (Union[Text, BaseAdapter]) -> None @@ -36,10 +37,13 @@ def __getattr__(self, command): """ Sends an arbitrary API command to the node. - This method is useful for invoking unsupported or experimental - methods, or if you just want to troll your node for awhile. + This method is useful for invoking undocumented or experimental + methods, or if you just want to troll your node for awhile. :param command: The name of the command to send. + + References: + - https://iota.readme.io/docs/making-requests """ try: return command_registry[command](self.adapter) @@ -50,12 +54,14 @@ def add_neighbors(self, uris): # type: (Iterable[Text]) -> dict """ Add one or more neighbors to the node. Lasts until the node is - restarted. + restarted. - :param uris: Use format `udp://:`. - Example: `add_neighbors(['udp://example.com:14265'])` + :param uris: + Use format ``udp://:``. + Example: ``add_neighbors(['udp://example.com:14265'])`` - :see: https://iota.readme.io/docs/addneighors + References: + - https://iota.readme.io/docs/addneighors """ return self.addNeighbors(uris=uris) @@ -69,15 +75,17 @@ def attach_to_tangle( # type: (TransactionId, TransactionId, Iterable[TryteString], int) -> dict """ Attaches the specified transactions (trytes) to the Tangle by doing - Proof of Work. You need to supply branchTransaction as well as - trunkTransaction (basically the tips which you're going to - validate and reference with this transaction) - both of which - you'll get through the getTransactionsToApprove API call. + Proof of Work. You need to supply branchTransaction as well as + trunkTransaction (basically the tips which you're going to + validate and reference with this transaction) - both of which + you'll get through the getTransactionsToApprove API call. The returned value is a different set of tryte values which you can - input into `broadcast_transactions` and `store_transactions`. + input into :py:method:`broadcast_transactions` and + :py:method:`store_transactions`. - :see: https://iota.readme.io/docs/attachtotangle + References: + - https://iota.readme.io/docs/attachtotangle """ return self.attachToTangle( trunk_transaction = trunk_transaction, @@ -91,9 +99,11 @@ def broadcast_transactions(self, trytes): """ Broadcast a list of transactions to all neighbors. - The input trytes for this call are provided by `attach_to_tangle`. + The input trytes for this call are provided by + :py:method:`attach_to_tangle`. - :see: https://iota.readme.io/docs/broadcasttransactions + References: + - https://iota.readme.io/docs/broadcasttransactions """ return self.broadcastTransactions(trytes=trytes) @@ -109,18 +119,19 @@ def find_transactions( Find the transactions which match the specified input and return. All input values are lists, for which a list of return values - (transaction hashes), in the same order, is returned for all - individual elements. + (transaction hashes), in the same order, is returned for all + individual elements. Using multiple of these input fields returns the intersection of - the values. + the values. :param bundles: List of transaction IDs. :param addresses: List of addresses. :param tags: List of tags. Each tag must be 27 trytes. :param approvees: List of approvee transaction IDs. - :see: https://iota.readme.io/docs/findtransactions + References: + - https://iota.readme.io/docs/findtransactions """ return self.findTransactions( bundles = bundles, @@ -133,18 +144,20 @@ def get_balances(self, addresses, threshold=100): # type: (Iterable[Address], int) -> dict """ Similar to `get_inclusion_states`. Returns the confirmed balance - which a list of addresses have at the latest confirmed milestone. + which a list of addresses have at the latest confirmed milestone. In addition to the balances, it also returns the milestone as well - as the index with which the confirmed balance was determined. - The balances are returned as a list in the same order as the - addresses were provided as input. + as the index with which the confirmed balance was determined. + The balances are returned as a list in the same order as the + addresses were provided as input. + + :param addresses: + List of addresses to get the confirmed balance for. - :param addresses: List of addresses to get the confirmed balance - for. :param threshold: Confirmation threshold. - :see: https://iota.readme.io/docs/getbalances + References: + - https://iota.readme.io/docs/getbalances """ return self.getBalances( addresses = addresses, @@ -155,16 +168,19 @@ def get_inclusion_states(self, transactions, tips): # type: (Iterable[TransactionId], Iterable[TransactionId]) -> dict """ Get the inclusion states of a set of transactions. This is for - determining if a transaction was accepted and confirmed by the - network or not. You can search for multiple tips (and thus, - milestones) to get past inclusion states of transactions. + determining if a transaction was accepted and confirmed by the + network or not. You can search for multiple tips (and thus, + milestones) to get past inclusion states of transactions. + + :param transactions: + List of transactions you want to get the inclusion state for. - :param transactions: List of transactions you want to get the - inclusion state for. - :param tips: List of tips (including milestones) you want to search - for the inclusion state. + :param tips: + List of tips (including milestones) you want to search for the + inclusion state. - :see: https://iota.readme.io/docs/getinclusionstates + References: + - https://iota.readme.io/docs/getinclusionstates """ return self.getInclusionStates( transactions = transactions, @@ -175,11 +191,12 @@ def get_neighbors(self): # type: () -> dict """ Returns the set of neighbors the node is connected with, as well as - their activity count. + their activity count. The activity counter is reset after restarting IRI. - :see: https://iota.readme.io/docs/getneighborsactivity + References: + - https://iota.readme.io/docs/getneighborsactivity """ return self.getNeighbors() @@ -188,7 +205,8 @@ def get_node_info(self): """ Returns information about the node. - :see: https://iota.readme.io/docs/getnodeinfo + References: + - https://iota.readme.io/docs/getnodeinfo """ return self.getNodeInfo() @@ -196,27 +214,30 @@ def get_tips(self): # type: () -> dict """ Returns the list of tips (transactions which have no other - transactions referencing them). + transactions referencing them). - :see: https://iota.readme.io/docs/gettips - :see: https://iota.readme.io/docs/glossary#iota-terms + References: + - https://iota.readme.io/docs/gettips + - https://iota.readme.io/docs/glossary#iota-terms """ return self.getTips() def get_transactions_to_approve(self, depth): # type: (int) -> dict """ - Tip selection which returns `trunkTransaction` and - `branchTransaction`. + Tip selection which returns ``trunkTransaction`` and + ``branchTransaction``. - :param depth: Determines how many bundles to go back to when - finding the transactions to approve. + :param depth: + Determines how many bundles to go back to when finding the + transactions to approve. The higher the depth value, the more "babysitting" the node will - perform for the network (as it will confirm more transactions - that way). + perform for the network (as it will confirm more transactions + that way). - :see: https://iota.readme.io/docs/gettransactionstoapprove + References: + - https://iota.readme.io/docs/gettransactionstoapprove """ return self.getTransactionsToApprove(depth=depth) @@ -226,16 +247,19 @@ def get_trytes(self, hashes): Returns the raw transaction data (trytes) of one or more transactions. - :see: https://iota.readme.io/docs/gettrytes + References: + - https://iota.readme.io/docs/gettrytes """ return self.getTrytes(hashes=hashes) def interrupt_attaching_to_tangle(self): # type: () -> dict """ - Interrupts and completely aborts the `attach_to_tangle` process. + Interrupts and completely aborts the :py:method:`attach_to_tangle` + process. - :see: https://iota.readme.io/docs/interruptattachingtotangle + References: + - https://iota.readme.io/docs/interruptattachingtotangle """ return self.interruptAttachingToTangle() @@ -243,13 +267,14 @@ def remove_neighbors(self, uris): # type: (Iterable[Text]) -> dict """ Removes one or more neighbors from the node. Lasts until the node - is restarted. + is restarted. :param uris: Use format ``udp://:``. Example: `remove_neighbors(['udp://example.com:14265'])` - :see: https://iota.readme.io/docs/removeneighors + References: + - https://iota.readme.io/docs/removeneighors """ return self.removeNeighbors(uris=uris) @@ -258,8 +283,10 @@ def store_transactions(self, trytes): """ Store transactions into local storage. - The input trytes for this call are provided by `attach_to_tangle`. + The input trytes for this call are provided by + :py:method:`attach_to_tangle`. - :see: https://iota.readme.io/docs/storetransactions + References: + - https://iota.readme.io/docs/storetransactions """ raise NotImplementedError('Not implemented yet.') From 83c08b46b95831e89ec9167dbc7ecb91fbf92647 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 12:30:55 -0500 Subject: [PATCH 084/239] Added validation to `getTrytes` request. --- iota/commands/get_trytes.py | 10 ++- test/commands/get_trytes_test.py | 143 +++++++++++++++++++++++++++---- 2 files changed, 136 insertions(+), 17 deletions(-) diff --git a/iota/commands/get_trytes.py b/iota/commands/get_trytes.py index 13fdbae..6f0e9de 100644 --- a/iota/commands/get_trytes.py +++ b/iota/commands/get_trytes.py @@ -2,7 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f + from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.filters import Trytes +from iota.types import TransactionId class GetTrytesCommand(FilterCommand): @@ -23,7 +27,11 @@ def get_response_filter(self): class GetTrytesRequestFilter(RequestFilter): def __init__(self): super(GetTrytesRequestFilter, self).__init__({ - + 'hashes': ( + f.Required + | f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + ), }) diff --git a/test/commands/get_trytes_test.py b/test/commands/get_trytes_test.py index 20ec3fa..ce78753 100644 --- a/test/commands/get_trytes_test.py +++ b/test/commands/get_trytes_test.py @@ -2,9 +2,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f from filters.test import BaseFilterTestCase +from six import binary_type, text_type from iota.commands.get_trytes import GetTrytesCommand +from iota.filters import Trytes +from iota.types import TransactionId, TryteString from test import MockAdapter @@ -12,48 +16,155 @@ class GetTrytesRequestFilterTestCase(BaseFilterTestCase): filter_type = GetTrytesCommand(MockAdapter()).get_request_filter skip_value_check = True + # noinspection SpellCheckingInspection + def setUp(self): + super(GetTrytesRequestFilterTestCase, self).setUp() + + # Define some valid tryte sequences that we can re-use between + # tests. + self.trytes1 = ( + b'OAATQS9VQLSXCLDJVJJVYUGONXAXOFMJOZNSYWRZ' + b'SWECMXAQQURHQBJNLD9IOFEPGZEPEMPXCIVRX9999' + ) + + self.trytes2 = ( + b'ZIJGAJ9AADLRPWNCYNNHUHRRAC9QOUDATEDQUMTN' + b'OTABUVRPTSTFQDGZKFYUUIE9ZEBIVCCXXXLKX9999' + ) + def test_pass_happy_path(self): """The request is valid.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'hashes': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ The request contains values that can be converted to the expected types. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'hashes': [ + # Any sequence that can be converted into a TransactionId is + # valid. + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'hashes': [ + TransactionId(self.trytes1), + TransactionId(self.trytes2), + ], + }, + ) def test_fail_empty(self): """The request is empty.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + {}, + + { + 'hashes': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) def test_fail_unexpected_parameters(self): """The request contains unexpected parameters.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': [TransactionId(self.trytes1)], + + # This is why we can't have nice things! + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_hashes_null(self): """`hashes` is null.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': None, + }, + + { + 'hashes': [f.Required.CODE_EMPTY], + }, + ) def test_fail_hashes_wrong_type(self): """`hashes` is not an array.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # `hashes` must be an array, even if we're only querying + # against a single transaction. + 'hashes': TransactionId(self.trytes1), + }, + + { + 'hashes': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_hashes_empty(self): """`hashes` is an array, but it is empty.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': [], + }, + + { + 'hashes': [f.Required.CODE_EMPTY], + }, + ) def test_fail_hashes_contents_invalid(self): """`hashes` is an array, but it contains invalid values.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes1), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'hashes.0': [f.Required.CODE_EMPTY], + 'hashes.1': [f.Type.CODE_WRONG_TYPE], + 'hashes.2': [f.Type.CODE_WRONG_TYPE], + 'hashes.3': [f.Required.CODE_EMPTY], + 'hashes.4': [Trytes.CODE_NOT_TRYTES], + 'hashes.6': [f.Type.CODE_WRONG_TYPE], + 'hashes.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) class GetTrytesResponseFilter(BaseFilterTestCase): From 9b354dd494f3f9dbcb8d876da28f6ae22006e681 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 12:36:08 -0500 Subject: [PATCH 085/239] Added validation to `getTrytes` responses. --- iota/commands/get_trytes.py | 5 +++- test/commands/get_trytes_test.py | 47 +++++++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 5 deletions(-) diff --git a/iota/commands/get_trytes.py b/iota/commands/get_trytes.py index 6f0e9de..878802f 100644 --- a/iota/commands/get_trytes.py +++ b/iota/commands/get_trytes.py @@ -38,5 +38,8 @@ def __init__(self): class GetTrytesResponseFilter(ResponseFilter): def __init__(self): super(GetTrytesResponseFilter, self).__init__({ - + 'trytes': ( + f.Array + | f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes) + ), }) diff --git a/test/commands/get_trytes_test.py b/test/commands/get_trytes_test.py index ce78753..7257c3f 100644 --- a/test/commands/get_trytes_test.py +++ b/test/commands/get_trytes_test.py @@ -171,12 +171,51 @@ class GetTrytesResponseFilter(BaseFilterTestCase): filter_type = GetTrytesCommand(MockAdapter()).get_response_filter skip_value_check = True + # noinspection SpellCheckingInspection + def setUp(self): + super(GetTrytesResponseFilter, self).setUp() + + # Define some valid tryte sequences that we can re-use between + # tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + def test_pass_transactions(self): """The response contains data for multiple transactions.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'trytes': [ + # In real life, these values would be a lot longer, but for the + # purposes of this test, any sequence of trytes will do. + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + + 'duration': 42, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + + 'duration': 42, + }, + ) def test_pass_no_transactions(self): """The response does not contain any transactions.""" - # :todo: Implement test. - self.skipTest('Not implemented yet.') + response = { + 'trytes': [], + 'duration': 42, + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, response) From ba2af61e1926592137af750efc9a86436637f078 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 12:37:19 -0500 Subject: [PATCH 086/239] Fixed type hints. --- iota/api.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/iota/api.py b/iota/api.py index 07c26e1..fd27f15 100644 --- a/iota/api.py +++ b/iota/api.py @@ -95,7 +95,7 @@ def attach_to_tangle( ) def broadcast_transactions(self, trytes): - # type: (Iterable[Text]) -> dict + # type: (Iterable[TryteString]) -> dict """ Broadcast a list of transactions to all neighbors. @@ -242,7 +242,7 @@ def get_transactions_to_approve(self, depth): return self.getTransactionsToApprove(depth=depth) def get_trytes(self, hashes): - # type: (Iterable[Text]) -> dict + # type: (Iterable[TransactionId]) -> dict """ Returns the raw transaction data (trytes) of one or more transactions. @@ -279,7 +279,7 @@ def remove_neighbors(self, uris): return self.removeNeighbors(uris=uris) def store_transactions(self, trytes): - # type: (Iterable[Text]) -> dict + # type: (Iterable[TryteString]) -> dict """ Store transactions into local storage. From 8519fb448de2daec48e068da7e26c940a63aac73 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 12:39:33 -0500 Subject: [PATCH 087/239] Minor imports cleanup. --- iota/commands/get_neighbors.py | 4 ++++ iota/commands/get_tips.py | 4 ++++ iota/commands/get_transactions_to_approve.py | 4 ++++ iota/commands/get_trytes.py | 4 ++++ iota/commands/interrupt_attaching_to_tangle.py | 4 ++++ 5 files changed, 20 insertions(+) diff --git a/iota/commands/get_neighbors.py b/iota/commands/get_neighbors.py index ce177b8..54e2a78 100644 --- a/iota/commands/get_neighbors.py +++ b/iota/commands/get_neighbors.py @@ -4,6 +4,10 @@ from iota.commands import FilterCommand, RequestFilter +__all__ = [ + 'GetNeighborsCommand', +] + class GetNeighborsCommand(FilterCommand): """ diff --git a/iota/commands/get_tips.py b/iota/commands/get_tips.py index e2baf9f..5e31fae 100644 --- a/iota/commands/get_tips.py +++ b/iota/commands/get_tips.py @@ -8,6 +8,10 @@ from iota.filters import Trytes from iota.types import Address +__all__ = [ + 'GetTipsCommand', +] + class GetTipsCommand(FilterCommand): """ diff --git a/iota/commands/get_transactions_to_approve.py b/iota/commands/get_transactions_to_approve.py index 381e2d9..eeaa8b6 100644 --- a/iota/commands/get_transactions_to_approve.py +++ b/iota/commands/get_transactions_to_approve.py @@ -8,6 +8,10 @@ from iota.filters import Trytes from iota.types import TransactionId +__all__ = [ + 'GetTransactionsToApproveCommand', +] + class GetTransactionsToApproveCommand(FilterCommand): """ diff --git a/iota/commands/get_trytes.py b/iota/commands/get_trytes.py index 878802f..92ec8f6 100644 --- a/iota/commands/get_trytes.py +++ b/iota/commands/get_trytes.py @@ -8,6 +8,10 @@ from iota.filters import Trytes from iota.types import TransactionId +__all__ = [ + 'GetTrytesCommand', +] + class GetTrytesCommand(FilterCommand): """ diff --git a/iota/commands/interrupt_attaching_to_tangle.py b/iota/commands/interrupt_attaching_to_tangle.py index f059c65..7b33926 100644 --- a/iota/commands/interrupt_attaching_to_tangle.py +++ b/iota/commands/interrupt_attaching_to_tangle.py @@ -4,6 +4,10 @@ from iota.commands import FilterCommand, RequestFilter +__all__ = [ + 'InterruptAttachingToTangleCommand', +] + class InterruptAttachingToTangleCommand(FilterCommand): """ From 64de64365ad61a1c4c70528937f4cc22b6892c27 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 12:43:07 -0500 Subject: [PATCH 088/239] Implemented `storeTransactions`. --- iota/api.py | 2 +- iota/commands/store_transactions.py | 34 +++++ test/commands/store_transactions_test.py | 160 +++++++++++++++++++++++ 3 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 iota/commands/store_transactions.py create mode 100644 test/commands/store_transactions_test.py diff --git a/iota/api.py b/iota/api.py index fd27f15..d12dfc9 100644 --- a/iota/api.py +++ b/iota/api.py @@ -289,4 +289,4 @@ def store_transactions(self, trytes): References: - https://iota.readme.io/docs/storetransactions """ - raise NotImplementedError('Not implemented yet.') + return self.storeTransactions(trytes=trytes) diff --git a/iota/commands/store_transactions.py b/iota/commands/store_transactions.py new file mode 100644 index 0000000..95f00c6 --- /dev/null +++ b/iota/commands/store_transactions.py @@ -0,0 +1,34 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f + +from iota.commands import FilterCommand, RequestFilter +from iota.filters import Trytes + +__all__ = [ + 'StoreTransactionsCommand', +] + + +class StoreTransactionsCommand(FilterCommand): + """ + Executes ``storeTransactions`` command. + + See :py:method:`iota.api.IotaApi.store_transactions`. + """ + command = 'storeTransactions' + + def get_request_filter(self): + return StoreTransactionsRequestFilter() + + def get_response_filter(self): + pass + + +class StoreTransactionsRequestFilter(RequestFilter): + def __init__(self): + super(StoreTransactionsRequestFilter, self).__init__({ + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + }) diff --git a/test/commands/store_transactions_test.py b/test/commands/store_transactions_test.py new file mode 100644 index 0000000..d92f5c1 --- /dev/null +++ b/test/commands/store_transactions_test.py @@ -0,0 +1,160 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase +from six import binary_type, text_type + +from iota.commands.store_transactions import StoreTransactionsCommand +from iota.filters import Trytes +from iota.types import TryteString +from test import MockAdapter + + +class StoreTransactionsRequestFilterTestCase(BaseFilterTestCase): + filter_type = StoreTransactionsCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(StoreTransactionsRequestFilterTestCase, self).setUp() + + # Define a few valid values here that we can reuse across multiple + # tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_pass_happy_path(self): + """The incoming request is valid.""" + request = { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The incoming request contains values that can be converted into the + expected types. + """ + # Any values that can be converted into TryteStrings are accepted. + filter_ = self._filter({ + 'trytes': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + # The values are converted into TryteStrings so that they can be + # sent to the node. + { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ) + + def test_fail_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """The incoming value contains unexpected parameters.""" + self.assertFilterErrors( + { + 'trytes': [TryteString(self.trytes1)], + + # Alright buddy, let's see some ID. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_trytes_null(self): + """`trytes` is null.""" + self.assertFilterErrors( + { + 'trytes': None, + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_trytes_wrong_type(self): + """`trytes` is not an array.""" + self.assertFilterErrors( + { + # `trytes` has to be an array, even if there's only one + # TryteString. + 'trytes': TryteString(self.trytes1), + }, + + { + 'trytes': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_trytes_empty(self): + """`trytes` is an array, but it's empty.""" + self.assertFilterErrors( + { + 'trytes': [], + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_trytes_contents_invalid(self): + """`trytes` is an array, but it contains invalid values.""" + self.assertFilterErrors( + { + 'trytes': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + ], + }, + + { + 'trytes.0': [f.NotEmpty.CODE_EMPTY], + 'trytes.1': [f.Type.CODE_WRONG_TYPE], + 'trytes.2': [f.Type.CODE_WRONG_TYPE], + 'trytes.3': [f.Required.CODE_EMPTY], + 'trytes.4': [Trytes.CODE_NOT_TRYTES], + 'trytes.6': [f.Type.CODE_WRONG_TYPE], + }, + ) From 9746277c9df555fe2a1316ec48dafaa17e1bb9fe Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 14:01:17 -0500 Subject: [PATCH 089/239] Renamed IotaApi to StrictIota. Core API will be separate from wrapper methods, for compatibility. --- examples/hello_world.py | 4 ++-- examples/shell.py | 4 ++-- iota/adapter.py | 2 +- iota/api.py | 22 ++++++++++++++++--- iota/commands/add_neighbors.py | 2 +- iota/commands/attach_to_tangle.py | 2 +- iota/commands/broadcast_transactions.py | 2 +- iota/commands/find_transactions.py | 2 +- iota/commands/get_balances.py | 2 +- iota/commands/get_inclusion_states.py | 2 +- iota/commands/get_neighbors.py | 2 +- iota/commands/get_node_info.py | 2 +- iota/commands/get_tips.py | 2 +- iota/commands/get_transactions_to_approve.py | 2 +- iota/commands/get_trytes.py | 2 +- .../commands/interrupt_attaching_to_tangle.py | 2 +- iota/commands/remove_neighbors.py | 2 +- iota/commands/store_transactions.py | 2 +- test/__init__.py | 2 +- test/api_test.py | 8 +++---- 20 files changed, 43 insertions(+), 27 deletions(-) diff --git a/examples/hello_world.py b/examples/hello_world.py index ce1ca0f..8dd32d6 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -15,12 +15,12 @@ from requests.exceptions import ConnectionError from six import text_type as text -from iota import BadApiResponse, IotaApi, __version__ +from iota import BadApiResponse, StrictIota, __version__ def main(uri): # type: (Text, int) -> None - api = IotaApi(uri) + api = StrictIota(uri) try: node_info = api.get_node_info() diff --git a/examples/shell.py b/examples/shell.py index d2aa42d..08f5bb6 100644 --- a/examples/shell.py +++ b/examples/shell.py @@ -10,12 +10,12 @@ from six import text_type as text -from iota import IotaApi, __version__ +from iota import Iota, __version__ def main(uri): # type: (Text) -> None - iota = IotaApi(uri) + iota = Iota(uri) _banner = ( 'IOTA API client for {uri} initialized as variable `iota`. ' diff --git a/iota/adapter.py b/iota/adapter.py index aff7f3f..4f59c54 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -77,7 +77,7 @@ class BaseAdapter(with_metaclass(_AdapterMeta)): """ Interface for IOTA API adapters. - Adapters make it easy to customize the way an IotaApi instance + Adapters make it easy to customize the way an StrictIota instance communicates with a node. """ supported_protocols = () # type: Tuple[Text] diff --git a/iota/api.py b/iota/api.py index d12dfc9..0318d93 100644 --- a/iota/api.py +++ b/iota/api.py @@ -9,14 +9,18 @@ from iota.types import Address, Tag, TransactionId, TryteString __all__ = [ - 'IotaApi', + 'Iota', + 'StrictIota', ] -class IotaApi(object): +class StrictIota(object): """ API to send HTTP requests for communicating with an IOTA node. + This implementation only exposes the "core" API methods. For a more + feature-complete implementation, use :py:class:`Iota` instead. + References: - https://iota.readme.io/docs/getting-started """ @@ -25,7 +29,7 @@ def __init__(self, adapter): """ :param adapter: URI string or BaseAdapter instance. """ - super(IotaApi, self).__init__() + super(StrictIota, self).__init__() if not isinstance(adapter, BaseAdapter): adapter = resolve_adapter(adapter) @@ -290,3 +294,15 @@ def store_transactions(self, trytes): - https://iota.readme.io/docs/storetransactions """ return self.storeTransactions(trytes=trytes) + + +class Iota(StrictIota): + """ + Implements the core API, plus additional wrapper methods for common + operations. + + References: + - https://iota.readme.io/docs/getting-started + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md + """ + pass diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index 0b71b63..6662b09 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -16,7 +16,7 @@ class AddNeighborsCommand(FilterCommand): """ Executes `addNeighbors` command. - See :py:method:`iota.api.IotaApi.add_neighbors`. + See :py:method:`iota.api.StrictIota.add_neighbors`. """ command = 'addNeighbors' diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index f9f697d..abe6d72 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -17,7 +17,7 @@ class AttachToTangleCommand(FilterCommand): """ Executes `attachToTangle` command. - See :py:method:`iota.api.IotaApi.attach_to_tangle`. + See :py:method:`iota.api.StrictIota.attach_to_tangle`. """ command = 'attachToTangle' diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index 19d1554..894ae07 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -16,7 +16,7 @@ class BroadcastTransactionsCommand(FilterCommand): """ Executes `broadcastTransactions` command. - See :py:method:`iota.api.IotaApi.broadcast_transactions`. + See :py:method:`iota.api.StrictIota.broadcast_transactions`. """ command = 'broadcastTransactions' diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index 019ae45..65a9a17 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -17,7 +17,7 @@ class FindTransactionsCommand(FilterCommand): """ Executes `findTransactions` command. - See :py:method:`iota.api.IotaApi.find_transactions`. + See :py:method:`iota.api.StrictIota.find_transactions`. """ command = 'findTransactions' diff --git a/iota/commands/get_balances.py b/iota/commands/get_balances.py index 589da0b..556ff86 100644 --- a/iota/commands/get_balances.py +++ b/iota/commands/get_balances.py @@ -17,7 +17,7 @@ class GetBalancesCommand(FilterCommand): """ Executes `getBalances` command. - See :py:method:`iota.api.IotaApi.get_balances`. + See :py:method:`iota.api.StrictIota.get_balances`. """ def get_request_filter(self): return GetBalancesRequestFilter() diff --git a/iota/commands/get_inclusion_states.py b/iota/commands/get_inclusion_states.py index d7b5bcf..aba31ab 100644 --- a/iota/commands/get_inclusion_states.py +++ b/iota/commands/get_inclusion_states.py @@ -17,7 +17,7 @@ class GetInclusionStatesCommand(FilterCommand): """ Executes ``getInclusionStates`` command. - See :py:method:`iota.api.IotaApi.get_inclusion_states`. + See :py:method:`iota.api.StrictIota.get_inclusion_states`. """ command = 'getInclusionStates' diff --git a/iota/commands/get_neighbors.py b/iota/commands/get_neighbors.py index 54e2a78..c96b373 100644 --- a/iota/commands/get_neighbors.py +++ b/iota/commands/get_neighbors.py @@ -13,7 +13,7 @@ class GetNeighborsCommand(FilterCommand): """ Executes ``getNeighbors`` command. - See :py:method:`iota.api.IotaApi.get_neighbors`. + See :py:method:`iota.api.StrictIota.get_neighbors`. """ command = 'getNeighbors' diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index 64add79..ed79d37 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -16,7 +16,7 @@ class GetNodeInfoCommand(FilterCommand): """ Executes `getNodeInfo` command. - See :py:method:`iota.api.IotaApi.get_node_info`. + See :py:method:`iota.api.StrictIota.get_node_info`. """ command = 'getNodeInfo' diff --git a/iota/commands/get_tips.py b/iota/commands/get_tips.py index 5e31fae..0c6ea5e 100644 --- a/iota/commands/get_tips.py +++ b/iota/commands/get_tips.py @@ -17,7 +17,7 @@ class GetTipsCommand(FilterCommand): """ Executes ``getTips`` command. - See :py:method:`iota.api.IotaApi.get_tips`. + See :py:method:`iota.api.StrictIota.get_tips`. """ command = 'getTips' diff --git a/iota/commands/get_transactions_to_approve.py b/iota/commands/get_transactions_to_approve.py index eeaa8b6..6b7c689 100644 --- a/iota/commands/get_transactions_to_approve.py +++ b/iota/commands/get_transactions_to_approve.py @@ -17,7 +17,7 @@ class GetTransactionsToApproveCommand(FilterCommand): """ Executes ``getTransactionsToApprove`` command. - See :py:method:`iota.api.IotaApi.get_transactions_to_approve`. + See :py:method:`iota.api.StrictIota.get_transactions_to_approve`. """ command = 'getTransactionsToApprove' diff --git a/iota/commands/get_trytes.py b/iota/commands/get_trytes.py index 92ec8f6..c3643a2 100644 --- a/iota/commands/get_trytes.py +++ b/iota/commands/get_trytes.py @@ -17,7 +17,7 @@ class GetTrytesCommand(FilterCommand): """ Executes ``getTrytes`` command. - See :py:method:`iota.api.IotaApi.get_trytes`. + See :py:method:`iota.api.StrictIota.get_trytes`. """ command = 'getTrytes' diff --git a/iota/commands/interrupt_attaching_to_tangle.py b/iota/commands/interrupt_attaching_to_tangle.py index 7b33926..c252dc3 100644 --- a/iota/commands/interrupt_attaching_to_tangle.py +++ b/iota/commands/interrupt_attaching_to_tangle.py @@ -13,7 +13,7 @@ class InterruptAttachingToTangleCommand(FilterCommand): """ Executes ``interruptAttachingToTangle`` command. - See :py:method:`iota.api.IotaApi.interrupt_attaching_to_tangle`. + See :py:method:`iota.api.StrictIota.interrupt_attaching_to_tangle`. """ command = 'interruptAttachingToTangle' diff --git a/iota/commands/remove_neighbors.py b/iota/commands/remove_neighbors.py index b28a3da..392ec45 100644 --- a/iota/commands/remove_neighbors.py +++ b/iota/commands/remove_neighbors.py @@ -12,7 +12,7 @@ class RemoveNeighborsCommand(FilterCommand): """ Executes ``removeNeighbors`` command. - See :py:method:`iota.api.IotaApi.remove_neighbors`. + See :py:method:`iota.api.StrictIota.remove_neighbors`. """ command = 'removeNeighbors' diff --git a/iota/commands/store_transactions.py b/iota/commands/store_transactions.py index 95f00c6..033cb90 100644 --- a/iota/commands/store_transactions.py +++ b/iota/commands/store_transactions.py @@ -16,7 +16,7 @@ class StoreTransactionsCommand(FilterCommand): """ Executes ``storeTransactions`` command. - See :py:method:`iota.api.IotaApi.store_transactions`. + See :py:method:`iota.api.StrictIota.store_transactions`. """ command = 'storeTransactions' diff --git a/test/__init__.py b/test/__init__.py index b834eff..dcf4a33 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -8,7 +8,7 @@ class MockAdapter(BaseAdapter): - """An adapter for IotaApi that always returns a mocked response.""" + """An adapter for StrictIota that always returns a mocked response.""" supported_protocols = ('mock',) def __init__(self, response=None): diff --git a/test/api_test.py b/test/api_test.py index ad668d5..edff600 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -4,7 +4,7 @@ from unittest import TestCase -from iota import IotaApi +from iota import StrictIota from iota.commands import CustomCommand from iota.commands.get_node_info import GetNodeInfoCommand from test import MockAdapter @@ -92,12 +92,12 @@ def test_init_with_uri(self): """ Passing a URI to the initializer instead of an adapter instance. """ - api = IotaApi('mock://') + api = StrictIota('mock://') self.assertIsInstance(api.adapter, MockAdapter) def test_registered_command(self): """Preparing a documented command.""" - api = IotaApi(MockAdapter()) + api = StrictIota(MockAdapter()) # We just need to make sure the correct command type is # instantiated; individual commands have their own unit tests. @@ -106,7 +106,7 @@ def test_registered_command(self): def test_custom_command(self): """Preparing an experimental/undocumented command.""" - api = IotaApi(MockAdapter()) + api = StrictIota(MockAdapter()) # We just need to make sure the correct command type is # instantiated; custom commands have their own unit tests. From a4c870af92168db60b235f36c9efec02e27f52b1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 8 Dec 2016 15:10:50 -0500 Subject: [PATCH 090/239] Stubbed out wrapper methods. --- iota/api.py | 235 ++++++++++++++++++++++++++++++++++++++++++++++++-- iota/types.py | 16 +++- 2 files changed, 245 insertions(+), 6 deletions(-) diff --git a/iota/api.py b/iota/api.py index 0318d93..7704917 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,11 +2,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Iterable, Optional, Text, Union +from typing import Iterable, List, Optional, Text, Union from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry -from iota.types import Address, Tag, TransactionId, TryteString +from iota.types import Address, Bundle, Tag, TransactionId, Transfer, \ + TryteString __all__ = [ 'Iota', @@ -302,7 +303,231 @@ class Iota(StrictIota): operations. References: - - https://iota.readme.io/docs/getting-started - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md + - https://iota.readme.io/docs/getting-started + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md """ - pass + def __init__(self, adapter, seed=None): + # type: (Union[Text, BaseAdapter], Optional[TryteString]) -> None + """ + :param seed: + Seed used to generate new addresses. + If not provided, a random one will be generated. + + Note: This value is never transferred to the node/network. + """ + super(Iota, self).__init__(adapter) + + self.seed = seed + + def get_inputs(self, start=None, end=None, threshold=None): + # type: (Optional[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:method:`find_transactions` returns an empty + result and then doing :py:method:`get_balances`), or by providing a + key range to search. + + :param start: Starting key index. + :param end: Starting key index. + :param threshold: Minimum required balance of accumulated inputs. + + :return: + Dict with the following keys:: + + { + 'inputs': , + 'totalBalance': , + } + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs + """ + raise NotImplementedError('Not implemented yet.') + + def prepare_transfers(self, transfers, inputs=None, change_address=None): + # type: (Iterable[Transfer], Optional[Iterable[TransactionId]], Optional[Address]) -> List[TryteString] + """ + Prepares transactions to be broadcast to the Tangle, by generating + the correct bundle, as well as choosing and signing the inputs (for + value transfers). + + :param transfers: Transfer objects to prepare. + + :param inputs: + List of inputs used to fund the transfer. + Not needed for zero-value transfers. + + :param change_address: + If inputs are provided, any unspent amount will be sent to this + address. + + If not specified, a change address will be generated + automatically. + + :return: + Array containing the trytes of the new bundle. + This value can be provided to :py:method:`broadcastTransaction` + and/or :py:method:`storeTransaction`. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers + """ + raise NotImplementedError('Not implemented yet.') + + def get_new_address(self, index=None, count=1): + # type: (Optional[int], Optional[int]) -> List[Address] + """ + Generates one or more new addresses from a seed. + + Note that this method always returns a list of addresses, even if + only one address is generated. + + :param index: + Specify the index of the new address. + If not provided, the address will generated deterministically. + + :param count: + Number of addresses to generate. + This is more efficient than calling :py:method:`get_new_address` + inside a loop. + + :return: List of generated addresses. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress + """ + raise NotImplementedError('Not implemented yet.') + + def get_bundle(self, transaction): + # type: (TransactionId) -> List[Bundle] + """ + Returns the bundle associated with the specified transaction hash. + + :param transaction: + Transaction hash. Can be any type of transaction (tail or non- + tail). + + :return: + List of bundles associated with the transaction. + If there are multiple bundles (e.g., because of a replay), all + valid matching bundles will be returned. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle + """ + raise NotImplementedError('Not implemented yet.') + + def get_transfers(self, indexes=None, inclusion_states=False): + # type: (Optional[Iterable[int]], bool) -> List[Bundle] + """ + Returns all transfers associated with the seed. + + :param indexes: + If specified, use addresses at these indexes to perform the + search. + + If not provided, _all_ transfers associated with the seed will be + returned. + + :param inclusion_states: + Whether to also fetch the inclusion states of the transfers. + + This requires an additional API call to the node, so it is + disabled by default. + + :return: List of bundles. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers + """ + raise NotImplementedError('Not implemented yet.') + + def replay_transfer(self, transaction): + # type: (TransactionId) -> Bundle + """ + Takes a tail transaction hash as input, gets the bundle associated + with the transaction and then replays the bundle by attaching it to + the tangle. + + :param transaction: Transaction hash. Must be a tail. + + :return: The bundle containing the replayed transfer. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#replaytransfer + """ + raise NotImplementedError('Not implemented yet.') + + def send_transfer( + self, + depth, + transfers, + inputs = None, + change_address = None, + min_weight_magnitude = 18, + ): + # type: (int, Iterable[Transfer], Optional[Iterable[TransactionId]], Optional[Address], int) -> Bundle + """ + Prepares a set of transfers and creates the bundle, then attaches + the bundle to the Tangle, and broadcasts and stores the + transactions. + + :param depth: Depth at which to attach the bundle. + :param transfers: Transfers to include in the bundle. + + :param inputs: + List of inputs used to fund the transfer. + Not needed for zero-value transfers. + + :param change_address: + If inputs are provided, any unspent amount will be sent to this + address. + + If not specified, a change address will be generated + automatically. + + :param min_weight_magnitude: + Min weight magnitude, used by the node to calibrate Proof of + Work. + + :return: The newly-attached bundle. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtransfer + """ + raise NotImplementedError('Not implemented yet.') + + def send_trytes(self, trytes, depth, min_weight_magnitude=18): + # type: (Iterable[TryteString], int, int) -> List[TryteString] + """ + Attaches transaction trytes to the Tangle, then broadcasts and + stores them. + + :param trytes: + Transaction encoded as a tryte sequence. + + :param depth: Depth at which to attach the bundle. + + :param min_weight_magnitude: + Min weight magnitude, used by the node to calibrate Proof of + Work. + + :return: The trytes that were attached to the Tangle. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes + """ + raise NotImplementedError('Not implemented yet.') + + def broadcast_and_store(self, trytes): + # type: (Iterable[TryteString]) -> List[TryteString] + """ + Broadcasts and stores a set of transaction trytes. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore + """ + raise NotImplementedError('Not implemented yet.') diff --git a/iota/types.py b/iota/types.py index 0931ba0..b38975f 100644 --- a/iota/types.py +++ b/iota/types.py @@ -3,7 +3,7 @@ unicode_literals from codecs import encode, decode -from typing import Generator, Text, Union +from typing import Generator, Optional, Text, Union, List from six import PY2, binary_type @@ -187,3 +187,17 @@ def __init__(self, trytes): if len(self.trytes) > self.LEN: raise ValueError('TransactionIds must be 81 trytes long.') + + +class Transfer(object): + """A message [to be] published to the Tangle.""" + def __init__(self, recipient, value, message=None, tag=None): + # type: (Address, int, Optional[TryteString], Optional[Tag]) -> None + self.recipient = recipient + self.value = value, + self.message = TryteString(message or b'') + self.tag = Tag(tag or b'') + + +Bundle = List[Transfer] +"""Placeholder for Bundle type in docstrings.""" From 50daede500e3937dfbeac5597614a26d1cd5cb10 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 10 Dec 2016 14:30:30 -0500 Subject: [PATCH 091/239] Integrated ccurl extension. --- .gitignore | 1 + .gitmodules | 3 +++ README.rst | 51 +++++++++++++++++++++++++++++++++------------------ ccurl | 1 + ext/ccurl | 1 + setup.py | 35 ++++++++++++++++++++++++++++++----- 6 files changed, 69 insertions(+), 23 deletions(-) create mode 100644 .gitmodules create mode 120000 ccurl create mode 160000 ext/ccurl diff --git a/.gitignore b/.gitignore index 5e15aa4..c1abef7 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,7 @@ build/* dist/* PyOTA.egg-info/* +*.so # Virtualenvs for unit tests. # :see: https://tox.readthedocs.io/en/latest/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..0fe13d4 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ext/ccurl"] + path = ext/ccurl + url = https://github.com/iotaledger/ccurl.git diff --git a/README.rst b/README.rst index 0d5e6a1..52332ec 100644 --- a/README.rst +++ b/README.rst @@ -3,19 +3,16 @@ PyOTA ===== 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). +It implements both the `official API`_, as well as newly-proposed functionality +(such as signing, bundles, utilities and conversion). Join the Discussion =================== If you want to get involved in the community, need help with getting setup, have any issues related with the library or just want to discuss Blockchain, -Distributed Ledgers and IoT with other people, feel free to join our -`Slack `. +Distributed Ledgers and IoT with other people, feel free to join our `Slack`_. -You can also ask questions on our -`dedicated forum `. +You can also ask questions on our `dedicated forum`_. ============ Dependencies @@ -27,26 +24,44 @@ Installation ============ To install the latest stable version:: - pip install https://github.com/iotaledger/pyota/archive/master.zip + pip install pyota To install the development version:: pip install https://github.com/iotaledger/pyota/archive/develop.zip -============= -Documentation -============= -For the full documentation of this library, please refer to the - `official API ` +Installing from Source +====================== +PyOTA uses the `curl extension`_, which requires `SWIG`_ in order to build. -============ -Contributing -============ +1. `Create virtualenv`_ (recommended, but not required). +2. ``git clone https://github.com/iotaledger/pyota.git`` +3. ``git submodule init --recursive`` +4. ``pip install -e .`` + - This step will fail if `SWIG`_ is not installed. Running Unit Tests -================== -To run unit tests for the project:: +------------------ +To run unit tests after installing from source:: + + python setup.py test + +PyOTA is also compatible with `tox`_:: pip install tox tox +============= +Documentation +============= +For the full documentation of this library, please refer to the +`official API`_ + + +.. _Create virtualenv: https://virtualenvwrapper.readthedocs.io/ +.. _curl extension: https://github.com/iotaledger/ccurl +.. _dedicated forum: http://forum.iotatoken.com/ +.. _official API: https://iota.readme.io/README.rst +.. _Slack: http://slack.iotatoken.com/ +.. _SWIG: http://www.swig.org/download.html +.. _tox: https://tox.readthedocs.io/ diff --git a/ccurl b/ccurl new file mode 120000 index 0000000..724ab80 --- /dev/null +++ b/ccurl @@ -0,0 +1 @@ +ext/ccurl/src \ No newline at end of file diff --git a/ext/ccurl b/ext/ccurl new file mode 160000 index 0000000..444620e --- /dev/null +++ b/ext/ccurl @@ -0,0 +1 @@ +Subproject commit 444620e6d88f2e547652b94ce48aa8755a72eb77 diff --git a/setup.py b/setup.py index 6eb060a..810e1e0 100644 --- a/setup.py +++ b/setup.py @@ -1,21 +1,42 @@ #!/usr/bin/env python # coding=utf-8 -from __future__ import absolute_import, division, print_function, \ - unicode_literals +# :bc: Not importing unicode_literals because in Python 2 distutils, +# certain values (e.g., extension name) have to be byte strings. +from __future__ import absolute_import, division, print_function from codecs import StreamReader, open -from setuptools import setup +from os.path import join, basename +from setuptools import Extension, setup +from setuptools.glob import iglob +from six import PY3 with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader long_description = f.read() +ccurl_sources = [ + f for f in iglob(join('ext', 'ccurl', 'src', '*.[ci]')) + if not basename(f).endswith('_wrap.c') +] + +if not ccurl_sources: + raise EnvironmentError( + 'Unable to find ccurl sources. Try running `git submodule init` first.', + ) + +swig_opts = ['-py3'] if PY3 else [] + setup( name = 'PyOTA', description = 'IOTA API library for Python', - url = 'https://github.com/iotaledger/iota.lib.py', + url = 'https://github.com/iotaledger/pyota', version = '1.0.0', - packages = ['iota'], + + packages = ['iota', 'ccurl'], + package_dir = { + 'iota': '', + 'ccurl': 'ext/ccurl/src', + }, long_description = long_description, @@ -26,6 +47,10 @@ 'typing ; python_version < "3.5"', ], + ext_modules = [ + Extension('_ccurl', ccurl_sources, swig_opts=swig_opts), + ], + test_suite = 'test', test_loader = 'nose.loader:TestLoader', tests_require = [ From c13a300dd5869eb6a2c4aaf23a400779e1e10c4f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 10 Dec 2016 14:33:28 -0500 Subject: [PATCH 092/239] Fixed broken link. --- README.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 52332ec..f6e35da 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ PyOTA uses the `curl extension`_, which requires `SWIG`_ in order to build. 2. ``git clone https://github.com/iotaledger/pyota.git`` 3. ``git submodule init --recursive`` 4. ``pip install -e .`` - - This step will fail if `SWIG`_ is not installed. + - This step will fail if `SWIG`_ is not installed. Running Unit Tests ------------------ @@ -61,7 +61,7 @@ For the full documentation of this library, please refer to the .. _Create virtualenv: https://virtualenvwrapper.readthedocs.io/ .. _curl extension: https://github.com/iotaledger/ccurl .. _dedicated forum: http://forum.iotatoken.com/ -.. _official API: https://iota.readme.io/README.rst +.. _official API: https://iota.readme.io/ .. _Slack: http://slack.iotatoken.com/ .. _SWIG: http://www.swig.org/download.html .. _tox: https://tox.readthedocs.io/ From 254b2b653a0e49eea2eaa92c242870bb4629dc29 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 10 Dec 2016 14:35:23 -0500 Subject: [PATCH 093/239] Removed extraneous/obsolete info. --- README.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/README.rst b/README.rst index f6e35da..98885d5 100644 --- a/README.rst +++ b/README.rst @@ -26,10 +26,6 @@ To install the latest stable version:: pip install pyota -To install the development version:: - - pip install https://github.com/iotaledger/pyota/archive/develop.zip - Installing from Source ====================== PyOTA uses the `curl extension`_, which requires `SWIG`_ in order to build. @@ -38,7 +34,6 @@ PyOTA uses the `curl extension`_, which requires `SWIG`_ in order to build. 2. ``git clone https://github.com/iotaledger/pyota.git`` 3. ``git submodule init --recursive`` 4. ``pip install -e .`` - - This step will fail if `SWIG`_ is not installed. Running Unit Tests ------------------ From 4437aca4046a7b40df1d4ae44559411e25b0e360 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 10 Dec 2016 14:37:00 -0500 Subject: [PATCH 094/239] More user-friendly error message if SWIG is not installed. --- setup.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 810e1e0..189acec 100644 --- a/setup.py +++ b/setup.py @@ -5,14 +5,17 @@ from __future__ import absolute_import, division, print_function from codecs import StreamReader, open +from distutils.spawn import find_executable from os.path import join, basename from setuptools import Extension, setup from setuptools.glob import iglob from six import PY3 -with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader - long_description = f.read() +if not find_executable('swig'): + raise EnvironmentError( + 'Unable to find `swig` executable. Check that SWIG is installed.' + ) ccurl_sources = [ f for f in iglob(join('ext', 'ccurl', 'src', '*.[ci]')) @@ -26,6 +29,9 @@ swig_opts = ['-py3'] if PY3 else [] +with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader + long_description = f.read() + setup( name = 'PyOTA', description = 'IOTA API library for Python', From 46bdef55ffed51f2e19718eb126275e164cf600e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 11 Dec 2016 17:46:50 -0500 Subject: [PATCH 095/239] Removed ccurl integration; will port instead. Need an easier release strategy, especially for early days. --- .gitmodules | 3 --- ccurl | 1 - ext/ccurl | 1 - setup.py | 35 +++-------------------------------- 4 files changed, 3 insertions(+), 37 deletions(-) delete mode 100644 .gitmodules delete mode 120000 ccurl delete mode 160000 ext/ccurl diff --git a/.gitmodules b/.gitmodules deleted file mode 100644 index 0fe13d4..0000000 --- a/.gitmodules +++ /dev/null @@ -1,3 +0,0 @@ -[submodule "ext/ccurl"] - path = ext/ccurl - url = https://github.com/iotaledger/ccurl.git diff --git a/ccurl b/ccurl deleted file mode 120000 index 724ab80..0000000 --- a/ccurl +++ /dev/null @@ -1 +0,0 @@ -ext/ccurl/src \ No newline at end of file diff --git a/ext/ccurl b/ext/ccurl deleted file mode 160000 index 444620e..0000000 --- a/ext/ccurl +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 444620e6d88f2e547652b94ce48aa8755a72eb77 diff --git a/setup.py b/setup.py index 189acec..9019d4c 100644 --- a/setup.py +++ b/setup.py @@ -1,33 +1,12 @@ #!/usr/bin/env python # coding=utf-8 # :bc: Not importing unicode_literals because in Python 2 distutils, -# certain values (e.g., extension name) have to be byte strings. +# some values are expected to be byte strings. from __future__ import absolute_import, division, print_function from codecs import StreamReader, open -from distutils.spawn import find_executable -from os.path import join, basename -from setuptools import Extension, setup -from setuptools.glob import iglob -from six import PY3 - -if not find_executable('swig'): - raise EnvironmentError( - 'Unable to find `swig` executable. Check that SWIG is installed.' - ) - -ccurl_sources = [ - f for f in iglob(join('ext', 'ccurl', 'src', '*.[ci]')) - if not basename(f).endswith('_wrap.c') -] - -if not ccurl_sources: - raise EnvironmentError( - 'Unable to find ccurl sources. Try running `git submodule init` first.', - ) - -swig_opts = ['-py3'] if PY3 else [] +from setuptools import setup with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader long_description = f.read() @@ -38,11 +17,7 @@ url = 'https://github.com/iotaledger/pyota', version = '1.0.0', - packages = ['iota', 'ccurl'], - package_dir = { - 'iota': '', - 'ccurl': 'ext/ccurl/src', - }, + packages = ['iota'], long_description = long_description, @@ -53,10 +28,6 @@ 'typing ; python_version < "3.5"', ], - ext_modules = [ - Extension('_ccurl', ccurl_sources, swig_opts=swig_opts), - ], - test_suite = 'test', test_loader = 'nose.loader:TestLoader', tests_require = [ From ca8697feda4fdf3f6090feefa164ce11da658562 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 12 Dec 2016 20:54:48 -0500 Subject: [PATCH 096/239] Better compat w/ older versions of pip + travis. --- .travis.yml | 6 ++++++ setup.py | 17 +++++++++++------ 2 files changed, 17 insertions(+), 6 deletions(-) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..1236c4a --- /dev/null +++ b/.travis.yml @@ -0,0 +1,6 @@ +language: python +python: + - "2.7" + - "3.5" +install: "pip install ." +script: "nosetests" diff --git a/setup.py b/setup.py index 9019d4c..3ffcb2b 100644 --- a/setup.py +++ b/setup.py @@ -5,12 +5,22 @@ from __future__ import absolute_import, division, print_function from codecs import StreamReader, open +from sys import version_info from setuptools import setup with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader long_description = f.read() +dependencies = [ + 'filters', + 'requests', + 'six', + ] + +if version_info[0:2] < (3, 5): + dependencies.append('typing') + setup( name = 'PyOTA', description = 'IOTA API library for Python', @@ -21,12 +31,7 @@ long_description = long_description, - install_requires = [ - 'filters', - 'requests', - 'six', - 'typing ; python_version < "3.5"', - ], + install_requires = dependencies, test_suite = 'test', test_loader = 'nose.loader:TestLoader', From 7bf0af411dd583451a8423b80510e39775c971b1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 12 Dec 2016 21:03:19 -0500 Subject: [PATCH 097/239] Made Command send request overrideable. --- iota/commands/__init__.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index e5c2803..713884a 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -83,9 +83,7 @@ def __call__(self, **kwargs): if replacement is not None: self.request = replacement - self.request['command'] = self.command - - self.response = self.adapter.send_request(self.request) + self.response = self.send_request(self.request) replacement = self._prepare_response(self.response) if replacement is not None: @@ -104,6 +102,17 @@ def reset(self): self.request = None # type: dict self.response = None # type: dict + def send_request(self, request): + # type: (dict) -> dict + """ + Sends the request object to the adapter and returns the response. + + The command name will be automatically injected into the request + before it is sent (note: this will modify the request object). + """ + request['command'] = self.command + return self.adapter.send_request(request) + @abstract_method def _prepare_request(self, request): # type: (dict) -> Optional[dict] From b2e08447d196548ff9c65caf79c750b64a00b2a4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 19:13:42 -0500 Subject: [PATCH 098/239] Implemented request filter for `broadcastAndStore`. --- iota/api.py | 2 +- iota/commands/broadcast_and_store.py | 33 +++++ test/commands/broadcast_and_store_test.py | 158 ++++++++++++++++++++++ 3 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 iota/commands/broadcast_and_store.py create mode 100644 test/commands/broadcast_and_store_test.py diff --git a/iota/api.py b/iota/api.py index 7704917..3ab6fd0 100644 --- a/iota/api.py +++ b/iota/api.py @@ -530,4 +530,4 @@ def broadcast_and_store(self, trytes): References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore """ - raise NotImplementedError('Not implemented yet.') + return self.broadcastAndStore(trytes=trytes) diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/broadcast_and_store.py new file mode 100644 index 0000000..aa0df61 --- /dev/null +++ b/iota/commands/broadcast_and_store.py @@ -0,0 +1,33 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from iota.commands import FilterCommand, RequestFilter +from iota.filters import Trytes + +__all__ = [ + 'BroadcastAndStoreCommand', +] + + +class BroadcastAndStoreCommand(FilterCommand): + """ + Executes `broadcastAndStore` extended API command. + + See :py:method:`iota.api.IotaApi.broadcast_and_store` for more info. + """ + command = 'broadcastAndStore' + + def get_request_filter(self): + return BroadcastAndStoreRequestFilter() + + def get_response_filter(self): + pass + + +class BroadcastAndStoreRequestFilter(RequestFilter): + def __init__(self): + super(BroadcastAndStoreRequestFilter, self).__init__({ + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + }) diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py new file mode 100644 index 0000000..f433937 --- /dev/null +++ b/test/commands/broadcast_and_store_test.py @@ -0,0 +1,158 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase +from iota.commands.broadcast_and_store import BroadcastAndStoreCommand +from iota.filters import Trytes +from iota.types import TryteString +from six import binary_type, text_type +from test import MockAdapter + + +class BroadcastAndStoreRequestFilterTestCase(BaseFilterTestCase): + filter_type = BroadcastAndStoreCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(BroadcastAndStoreRequestFilterTestCase, self).setUp() + + # Define a few valid values that we can reuse across tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_pass_happy_path(self): + """The incoming request is valid.""" + request = { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The incoming request contains values that can be converted into the + expected types. + """ + # Any values that can be converted into TryteStrings are accepted. + filter_ = self._filter({ + 'trytes': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + # The values are converted into TryteStrings so that they can be + # sent to the node. + { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ) + + def test_fail_empty(self): + """The incoming request is empty.""" + self.assertFilterErrors( + {}, + + { + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """The incoming value contains unexpected parameters.""" + self.assertFilterErrors( + { + 'trytes': [TryteString(self.trytes1)], + + # Alright buddy, let's see some ID. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_trytes_null(self): + """`trytes` is null.""" + self.assertFilterErrors( + { + 'trytes': None, + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_trytes_wrong_type(self): + """`trytes` is not an array.""" + self.assertFilterErrors( + { + # `trytes` has to be an array, even if there's only one + # TryteString. + 'trytes': TryteString(self.trytes1), + }, + + { + 'trytes': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_trytes_empty(self): + """`trytes` is an array, but it's empty.""" + self.assertFilterErrors( + { + 'trytes': [], + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_trytes_contents_invalid(self): + """`trytes` is an array, but it contains invalid values.""" + self.assertFilterErrors( + { + 'trytes': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + ], + }, + + { + 'trytes.0': [f.NotEmpty.CODE_EMPTY], + 'trytes.1': [f.Type.CODE_WRONG_TYPE], + 'trytes.2': [f.Type.CODE_WRONG_TYPE], + 'trytes.3': [f.Required.CODE_EMPTY], + 'trytes.4': [Trytes.CODE_NOT_TRYTES], + 'trytes.6': [f.Type.CODE_WRONG_TYPE], + }, + ) From 14ad4effb29e1e00ac6a0a40697b4f8717d13469 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 19:16:32 -0500 Subject: [PATCH 099/239] Added response filter to `broadcastAndStore`. --- iota/commands/broadcast_and_store.py | 11 +++- test/commands/broadcast_and_store_test.py | 55 ++++++++++++++++++++ test/commands/broadcast_transactions_test.py | 1 - 3 files changed, 64 insertions(+), 3 deletions(-) diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/broadcast_and_store.py index aa0df61..f52367c 100644 --- a/iota/commands/broadcast_and_store.py +++ b/iota/commands/broadcast_and_store.py @@ -3,7 +3,7 @@ unicode_literals import filters as f -from iota.commands import FilterCommand, RequestFilter +from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes __all__ = [ @@ -23,7 +23,7 @@ def get_request_filter(self): return BroadcastAndStoreRequestFilter() def get_response_filter(self): - pass + return BroadcastAndStoreResponseFilter() class BroadcastAndStoreRequestFilter(RequestFilter): @@ -31,3 +31,10 @@ def __init__(self): super(BroadcastAndStoreRequestFilter, self).__init__({ 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), }) + + +class BroadcastAndStoreResponseFilter(ResponseFilter): + def __init__(self): + super(BroadcastAndStoreResponseFilter, self).__init__({ + 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), + }) diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index f433937..8dfc387 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -156,3 +156,58 @@ def test_trytes_contents_invalid(self): 'trytes.6': [f.Type.CODE_WRONG_TYPE], }, ) + +class BroadcastAndStoreResponseFilterTestCase(BaseFilterTestCase): + filter_type = BroadcastAndStoreCommand(MockAdapter()).get_response_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(BroadcastAndStoreResponseFilterTestCase, self).setUp() + + # Define a few valid values that we can reuse across tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_pass_happy_path(self): + """The incoming response contains valid values.""" + # Responses from the node arrive as strings. + filter_ = self._filter({ + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + self.assertFilterPasses(filter_) + + # The filter converts them into TryteStrings. + self.assertDictEqual( + filter_.cleaned_data, + + { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ) + + def test_pass_correct_types(self): + """ + The incoming response already contains correct types. + + This scenario is highly unusual, but who's complaining? + """ + response = { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ] + } + + filter_ = self._filter(response) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, response) diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index d20aad6..e7c6778 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -215,4 +215,3 @@ def test_pass_correct_types(self): self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, response) - From 778c7aeada93df917378bd26ab4b1f0bce9843aa Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 19:31:19 -0500 Subject: [PATCH 100/239] Can now seed multiple responses to MockAdapter. --- test/__init__.py | 47 ++++++++++++++++++++++++++++++++++++++++------- test/api_test.py | 13 +++++++------ 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index dcf4a33..6879f55 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -2,22 +2,55 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Optional +from typing import Dict, List, Optional, Text -from iota.adapter import BaseAdapter +from iota.adapter import BaseAdapter, BadApiResponse class MockAdapter(BaseAdapter): """An adapter for StrictIota that always returns a mocked response.""" supported_protocols = ('mock',) - def __init__(self, response=None): + # noinspection PyUnusedLocal + @classmethod + def configure(cls, uri): + return cls() + + def __init__(self): # type: (Optional[dict]) -> None super(MockAdapter, self).__init__() - self.response = response or {} - self.requests = [] + self.responses = {} # type: Dict[Text, dict] + self.requests = [] # type: List[dict] + + def seed_response(self, command, response): + # type: (Text, dict) -> MockAdapter + """ + Sets the response that the adapter will return for the specified + command. + """ + self.responses[command] = response + return self def send_request(self, payload, **kwargs): - self.requests.append((payload, kwargs)) - return self.response + # type: (dict, dict) -> dict + self.requests.append(payload) + + try: + command = payload['command'] + except KeyError: + raise BadApiResponse( + 'Payload missing `command` value: {payload!r}'.format( + payload = payload, + ), + ) + + try: + return self.responses[command] + except KeyError: + raise BadApiResponse( + 'Unknown request {command!r} (expected one of: {seeds!r}).'.format( + command = command, + seeds = list(sorted(self.responses.keys())), + ), + ) diff --git a/test/api_test.py b/test/api_test.py index edff600..10430f7 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -22,7 +22,7 @@ def test_call(self): """Sending a custom command.""" expected_response = {'message': 'Hello, IOTA!'} - self.adapter.response = expected_response + self.adapter.seed_response('helloWorld', expected_response) response = self.command() @@ -31,14 +31,14 @@ def test_call(self): self.assertListEqual( self.adapter.requests, - [({'command': 'helloWorld'}, {})], + [{'command': 'helloWorld'}], ) def test_call_with_parameters(self): """Sending a custom command with parameters.""" expected_response = {'message': 'Hello, IOTA!'} - self.adapter.response = expected_response + self.adapter.seed_response('helloWorld', expected_response) response = self.command(foo='bar', baz='luhrmann') @@ -47,11 +47,12 @@ def test_call_with_parameters(self): self.assertListEqual( self.adapter.requests, - [({'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}, {})], + [{'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}], ) def test_call_error_already_called(self): """A command can only be called once.""" + self.adapter.seed_response('helloWorld', {}) self.command() with self.assertRaises(RuntimeError): @@ -61,7 +62,7 @@ def test_call_error_already_called(self): def test_call_reset(self): """Resetting a command allows it to be called more than once.""" - self.adapter.response = {'message': 'Hello, IOTA!'} + self.adapter.seed_response('helloWorld', {'message': 'Hello, IOTA!'}) self.command() self.command.reset() @@ -71,7 +72,7 @@ def test_call_reset(self): self.assertIsNone(self.command.response) expected_response = {'message': 'Welcome back!'} - self.adapter.response = expected_response + self.adapter.seed_response('helloWorld', expected_response) response = self.command(foo='bar') self.assertDictEqual(response, expected_response) From 9ec6079c52341c2bd41c088221a582af9a5cdf74 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 19:37:50 -0500 Subject: [PATCH 101/239] Simple impl for `broadcastAndStore`. --- iota/commands/broadcast_and_store.py | 6 +++ test/commands/broadcast_and_store_test.py | 57 +++++++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/broadcast_and_store.py index f52367c..9cb6cdc 100644 --- a/iota/commands/broadcast_and_store.py +++ b/iota/commands/broadcast_and_store.py @@ -4,6 +4,8 @@ import filters as f from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.commands.broadcast_transactions import BroadcastTransactionsCommand +from iota.commands.store_transactions import StoreTransactionsCommand from iota.filters import Trytes __all__ = [ @@ -25,6 +27,10 @@ def get_request_filter(self): def get_response_filter(self): return BroadcastAndStoreResponseFilter() + def send_request(self, request): + BroadcastTransactionsCommand(self.adapter)(trytes=request['trytes']) + return StoreTransactionsCommand(self.adapter)(trytes=request['trytes']) + class BroadcastAndStoreRequestFilter(RequestFilter): def __init__(self): diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index 8dfc387..02ccd7f 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -2,6 +2,8 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase from iota.commands.broadcast_and_store import BroadcastAndStoreCommand @@ -211,3 +213,58 @@ def test_pass_correct_types(self): self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, response) + + +class BroadcastAndStoreCommandTestCase(TestCase): + # noinspection SpellCheckingInspection + def setUp(self): + self.adapter = MockAdapter() + self.command = BroadcastAndStoreCommand(self.adapter) + + # Define a few valid values that we can reuse across tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_happy_path(self): + """ + Successful invocation of `broadcastAndStore`. + """ + self.adapter.seed_response('broadcastTransactions', { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + self.adapter.seed_response('storeTransactions', { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + trytes = [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ] + + response = self.command(trytes=trytes) + + self.assertDictEqual(response, {'trytes': trytes}) + + self.assertListEqual( + self.adapter.requests, + + [ + { + 'command': 'broadcastTransactions', + 'trytes': trytes, + }, + + { + 'command': 'storeTransactions', + 'trytes': trytes, + }, + ] + ) From 86b191645f3075b6ea8f01b07f9972a84d780771 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 19:42:59 -0500 Subject: [PATCH 102/239] Bypassed redundant validation. --- iota/commands/broadcast_and_store.py | 4 ++-- test/__init__.py | 3 ++- test/commands/broadcast_and_store_test.py | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/broadcast_and_store.py index 9cb6cdc..7914f45 100644 --- a/iota/commands/broadcast_and_store.py +++ b/iota/commands/broadcast_and_store.py @@ -28,8 +28,8 @@ def get_response_filter(self): return BroadcastAndStoreResponseFilter() def send_request(self, request): - BroadcastTransactionsCommand(self.adapter)(trytes=request['trytes']) - return StoreTransactionsCommand(self.adapter)(trytes=request['trytes']) + BroadcastTransactionsCommand(self.adapter).send_request(request) + return StoreTransactionsCommand(self.adapter).send_request(request) class BroadcastAndStoreRequestFilter(RequestFilter): diff --git a/test/__init__.py b/test/__init__.py index 6879f55..b94ba76 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -34,7 +34,8 @@ def seed_response(self, command, response): def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict - self.requests.append(payload) + # Store a snapshot so that we can inspect the request later. + self.requests.append(payload.copy()) try: command = payload['command'] diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index 02ccd7f..7eaa412 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -266,5 +266,5 @@ def test_happy_path(self): 'command': 'storeTransactions', 'trytes': trytes, }, - ] + ], ) From f301d43118a258c4c144cda2b0867b284e1cdb2c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 19:47:21 -0500 Subject: [PATCH 103/239] Document `broadcastAndStore` error response. --- test/__init__.py | 8 +++++++- test/commands/broadcast_and_store_test.py | 22 ++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/test/__init__.py b/test/__init__.py index b94ba76..17bf49a 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -47,7 +47,7 @@ def send_request(self, payload, **kwargs): ) try: - return self.responses[command] + response = self.responses[command] except KeyError: raise BadApiResponse( 'Unknown request {command!r} (expected one of: {seeds!r}).'.format( @@ -55,3 +55,9 @@ def send_request(self, payload, **kwargs): seeds = list(sorted(self.responses.keys())), ), ) + + error = response.get('exception') or response.get('error') + if error: + raise BadApiResponse(error) + + return response diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index 7eaa412..0e78b15 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -6,6 +6,7 @@ import filters as f from filters.test import BaseFilterTestCase +from iota import BadApiResponse from iota.commands.broadcast_and_store import BroadcastAndStoreCommand from iota.filters import Trytes from iota.types import TryteString @@ -268,3 +269,24 @@ def test_happy_path(self): }, ], ) + + def test_broadcast_fails(self): + """ + Calling `broadcastAndStore`, but the initial API call fails. + """ + self.adapter.seed_response('broadcastTransactions', { + 'error': "I'm a teapot.", + }) + + with self.assertRaises(BadApiResponse): + self.command(trytes=[TryteString(self.trytes1)]) + + # The command stopped after the first request failed. + self.assertListEqual( + self.adapter.requests, + + [{ + 'command': 'broadcastTransactions', + 'trytes': [TryteString(self.trytes1)], + }], + ) From d1f98680b2fb6eff5c2d0aab0bacd3a76524e0bc Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 19:48:52 -0500 Subject: [PATCH 104/239] Removed unnecessary error handling. --- test/__init__.py | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 17bf49a..5edccfb 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -37,14 +37,7 @@ def send_request(self, payload, **kwargs): # Store a snapshot so that we can inspect the request later. self.requests.append(payload.copy()) - try: - command = payload['command'] - except KeyError: - raise BadApiResponse( - 'Payload missing `command` value: {payload!r}'.format( - payload = payload, - ), - ) + command = payload['command'] try: response = self.responses[command] From 42c1805045926304b920837ffcd618227eebdf8b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 20:36:41 -0500 Subject: [PATCH 105/239] Added validation to `sendTrytes` request. --- iota/api.py | 6 +- iota/commands/send_trytes.py | 43 ++++ test/__init__.py | 4 +- test/commands/broadcast_and_store_test.py | 2 +- test/commands/send_trytes_test.py | 285 ++++++++++++++++++++++ 5 files changed, 337 insertions(+), 3 deletions(-) create mode 100644 iota/commands/send_trytes.py create mode 100644 test/commands/send_trytes_test.py diff --git a/iota/api.py b/iota/api.py index 3ab6fd0..159cf3d 100644 --- a/iota/api.py +++ b/iota/api.py @@ -520,7 +520,11 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=18): References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes """ - raise NotImplementedError('Not implemented yet.') + raise self.sendTrytes( + trytes = trytes, + depth = depth, + min_weight_magnitude = min_weight_magnitude, + ) def broadcast_and_store(self, trytes): # type: (Iterable[TryteString]) -> List[TryteString] diff --git a/iota/commands/send_trytes.py b/iota/commands/send_trytes.py new file mode 100644 index 0000000..dbf72e4 --- /dev/null +++ b/iota/commands/send_trytes.py @@ -0,0 +1,43 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from iota.commands import FilterCommand, RequestFilter +from iota.filters import Trytes + +__all__ = [ + 'SendTrytesCommand', +] + + +class SendTrytesCommand(FilterCommand): + """ + Executes `sendTrytes` extended API command. + + See :py:method:`iota.api.IotaApi.send_trytes` for more info. + """ + command = 'sendTrytes' + + def get_request_filter(self): + return SendTrytesRequestFilter() + + def get_response_filter(self): + pass + + +class SendTrytesRequestFilter(RequestFilter): + def __init__(self): + super(SendTrytesRequestFilter, self).__init__( + { + 'depth': f.Type(int) | f.Min(1), + + 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), + + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + }, + + allow_missing_keys = { + 'min_weight_magnitude', + }, + ) diff --git a/test/__init__.py b/test/__init__.py index 5edccfb..664bcda 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -8,7 +8,9 @@ class MockAdapter(BaseAdapter): - """An adapter for StrictIota that always returns a mocked response.""" + """ + An adapter for IotaApi that always returns a mocked response. + """ supported_protocols = ('mock',) # noinspection PyUnusedLocal diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index 0e78b15..e2320db 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -281,7 +281,7 @@ def test_broadcast_fails(self): with self.assertRaises(BadApiResponse): self.command(trytes=[TryteString(self.trytes1)]) - # The command stopped after the first request failed. + #w The command stopped after the first request failed. self.assertListEqual( self.adapter.requests, diff --git a/test/commands/send_trytes_test.py b/test/commands/send_trytes_test.py new file mode 100644 index 0000000..e81dcfa --- /dev/null +++ b/test/commands/send_trytes_test.py @@ -0,0 +1,285 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase +from iota.commands.send_trytes import SendTrytesCommand +from iota.filters import Trytes +from iota.types import TryteString +from six import binary_type, text_type +from test import MockAdapter + + +class SendTrytesRequestFilterTestCase(BaseFilterTestCase): + filter_type = SendTrytesCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(SendTrytesRequestFilterTestCase, self).setUp() + + # Define a few valid values that we can reuse across tests. + self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' + self.trytes2 =\ + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + + def test_pass_happy_path(self): + """ + The incoming request is valid. + """ + request = { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + + 'depth': 100, + 'min_weight_magnitude': 18, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + 'trytes': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + + # These values still have to be ints. + 'depth': 100, + 'min_weight_magnitude': 18, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + + 'depth': 100, + 'min_weight_magnitude': 18, + }, + ) + + def test_pass_min_weight_magnitude_missing(self): + """ + ``min_weight_magnitude`` is optional. + """ + filter_ = self._filter({ + 'trytes': [TryteString(self.trytes1)], + 'depth': 100, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'trytes': [TryteString(self.trytes1)], + 'depth': 100, + + # Default value is used if not included in request. + 'min_weight_magnitude': 18, + } + ) + + def test_fail_empty(self): + """ + The request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + } + ) + + def test_fail_unexpected_parameters(self): + """The incoming value contains unexpected parameters.""" + self.assertFilterErrors( + { + 'trytes': [TryteString(self.trytes1)], + 'depth': 100, + 'min_weight_magnitude': 18, + + # Aw, and you were doing so well! + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_trytes_null(self): + """`trytes` is null.""" + self.assertFilterErrors( + { + 'trytes': None, + 'depth': 100, + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_trytes_wrong_type(self): + """`trytes` is not an array.""" + self.assertFilterErrors( + { + # `trytes` has to be an array, even if there's only one + # TryteString. + 'trytes': TryteString(self.trytes1), + 'depth': 100, + }, + + { + 'trytes': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_trytes_empty(self): + """`trytes` is an array, but it's empty.""" + self.assertFilterErrors( + { + 'trytes': [], + 'depth': 100, + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_trytes_contents_invalid(self): + """`trytes` is an array, but it contains invalid values.""" + self.assertFilterErrors( + { + 'trytes': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes2), + + 2130706433, + ], + + 'depth': 100, + }, + + { + 'trytes.0': [f.NotEmpty.CODE_EMPTY], + 'trytes.1': [f.Type.CODE_WRONG_TYPE], + 'trytes.2': [f.Type.CODE_WRONG_TYPE], + 'trytes.3': [f.Required.CODE_EMPTY], + 'trytes.4': [Trytes.CODE_NOT_TRYTES], + 'trytes.6': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_depth_float(self): + """`depth` is a float.""" + self.assertFilterErrors( + { + 'depth': 100.0, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_depth_string(self): + """`depth` is a string.""" + self.assertFilterErrors( + { + 'depth': '100', + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_depth_too_small(self): + """`depth` is less than 1.""" + self.assertFilterErrors( + { + 'depth': 0, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'depth': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_min_weight_magnitude_float(self): + """`min_weight_magnitude` is a float.""" + self.assertFilterErrors( + { + # I don't care if the fpart is empty; it's still not an int! + 'min_weight_magnitude': 20.0, + + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_min_weight_magnitude_string(self): + """`min_weight_magnitude` is a string.""" + self.assertFilterErrors( + { + # For want of an int cast, the transaction was lost. + 'min_weight_magnitude': '20', + + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_min_weight_magnitude_too_small(self): + """`min_weight_magnitude` is less than 18.""" + self.assertFilterErrors( + { + 'min_weight_magnitude': 17, + + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'min_weight_magnitude': [f.Min.CODE_TOO_SMALL], + }, + ) From 6ace5b7d4d7a26ba59a1bb8c1ece56653772b1f0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 20:41:19 -0500 Subject: [PATCH 106/239] Updated `broadcastAndStore` with correct response. --- iota/commands/broadcast_and_store.py | 11 +-- test/commands/broadcast_and_store_test.py | 70 ++------------------ test/commands/broadcast_transactions_test.py | 18 ----- 3 files changed, 7 insertions(+), 92 deletions(-) diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/broadcast_and_store.py index 7914f45..8367090 100644 --- a/iota/commands/broadcast_and_store.py +++ b/iota/commands/broadcast_and_store.py @@ -3,7 +3,7 @@ unicode_literals import filters as f -from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.commands import FilterCommand, RequestFilter from iota.commands.broadcast_transactions import BroadcastTransactionsCommand from iota.commands.store_transactions import StoreTransactionsCommand from iota.filters import Trytes @@ -25,7 +25,7 @@ def get_request_filter(self): return BroadcastAndStoreRequestFilter() def get_response_filter(self): - return BroadcastAndStoreResponseFilter() + pass def send_request(self, request): BroadcastTransactionsCommand(self.adapter).send_request(request) @@ -37,10 +37,3 @@ def __init__(self): super(BroadcastAndStoreRequestFilter, self).__init__({ 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), }) - - -class BroadcastAndStoreResponseFilter(ResponseFilter): - def __init__(self): - super(BroadcastAndStoreResponseFilter, self).__init__({ - 'trytes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), - }) diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index e2320db..31f17c9 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -59,7 +59,7 @@ def test_pass_compatible_types(self): filter_.cleaned_data, # The values are converted into TryteStrings so that they can be - # sent to the node. + # sent to the node. { 'trytes': [ TryteString(self.trytes1), @@ -110,7 +110,7 @@ def test_fail_trytes_wrong_type(self): self.assertFilterErrors( { # `trytes` has to be an array, even if there's only one - # TryteString. + # TryteString. 'trytes': TryteString(self.trytes1), }, @@ -143,7 +143,7 @@ def test_trytes_contents_invalid(self): b'not valid trytes', # This is actually valid; I just added it to make sure the - # filter isn't cheating! + # filter isn't cheating! TryteString(self.trytes2), 2130706433, @@ -160,61 +160,6 @@ def test_trytes_contents_invalid(self): }, ) -class BroadcastAndStoreResponseFilterTestCase(BaseFilterTestCase): - filter_type = BroadcastAndStoreCommand(MockAdapter()).get_response_filter - skip_value_check = True - - # noinspection SpellCheckingInspection - def setUp(self): - super(BroadcastAndStoreResponseFilterTestCase, self).setUp() - - # Define a few valid values that we can reuse across tests. - self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' - self.trytes2 =\ - b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' - - def test_pass_happy_path(self): - """The incoming response contains valid values.""" - # Responses from the node arrive as strings. - filter_ = self._filter({ - 'trytes': [ - text_type(self.trytes1, 'ascii'), - text_type(self.trytes2, 'ascii'), - ], - }) - - self.assertFilterPasses(filter_) - - # The filter converts them into TryteStrings. - self.assertDictEqual( - filter_.cleaned_data, - - { - 'trytes': [ - TryteString(self.trytes1), - TryteString(self.trytes2), - ], - }, - ) - - def test_pass_correct_types(self): - """ - The incoming response already contains correct types. - - This scenario is highly unusual, but who's complaining? - """ - response = { - 'trytes': [ - TryteString(self.trytes1), - TryteString(self.trytes2), - ] - } - - filter_ = self._filter(response) - - self.assertFilterPasses(filter_) - self.assertDictEqual(filter_.cleaned_data, response) - class BroadcastAndStoreCommandTestCase(TestCase): # noinspection SpellCheckingInspection @@ -238,12 +183,7 @@ def test_happy_path(self): ], }) - self.adapter.seed_response('storeTransactions', { - 'trytes': [ - text_type(self.trytes1, 'ascii'), - text_type(self.trytes2, 'ascii'), - ], - }) + self.adapter.seed_response('storeTransactions', {}) trytes = [ TryteString(self.trytes1), @@ -252,7 +192,7 @@ def test_happy_path(self): response = self.command(trytes=trytes) - self.assertDictEqual(response, {'trytes': trytes}) + self.assertDictEqual(response, {}) self.assertListEqual( self.adapter.requests, diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index e7c6778..65a5157 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -197,21 +197,3 @@ def test_pass_happy_path(self): ], }, ) - - def test_pass_correct_types(self): - """ - The incoming response already contains correct types. - - This scenario is highly unusual, but who's complaining? - """ - response = { - 'trytes': [ - TryteString(self.trytes1), - TryteString(self.trytes2), - ] - } - - filter_ = self._filter(response) - - self.assertFilterPasses(filter_) - self.assertDictEqual(filter_.cleaned_data, response) From 0caaa100ebc0e1ec010327b96a32ae4b99aeaaf7 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 13 Dec 2016 21:27:26 -0500 Subject: [PATCH 107/239] Implemented `sendTrytes`, made code more DRY. --- iota/adapter.py | 28 +- iota/commands/__init__.py | 25 +- iota/commands/broadcast_and_store.py | 28 +- iota/commands/send_trytes.py | 27 ++ test/__init__.py | 14 +- test/commands/attach_to_tangle_test.py | 2 +- test/commands/broadcast_and_store_test.py | 194 ++------- test/commands/send_trytes_test.py | 505 ++++++++++++---------- 8 files changed, 413 insertions(+), 410 deletions(-) diff --git a/iota/adapter.py b/iota/adapter.py index 4f59c54..7b95aea 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -24,7 +24,13 @@ class BadApiResponse(ValueError): """ Indicates that a non-success response was received from the node. """ - pass + def __init__(self, message, request): + # Type: (Text, dict) -> None + super(BadApiResponse, self).__init__(message) + + self.context = { + 'request': request, + } class InvalidUri(ValueError): """ @@ -184,13 +190,19 @@ def send_request(self, payload, **kwargs): raw_content = response.text if not raw_content: - raise BadApiResponse('Empty response from node.') + raise BadApiResponse('Empty response from node.', payload) try: decoded = json.loads(raw_content) # type: dict # :bc: py2k doesn't have JSONDecodeError except ValueError: - raise BadApiResponse('Non-JSON response from node: ' + raw_content) + raise BadApiResponse( + message = 'Non-JSON response from node: {raw_content}'.format( + raw_content = raw_content, + ), + + request = payload, + ) try: # Response always has 200 status, even for errors/exceptions, so the @@ -199,10 +211,16 @@ def send_request(self, payload, **kwargs): # :see:`https://github.com/iotaledger/iri/issues/12` error = decoded.get('exception') or decoded.get('error') except AttributeError: - raise BadApiResponse('Invalid response from node: ' + raw_content) + raise BadApiResponse( + message = 'Invalid response from node: {raw_content}'.format( + raw_content = raw_content, + ), + + request = payload, + ) if error: - raise BadApiResponse(error) + raise BadApiResponse(error, payload) return decoded diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 713884a..215da08 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -63,10 +63,20 @@ class BaseCommand(with_metaclass(CommandMeta)): """An API command ready to send to the node.""" command = None # Text - def __init__(self, adapter): - # type: (BaseAdapter) -> None + def __init__(self, adapter, prepare_request=True): + # type: (BaseAdapter, bool) -> None + """ + :param adapter: + Adapter that will send request payloads to the node. + + :param prepare_request: + Whether to prepare the request before sending it. + Generally, this should be set to ``True``. + """ self.adapter = adapter + self.prepare_request = prepare_request + self.called = False self.request = None # type: dict self.response = None # type: dict @@ -79,11 +89,12 @@ def __call__(self, **kwargs): self.request = kwargs - replacement = self._prepare_request(self.request) - if replacement is not None: - self.request = replacement + if self.prepare_request: + replacement = self._prepare_request(self.request) + if replacement is not None: + self.request = replacement - self.response = self.send_request(self.request) + self.response = self._send_request(self.request) replacement = self._prepare_response(self.response) if replacement is not None: @@ -102,7 +113,7 @@ def reset(self): self.request = None # type: dict self.response = None # type: dict - def send_request(self, request): + def _send_request(self, request): # type: (dict) -> dict """ Sends the request object to the adapter and returns the response. diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/broadcast_and_store.py index 8367090..258124d 100644 --- a/iota/commands/broadcast_and_store.py +++ b/iota/commands/broadcast_and_store.py @@ -2,11 +2,9 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -import filters as f -from iota.commands import FilterCommand, RequestFilter +from iota.commands import FilterCommand from iota.commands.broadcast_transactions import BroadcastTransactionsCommand from iota.commands.store_transactions import StoreTransactionsCommand -from iota.filters import Trytes __all__ = [ 'BroadcastAndStoreCommand', @@ -22,18 +20,20 @@ class BroadcastAndStoreCommand(FilterCommand): command = 'broadcastAndStore' def get_request_filter(self): - return BroadcastAndStoreRequestFilter() + pass def get_response_filter(self): pass - def send_request(self, request): - BroadcastTransactionsCommand(self.adapter).send_request(request) - return StoreTransactionsCommand(self.adapter).send_request(request) - - -class BroadcastAndStoreRequestFilter(RequestFilter): - def __init__(self): - super(BroadcastAndStoreRequestFilter, self).__init__({ - 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), - }) + def _send_request(self, request): + bt_command = BroadcastTransactionsCommand( + adapter = self.adapter, + prepare_request = self.prepare_request, + ) + bt_command(**request) + + # `storeTransactions` accepts the exact same request object as + # `broadcastTransactions`, so it's safe to bypass request + # validation here. + return \ + StoreTransactionsCommand(self.adapter, prepare_request=False)(**request) diff --git a/iota/commands/send_trytes.py b/iota/commands/send_trytes.py index dbf72e4..7dc4d28 100644 --- a/iota/commands/send_trytes.py +++ b/iota/commands/send_trytes.py @@ -4,6 +4,10 @@ import filters as f from iota.commands import FilterCommand, RequestFilter +from iota.commands.attach_to_tangle import AttachToTangleCommand +from iota.commands.broadcast_and_store import BroadcastAndStoreCommand +from iota.commands.get_transactions_to_approve import \ + GetTransactionsToApproveCommand from iota.filters import Trytes __all__ = [ @@ -25,6 +29,29 @@ def get_request_filter(self): def get_response_filter(self): pass + def _send_request(self, request): + # Call ``getTransactionsToApprove`` to locate trunk and branch + # transactions so that we can attach the bundle to the Tangle. + gta_command = GetTransactionsToApproveCommand( + adapter = self.adapter, + prepare_request = self.prepare_request, + ) + gta_response = gta_command(depth=request['depth']) + + AttachToTangleCommand(self.adapter, prepare_request=self.prepare_request)( + branch_transaction = gta_response.get('branchTransaction'), + trunk_transaction = gta_response.get('trunkTransaction'), + + min_weight_magnitude = request['min_weight_magnitude'], + trytes = request['trytes'], + ) + + # By this point, ``request['trytes']`` has already been validated, + # so we can bypass validation for `broadcastAndStore`. + return BroadcastAndStoreCommand(self.adapter, prepare_request=False)( + trytes = request['trytes'], + ) + class SendTrytesRequestFilter(RequestFilter): def __init__(self): diff --git a/test/__init__.py b/test/__init__.py index 664bcda..ebe9d16 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -37,7 +37,7 @@ def seed_response(self, command, response): def send_request(self, payload, **kwargs): # type: (dict, dict) -> dict # Store a snapshot so that we can inspect the request later. - self.requests.append(payload.copy()) + self.requests.append(dict(payload)) command = payload['command'] @@ -45,14 +45,18 @@ def send_request(self, payload, **kwargs): response = self.responses[command] except KeyError: raise BadApiResponse( - 'Unknown request {command!r} (expected one of: {seeds!r}).'.format( - command = command, - seeds = list(sorted(self.responses.keys())), + message = ( + 'Unknown request {command!r} (expected one of: {seeds!r}).'.format( + command = command, + seeds = list(sorted(self.responses.keys())), + ) ), + + request = payload, ) error = response.get('exception') or response.get('error') if error: - raise BadApiResponse(error) + raise BadApiResponse(error, payload) return response diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 52cf181..8e651c9 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -31,7 +31,7 @@ def setUp(self): self.trytes2 =\ b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' - def test_pass_valid_request(self): + def test_pass_happy_path(self): """The incoming request is valid.""" request = { 'trunk_transaction': TransactionId(self.txn_id), diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index 31f17c9..ea77109 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -4,163 +4,13 @@ from unittest import TestCase -import filters as f -from filters.test import BaseFilterTestCase from iota import BadApiResponse from iota.commands.broadcast_and_store import BroadcastAndStoreCommand -from iota.filters import Trytes from iota.types import TryteString -from six import binary_type, text_type +from six import text_type from test import MockAdapter -class BroadcastAndStoreRequestFilterTestCase(BaseFilterTestCase): - filter_type = BroadcastAndStoreCommand(MockAdapter()).get_request_filter - skip_value_check = True - - # noinspection SpellCheckingInspection - def setUp(self): - super(BroadcastAndStoreRequestFilterTestCase, self).setUp() - - # Define a few valid values that we can reuse across tests. - self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' - self.trytes2 =\ - b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' - - def test_pass_happy_path(self): - """The incoming request is valid.""" - request = { - 'trytes': [ - TryteString(self.trytes1), - TryteString(self.trytes2), - ], - } - - filter_ = self._filter(request) - - self.assertFilterPasses(filter_) - self.assertDictEqual(filter_.cleaned_data, request) - - def test_pass_compatible_types(self): - """ - The incoming request contains values that can be converted into the - expected types. - """ - # Any values that can be converted into TryteStrings are accepted. - filter_ = self._filter({ - 'trytes': [ - binary_type(self.trytes1), - bytearray(self.trytes2), - ], - }) - - self.assertFilterPasses(filter_) - self.assertDictEqual( - filter_.cleaned_data, - - # The values are converted into TryteStrings so that they can be - # sent to the node. - { - 'trytes': [ - TryteString(self.trytes1), - TryteString(self.trytes2), - ], - }, - ) - - def test_fail_empty(self): - """The incoming request is empty.""" - self.assertFilterErrors( - {}, - - { - 'trytes': [f.FilterMapper.CODE_MISSING_KEY], - }, - ) - - def test_fail_unexpected_parameters(self): - """The incoming value contains unexpected parameters.""" - self.assertFilterErrors( - { - 'trytes': [TryteString(self.trytes1)], - - # Alright buddy, let's see some ID. - 'foo': 'bar', - }, - - { - 'foo': [f.FilterMapper.CODE_EXTRA_KEY], - }, - ) - - def test_fail_trytes_null(self): - """`trytes` is null.""" - self.assertFilterErrors( - { - 'trytes': None, - }, - - { - 'trytes': [f.Required.CODE_EMPTY], - }, - ) - - def test_fail_trytes_wrong_type(self): - """`trytes` is not an array.""" - self.assertFilterErrors( - { - # `trytes` has to be an array, even if there's only one - # TryteString. - 'trytes': TryteString(self.trytes1), - }, - - { - 'trytes': [f.Type.CODE_WRONG_TYPE], - }, - ) - - def test_fail_trytes_empty(self): - """`trytes` is an array, but it's empty.""" - self.assertFilterErrors( - { - 'trytes': [], - }, - - { - 'trytes': [f.Required.CODE_EMPTY], - }, - ) - - def test_trytes_contents_invalid(self): - """`trytes` is an array, but it contains invalid values.""" - self.assertFilterErrors( - { - 'trytes': [ - b'', - text_type(self.trytes1, 'ascii'), - True, - None, - b'not valid trytes', - - # This is actually valid; I just added it to make sure the - # filter isn't cheating! - TryteString(self.trytes2), - - 2130706433, - ], - }, - - { - 'trytes.0': [f.NotEmpty.CODE_EMPTY], - 'trytes.1': [f.Type.CODE_WRONG_TYPE], - 'trytes.2': [f.Type.CODE_WRONG_TYPE], - 'trytes.3': [f.Required.CODE_EMPTY], - 'trytes.4': [Trytes.CODE_NOT_TRYTES], - 'trytes.6': [f.Type.CODE_WRONG_TYPE], - }, - ) - - class BroadcastAndStoreCommandTestCase(TestCase): # noinspection SpellCheckingInspection def setUp(self): @@ -210,9 +60,9 @@ def test_happy_path(self): ], ) - def test_broadcast_fails(self): + def test_broadcast_transactions_fails(self): """ - Calling `broadcastAndStore`, but the initial API call fails. + The `broadcastTransactions` command fails. """ self.adapter.seed_response('broadcastTransactions', { 'error': "I'm a teapot.", @@ -221,7 +71,7 @@ def test_broadcast_fails(self): with self.assertRaises(BadApiResponse): self.command(trytes=[TryteString(self.trytes1)]) - #w The command stopped after the first request failed. + # The command stopped after the first request failed. self.assertListEqual( self.adapter.requests, @@ -230,3 +80,39 @@ def test_broadcast_fails(self): 'trytes': [TryteString(self.trytes1)], }], ) + + def test_store_transactions_fails(self): + """ + The `storeTransactions` command fails. + """ + self.adapter.seed_response('broadcastTransactions', { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + self.adapter.seed_response('storeTransactions', { + 'error': "I'm a teapot.", + }) + + with self.assertRaises(BadApiResponse): + self.command(trytes=[TryteString(self.trytes1)]) + + # The `broadcastTransactions` command was still executed; there is + # no way to execute these commands atomically. + self.assertListEqual( + self.adapter.requests, + + [ + { + 'command': 'broadcastTransactions', + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'command': 'storeTransactions', + 'trytes': [TryteString(self.trytes1)], + }, + ], + ) diff --git a/test/commands/send_trytes_test.py b/test/commands/send_trytes_test.py index e81dcfa..34580c0 100644 --- a/test/commands/send_trytes_test.py +++ b/test/commands/send_trytes_test.py @@ -2,284 +2,341 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -import filters as f -from filters.test import BaseFilterTestCase +from unittest import TestCase + +from iota import BadApiResponse from iota.commands.send_trytes import SendTrytesCommand -from iota.filters import Trytes -from iota.types import TryteString -from six import binary_type, text_type +from iota.types import TransactionId, TryteString +from six import text_type from test import MockAdapter -class SendTrytesRequestFilterTestCase(BaseFilterTestCase): - filter_type = SendTrytesCommand(MockAdapter()).get_request_filter - skip_value_check = True - +class SendTrytesCommandTestCase(TestCase): # noinspection SpellCheckingInspection def setUp(self): - super(SendTrytesRequestFilterTestCase, self).setUp() + super(SendTrytesCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = SendTrytesCommand(self.adapter) # Define a few valid values that we can reuse across tests. self.trytes1 = b'RBTC9D9DCDQAEASBYBCCKBFA' self.trytes2 =\ b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' - def test_pass_happy_path(self): + self.transaction1 = ( + b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' + b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' + ) + + self.transaction2 = ( + b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' + b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' + ) + + def test_happy_path(self): """ - The incoming request is valid. + Successful invocation of `sendTrytes`. """ - request = { + self.adapter.seed_response('getTransactionsToApprove', { + 'trunkTransaction': text_type(self.transaction1, 'ascii'), + 'branchTransaction': text_type(self.transaction2, 'ascii'), + }) + + self.adapter.seed_response('attachToTangle', { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + self.adapter.seed_response('broadcastTransactions', { 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) + + self.adapter.seed_response('storeTransactions', {}) + + response = self.command( + trytes = [ TryteString(self.trytes1), TryteString(self.trytes2), ], - 'depth': 100, - 'min_weight_magnitude': 18, - } - - filter_ = self._filter(request) + depth = 100, + min_weight_magnitude = 18, + ) - self.assertFilterPasses(filter_) - self.assertDictEqual(filter_.cleaned_data, request) + self.assertDictEqual(response, {}) - def test_pass_compatible_types(self): - """ - The request contains values that can be converted to the expected - types. - """ - filter_ = self._filter({ - 'trytes': [ - binary_type(self.trytes1), - bytearray(self.trytes2), - ], + self.assertListEqual( + self.adapter.requests, - # These values still have to be ints. - 'depth': 100, - 'min_weight_magnitude': 18, - }) + [ + { + 'command': 'getTransactionsToApprove', + 'depth': 100, + }, - self.assertFilterPasses(filter_) - self.assertDictEqual( - filter_.cleaned_data, + { + 'command': 'attachToTangle', - { - 'trytes': [ - TryteString(self.trytes1), - TryteString(self.trytes2), - ], + 'trunk_transaction': TransactionId(self.transaction1), + 'branch_transaction': TransactionId(self.transaction2), + 'min_weight_magnitude': 18, - 'depth': 100, - 'min_weight_magnitude': 18, - }, - ) + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, - def test_pass_min_weight_magnitude_missing(self): - """ - ``min_weight_magnitude`` is optional. - """ - filter_ = self._filter({ - 'trytes': [TryteString(self.trytes1)], - 'depth': 100, - }) + { + 'command': 'broadcastTransactions', - self.assertFilterPasses(filter_) - self.assertDictEqual( - filter_.cleaned_data, + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, - { - 'trytes': [TryteString(self.trytes1)], - 'depth': 100, + { + 'command': 'storeTransactions', - # Default value is used if not included in request. - 'min_weight_magnitude': 18, - } + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ], ) - def test_fail_empty(self): + def test_get_transactions_to_approve_fails(self): """ - The request is empty. + The `getTransactionsToApprove` call fails. """ - self.assertFilterErrors( - {}, + self.adapter.seed_response('getTransactionsToApprove', { + 'error': "I'm a teapot.", + }) - { - 'trytes': [f.FilterMapper.CODE_MISSING_KEY], - 'depth': [f.FilterMapper.CODE_MISSING_KEY], - } - ) + with self.assertRaises(BadApiResponse): + self.command( + trytes = [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], - def test_fail_unexpected_parameters(self): - """The incoming value contains unexpected parameters.""" - self.assertFilterErrors( - { - 'trytes': [TryteString(self.trytes1)], - 'depth': 100, - 'min_weight_magnitude': 18, - - # Aw, and you were doing so well! - 'foo': 'bar', - }, - - { - 'foo': [f.FilterMapper.CODE_EXTRA_KEY], - }, - ) + depth = 100, + min_weight_magnitude = 18, + ) - def test_fail_trytes_null(self): - """`trytes` is null.""" - self.assertFilterErrors( - { - 'trytes': None, - 'depth': 100, - }, - - { - 'trytes': [f.Required.CODE_EMPTY], - }, - ) + # As soon as a request fails, the process halts. + # Note that this operation is not atomic! + self.assertListEqual( + self.adapter.requests, - def test_fail_trytes_wrong_type(self): - """`trytes` is not an array.""" - self.assertFilterErrors( - { - # `trytes` has to be an array, even if there's only one - # TryteString. - 'trytes': TryteString(self.trytes1), - 'depth': 100, - }, - - { - 'trytes': [f.Type.CODE_WRONG_TYPE], - }, + [ + { + 'command': 'getTransactionsToApprove', + 'depth': 100, + }, + ], ) - def test_fail_trytes_empty(self): - """`trytes` is an array, but it's empty.""" - self.assertFilterErrors( - { - 'trytes': [], - 'depth': 100, - }, - - { - 'trytes': [f.Required.CODE_EMPTY], - }, - ) + def test_attach_to_tangle_fails(self): + """ + The `attachToTangle` call fails. + """ + self.adapter.seed_response('getTransactionsToApprove', { + 'trunkTransaction': text_type(self.transaction1, 'ascii'), + 'branchTransaction': text_type(self.transaction2, 'ascii'), + }) - def test_trytes_contents_invalid(self): - """`trytes` is an array, but it contains invalid values.""" - self.assertFilterErrors( - { - 'trytes': [ - b'', - text_type(self.trytes1, 'ascii'), - True, - None, - b'not valid trytes', - - # This is actually valid; I just added it to make sure the - # filter isn't cheating! - TryteString(self.trytes2), + self.adapter.seed_response('attachToTangle', { + 'error': "I'm a teapot.", + }) - 2130706433, + with self.assertRaises(BadApiResponse): + self.command( + trytes = [ + TryteString(self.trytes1), + TryteString(self.trytes2), ], - 'depth': 100, - }, - - { - 'trytes.0': [f.NotEmpty.CODE_EMPTY], - 'trytes.1': [f.Type.CODE_WRONG_TYPE], - 'trytes.2': [f.Type.CODE_WRONG_TYPE], - 'trytes.3': [f.Required.CODE_EMPTY], - 'trytes.4': [Trytes.CODE_NOT_TRYTES], - 'trytes.6': [f.Type.CODE_WRONG_TYPE], - }, - ) - - def test_fail_depth_float(self): - """`depth` is a float.""" - self.assertFilterErrors( - { - 'depth': 100.0, - 'trytes': [TryteString(self.trytes1)], - }, - - { - 'depth': [f.Type.CODE_WRONG_TYPE], - }, + depth = 100, + min_weight_magnitude = 18, + ) + + # As soon as a request fails, the process halts. + # Note that this operation is not atomic! + self.assertListEqual( + self.adapter.requests, + + [ + { + 'command': 'getTransactionsToApprove', + 'depth': 100, + }, + + { + 'command': 'attachToTangle', + + 'trunk_transaction': TransactionId(self.transaction1), + 'branch_transaction': TransactionId(self.transaction2), + 'min_weight_magnitude': 18, + + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ], ) - def test_fail_depth_string(self): - """`depth` is a string.""" - self.assertFilterErrors( - { - 'depth': '100', - 'trytes': [TryteString(self.trytes1)], - }, - - { - 'depth': [f.Type.CODE_WRONG_TYPE], - }, - ) + def test_broadcast_transactions_fails(self): + """ + The `broadcastTransactions` call fails. + """ + self.adapter.seed_response('getTransactionsToApprove', { + 'trunkTransaction': text_type(self.transaction1, 'ascii'), + 'branchTransaction': text_type(self.transaction2, 'ascii'), + }) - def test_fail_depth_too_small(self): - """`depth` is less than 1.""" - self.assertFilterErrors( - { - 'depth': 0, - 'trytes': [TryteString(self.trytes1)], - }, - - { - 'depth': [f.Min.CODE_TOO_SMALL], - }, - ) + self.adapter.seed_response('attachToTangle', { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) - def test_fail_min_weight_magnitude_float(self): - """`min_weight_magnitude` is a float.""" - self.assertFilterErrors( - { - # I don't care if the fpart is empty; it's still not an int! - 'min_weight_magnitude': 20.0, + self.adapter.seed_response('broadcastTransactions', { + 'error': "I'm a teapot.", + }) - 'depth': 100, - 'trytes': [TryteString(self.trytes1)], - }, + with self.assertRaises(BadApiResponse): + self.command( + trytes = [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], - { - 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], - }, + depth = 100, + min_weight_magnitude = 18, + ) + + # As soon as a request fails, the process halts. + # Note that this operation is not atomic! + self.assertListEqual( + self.adapter.requests, + + [ + { + 'command': 'getTransactionsToApprove', + 'depth': 100, + }, + + { + 'command': 'attachToTangle', + + 'trunk_transaction': TransactionId(self.transaction1), + 'branch_transaction': TransactionId(self.transaction2), + 'min_weight_magnitude': 18, + + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + + { + 'command': 'broadcastTransactions', + + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ], ) - def test_fail_min_weight_magnitude_string(self): - """`min_weight_magnitude` is a string.""" - self.assertFilterErrors( - { - # For want of an int cast, the transaction was lost. - 'min_weight_magnitude': '20', + def test_store_transactions_fails(self): + """ + The `storeTransactions` call fails. + """ + self.adapter.seed_response('getTransactionsToApprove', { + 'trunkTransaction': text_type(self.transaction1, 'ascii'), + 'branchTransaction': text_type(self.transaction2, 'ascii'), + }) - 'depth': 100, - 'trytes': [TryteString(self.trytes1)], - }, + self.adapter.seed_response('attachToTangle', { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) - { - 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], - }, - ) + self.adapter.seed_response('broadcastTransactions', { + 'trytes': [ + text_type(self.trytes1, 'ascii'), + text_type(self.trytes2, 'ascii'), + ], + }) - def test_fail_min_weight_magnitude_too_small(self): - """`min_weight_magnitude` is less than 18.""" - self.assertFilterErrors( - { - 'min_weight_magnitude': 17, + self.adapter.seed_response('storeTransactions', { + 'error': "I'm a teapot.", + }) - 'depth': 100, - 'trytes': [TryteString(self.trytes1)], - }, + with self.assertRaises(BadApiResponse): + self.command( + trytes = [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], - { - 'min_weight_magnitude': [f.Min.CODE_TOO_SMALL], - }, + depth = 100, + min_weight_magnitude = 18, + ) + + self.assertListEqual( + self.adapter.requests, + + [ + { + 'command': 'getTransactionsToApprove', + 'depth': 100, + }, + + { + 'command': 'attachToTangle', + + 'trunk_transaction': TransactionId(self.transaction1), + 'branch_transaction': TransactionId(self.transaction2), + 'min_weight_magnitude': 18, + + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + + { + 'command': 'broadcastTransactions', + + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + + { + 'command': 'storeTransactions', + + 'trytes': [ + TryteString(self.trytes1), + TryteString(self.trytes2), + ], + }, + ], ) From 0ddcc903beeb1389a1f7e17a8daeb03a7c92ea78 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 09:32:31 -0500 Subject: [PATCH 108/239] PyOTA is compatible with Python 3.6. --- .travis.yml | 3 ++- README.rst | 2 +- tox.ini | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.travis.yml b/.travis.yml index 1236c4a..12ee707 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,5 +2,6 @@ language: python python: - "2.7" - "3.5" + - "3.6" install: "pip install ." -script: "nosetests" +script: "setup.py test" diff --git a/README.rst b/README.rst index 98885d5..4895bb0 100644 --- a/README.rst +++ b/README.rst @@ -17,7 +17,7 @@ You can also ask questions on our `dedicated forum`_. ============ Dependencies ============ -PyOTA requires Python v3.5 or v2.7. +PyOTA is compatible with Python 3.6, 3.5 and 2.7. ============ Installation diff --git a/tox.ini b/tox.ini index 9d73524..2f081ee 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py27, py35 +envlist = py27, py35, py36 [testenv] commands = nosetests From 1b2882e22657992de734a71b036aaeca5c777050 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 11:15:39 -0500 Subject: [PATCH 109/239] Implemented trit and tryte conversions for TryteString. --- iota/codecs.py | 32 ++++--- iota/types.py | 101 ++++++++++++++++++---- test/types_test.py | 203 +++++++++++++++++++++++++++++++++++++++------ 3 files changed, 288 insertions(+), 48 deletions(-) diff --git a/iota/codecs.py b/iota/codecs.py index 472cf06..d25d31d 100644 --- a/iota/codecs.py +++ b/iota/codecs.py @@ -13,28 +13,36 @@ class TrytesDecodeError(ValueError): - """Indicates that a tryte string could not be decoded to bytes.""" + """ + Indicates that a tryte string could not be decoded to bytes. + """ pass class TrytesCodec(Codec): - """Codec for converting byte strings into trytes and vice versa.""" + """ + Codec for converting byte strings into trytes, and vice versa. + """ name = 'trytes' # :bc: Without the bytearray cast, Python 2 will populate the dict - # with characters instead of integers. + # with characters instead of integers. # noinspection SpellCheckingInspection alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) - """Used to encode bytes into trytes.""" + """ + Used to encode bytes into trytes. + """ index = dict(zip(alphabet.values(), alphabet.keys())) - """Used to decode trytes into bytes.""" + """ + Used to decode trytes into bytes. + """ @classmethod def get_codec_info(cls): """ Returns information used by the codecs library to configure the - codec for use. + codec for use. """ codec = cls() @@ -46,7 +54,9 @@ def get_codec_info(cls): # noinspection PyShadowingBuiltins def encode(self, input, errors='strict'): - """Encodes a byte string into trytes.""" + """ + Encodes a byte string into trytes. + """ if isinstance(input, memoryview): input = input.tobytes() @@ -56,7 +66,7 @@ def encode(self, input, errors='strict'): )) # :bc: In Python 2, iterating over a byte string yields characters - # instead of integers. + # instead of integers. if not isinstance(input, bytearray): input = bytearray(input) @@ -72,7 +82,9 @@ def encode(self, input, errors='strict'): # noinspection PyShadowingBuiltins def decode(self, input, errors='strict'): - """Decodes a tryte string into bytes.""" + """ + Decodes a tryte string into bytes. + """ if isinstance(input, memoryview): input = input.tobytes() @@ -111,7 +123,7 @@ def decode(self, input, errors='strict'): ) except ValueError: # This combination of trytes yields a value > 255 when - # decoded. Naturally, we can't represent this using ASCII. + # decoded. Naturally, we can't represent this using ASCII. if errors == 'strict': raise TrytesDecodeError( "'{name}' codec can't decode trytes {pair} at position {i}-{j}: " diff --git a/iota/types.py b/iota/types.py index b38975f..8de8464 100644 --- a/iota/types.py +++ b/iota/types.py @@ -3,6 +3,7 @@ unicode_literals from codecs import encode, decode +from itertools import chain from typing import Generator, Optional, Text, Union, List from six import PY2, binary_type @@ -31,25 +32,31 @@ class TryteString(object): @classmethod def from_bytes(cls, bytes_): # type: (Union[binary_type, bytearray]) -> TryteString - """Creates a TryteString from a byte string.""" + """ + Creates a TryteString from an ASCII representation. + """ return cls(encode(bytes_, 'trytes')) def __init__(self, trytes, pad=None): # type: (TrytesCompatible, int) -> None """ - :param trytes: Byte string or bytearray. - :param pad: Ensure at least this many trytes. + :param trytes: + Byte string or bytearray. + + :param pad: + Ensure at least this many trytes. + If there are too few, additional ``Tryte([-1, -1, -1])`` values will be appended to the TryteString. Note: If the TryteString is too long, it will _not_ be - truncated! + truncated! """ super(TryteString, self).__init__() if isinstance(trytes, int): # This is potentially a valid use case, and we might support it - # at some point. + # at some point. raise TypeError( 'Converting {type} to {cls} is not supported.'.format( type = type(trytes).__name__, @@ -59,7 +66,7 @@ def __init__(self, trytes, pad=None): if isinstance(trytes, TryteString): # Create a copy of the incoming TryteString's trytes, to ensure - # we don't modify it when we apply padding. + # we don't modify it when we apply padding. trytes = bytearray(trytes.trytes) else: if not isinstance(trytes, bytearray): @@ -112,10 +119,11 @@ def as_bytes(self, errors='strict'): """ Converts the TryteString into a byte string. - :param errors: How to handle trytes that can't be converted: - - 'strict': raise a TrytesDecodeError. - - 'replace': replace with '?'. - - 'ignore': omit the tryte from the byte string. + :param errors: + How to handle trytes that can't be converted: + - 'strict': raise a TrytesDecodeError. + - 'replace': replace with '?'. + - 'ignore': omit the tryte from the byte string. """ # :bc: In Python 2, `decode` does not accept keyword arguments. return decode(self.trytes, 'trytes', errors) @@ -129,6 +137,63 @@ def as_json(self): """ return self.trytes.decode('ascii') + def as_trytes(self): + """ + Converts the TryteString into a sequence of trytes. + + Each tryte is represented as a list with 3 trit values. + + See :py:method:`as_trits` for more info. + """ + return [ + self._tryte_from_int(TrytesCodec.index[c]) + for c in self.trytes + ] + + def as_trits(self): + """ + Converts the TryteString into a sequence of trit values. + + A trit may have value 1, 0, or -1. + + References: + - https://en.wikipedia.org/wiki/Balanced_ternary + """ + # http://stackoverflow.com/a/952952/5568265#comment4204394_952952 + return list(chain.from_iterable(self.as_trytes())) + + def _tryte_from_int(self, n): + """ + Converts an integer into a tryte. + """ + # For values greater than 13, trigger an overflow. + # E.g., 14 => -13, 15 => -12, etc. + if n > 13: + n -= 27 + + trits = self._trits_from_int(n) + + # Pad the tryte out to 3 trits if necessary. + trits += [0] * (3 - len(trits)) + + return trits + + def _trits_from_int(self, n): + """ + Converts an integer into a sequence of trits. + """ + if n == 0: + return [] + + quotient, remainder = divmod(n, 3) + + if remainder == 2: + # Lend 1 to the next place so we can make this trit negative. + quotient += 1 + remainder = -1 + + return [remainder] + self._trits_from_int(quotient) + def __eq__(self, other): # type: (TrytesCompatible) -> bool if isinstance(other, TryteString): @@ -166,7 +231,9 @@ def __init__(self, trytes): class Tag(TryteString): - """A TryteString that acts as a transaction tag.""" + """ + A TryteString that acts as a transaction tag. + """ LEN = 27 def __init__(self, trytes): @@ -178,7 +245,9 @@ def __init__(self, trytes): class TransactionId(TryteString): - """A TryteString that acts as a transaction ID.""" + """ + A TryteString that acts as a transaction or bundle ID. + """ LEN = 81 def __init__(self, trytes): @@ -190,7 +259,9 @@ def __init__(self, trytes): class Transfer(object): - """A message [to be] published to the Tangle.""" + """ + A message [to be] published to the Tangle. + """ def __init__(self, recipient, value, message=None, tag=None): # type: (Address, int, Optional[TryteString], Optional[Tag]) -> None self.recipient = recipient @@ -200,4 +271,6 @@ def __init__(self, recipient, value, message=None, tag=None): Bundle = List[Transfer] -"""Placeholder for Bundle type in docstrings.""" +""" +Placeholder for Bundle type in docstrings. +""" diff --git a/test/types_test.py b/test/types_test.py index 62e6c2b..1487b8b 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -6,14 +6,16 @@ from six import binary_type -from iota import TrytesDecodeError +from iota import TrytesCodec, TrytesDecodeError from iota.types import Address, Tag, TransactionId, TryteString # noinspection SpellCheckingInspection class TryteStringTestCase(TestCase): def test_from_bytes(self): - """Converting a sequence of bytes into a TryteString""" + """ + Converting a sequence of bytes into a TryteString. + """ self.assertEqual( TryteString.from_bytes(b'Hello, IOTA!').trytes, b'RBTC9D9DCDQAEASBYBCCKBFA', @@ -23,7 +25,9 @@ def test_equality_comparison(self): """Comparing TryteStrings for equality.""" trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') trytes2 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') - trytes3 = TryteString(b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA') + trytes3 = TryteString( + b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA', + ) self.assertTrue(trytes1 == trytes2) self.assertFalse(trytes1 != trytes2) @@ -56,13 +60,13 @@ def test_equality_comparison(self): def test_equality_comparison_error_wrong_type(self): """ Attempting to compare a TryteString with something that is not a - TryteString. + TryteString. """ trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') with self.assertRaises(TypeError): # Comparing against unicode strings is not allowed because it is - # ambiguous how to encode the unicode string for comparison. + # ambiguous how to encode the unicode string for comparison. trytes == 'RBTC9D9DCDQAEASBYBCCKBFA' with self.assertRaises(TypeError): @@ -74,7 +78,9 @@ def test_equality_comparison_error_wrong_type(self): self.assertTrue(trytes is not 'RBTC9D9DCDQAEASBYBCCKBFA') def test_init_from_tryte_string(self): - """Initializing a TryteString from another TryteString.""" + """ + Initializing a TryteString from another TryteString. + """ trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') trytes2 = TryteString(trytes1) @@ -82,7 +88,9 @@ def test_init_from_tryte_string(self): self.assertTrue(trytes1 == trytes2) def test_init_padding(self): - """Apply padding to ensure a TryteString has a minimum length.""" + """ + Apply padding to ensure a TryteString has a minimum length. + """ trytes = TryteString( trytes = b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQL' @@ -103,7 +111,7 @@ def test_init_padding(self): def test_init_from_tryte_string_with_padding(self): """ Initializing a TryteString from another TryteString, and padding - the new one to a specific length. + the new one to a specific length. """ trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') trytes2 = TryteString(trytes1, pad=27) @@ -116,24 +124,30 @@ def test_init_from_tryte_string_with_padding(self): def test_init_error_invalid_characters(self): """ Attempting to initialize a TryteString with a value that contains - invalid characters. + invalid characters. """ with self.assertRaises(ValueError): TryteString(b'not valid') # noinspection PyTypeChecker def test_init_error_int(self): - """Attempting to initialize a TryteString from an int.""" + """ + Attempting to initialize a TryteString from an int. + """ with self.assertRaises(TypeError): TryteString(42) def test_length(self): - """Just like byte strings, TryteStrings have length.""" + """ + Just like byte strings, TryteStrings have length. + """ self.assertEqual(len(TryteString(b'RBTC')), 4) self.assertEqual(len(TryteString(b'RBTC', pad=81)), 81) def test_iterator(self): - """Just like byte strings, you can iterate over TryteStrings.""" + """ + Just like byte strings, you can iterate over TryteStrings. + """ self.assertListEqual( list(TryteString(b'RBTC')), [b'R', b'B', b'T', b'C'], @@ -158,7 +172,7 @@ def test_string_conversion(self): def test_as_bytes_partial_sequence_errors_strict(self): """ Attempting to convert an odd number of trytes into bytes using the - `as_bytes` method with errors='strict'. + `as_bytes` method with errors='strict'. """ trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') @@ -168,7 +182,7 @@ def test_as_bytes_partial_sequence_errors_strict(self): def test_as_bytes_partial_sequence_errors_ignore(self): """ Attempting to convert an odd number of trytes into bytes using the - `as_bytes` method with errors='ignore'. + `as_bytes` method with errors='ignore'. """ trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') @@ -182,7 +196,7 @@ def test_as_bytes_partial_sequence_errors_ignore(self): def test_as_bytes_partial_sequence_errors_replace(self): """ Attempting to convert an odd number of trytes into bytes using the - `as_bytes` method with errors='replace'. + `as_bytes` method with errors='replace'. """ trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') @@ -196,7 +210,7 @@ def test_as_bytes_partial_sequence_errors_replace(self): def test_as_bytes_non_ascii_errors_strict(self): """ Converting a sequence of trytes into bytes using the `as_bytes` - method yields non-ASCII characters, and errors='strict'. + method yields non-ASCII characters, and errors='strict'. """ trytes = TryteString(b'ZJVYUGTDRPDYFGFXMK') @@ -206,7 +220,7 @@ def test_as_bytes_non_ascii_errors_strict(self): def test_as_bytes_non_ascii_errors_ignore(self): """ Converting a sequence of trytes into bytes using the `as_bytes` - method yields non-ASCII characters, and errors='ignore'. + method yields non-ASCII characters, and errors='ignore'. """ trytes = TryteString(b'ZJVYUGTDRPDYFGFXMK') @@ -218,7 +232,7 @@ def test_as_bytes_non_ascii_errors_ignore(self): def test_as_bytes_non_ascii_errors_replace(self): """ Converting a sequence of trytes into bytes using the `as_bytes` - method yields non-ASCII characters, and errors='replace'. + method yields non-ASCII characters, and errors='replace'. """ trytes = TryteString(b'ZJVYUGTDRPDYFGFXMK') @@ -227,11 +241,142 @@ def test_as_bytes_non_ascii_errors_replace(self): b'??\xd2\x80??\xc3??', ) + def test_as_trytes_single_tryte(self): + """ + Converting a single-tryte TryteString into a sequence of tryte + values. + """ + # Fortunately, there's only 27 possible tryte configurations, so + # it's not too painful to test them all. + self.assertListEqual(TryteString(b'9').as_trytes(), [[ 0, 0, 0]]) + self.assertListEqual(TryteString(b'A').as_trytes(), [[ 1, 0, 0]]) + self.assertListEqual(TryteString(b'B').as_trytes(), [[-1, 1, 0]]) + self.assertListEqual(TryteString(b'C').as_trytes(), [[ 0, 1, 0]]) + self.assertListEqual(TryteString(b'D').as_trytes(), [[ 1, 1, 0]]) + self.assertListEqual(TryteString(b'E').as_trytes(), [[-1, -1, 1]]) + self.assertListEqual(TryteString(b'F').as_trytes(), [[ 0, -1, 1]]) + self.assertListEqual(TryteString(b'G').as_trytes(), [[ 1, -1, 1]]) + self.assertListEqual(TryteString(b'H').as_trytes(), [[-1, 0, 1]]) + self.assertListEqual(TryteString(b'I').as_trytes(), [[ 0, 0, 1]]) + self.assertListEqual(TryteString(b'J').as_trytes(), [[ 1, 0, 1]]) + self.assertListEqual(TryteString(b'K').as_trytes(), [[-1, 1, 1]]) + self.assertListEqual(TryteString(b'L').as_trytes(), [[ 0, 1, 1]]) + self.assertListEqual(TryteString(b'M').as_trytes(), [[ 1, 1, 1]]) + self.assertListEqual(TryteString(b'N').as_trytes(), [[-1, -1, -1]]) + self.assertListEqual(TryteString(b'O').as_trytes(), [[ 0, -1, -1]]) + self.assertListEqual(TryteString(b'P').as_trytes(), [[ 1, -1, -1]]) + self.assertListEqual(TryteString(b'Q').as_trytes(), [[-1, 0, -1]]) + self.assertListEqual(TryteString(b'R').as_trytes(), [[ 0, 0, -1]]) + self.assertListEqual(TryteString(b'S').as_trytes(), [[ 1, 0, -1]]) + self.assertListEqual(TryteString(b'T').as_trytes(), [[-1, 1, -1]]) + self.assertListEqual(TryteString(b'U').as_trytes(), [[ 0, 1, -1]]) + self.assertListEqual(TryteString(b'V').as_trytes(), [[ 1, 1, -1]]) + self.assertListEqual(TryteString(b'W').as_trytes(), [[-1, -1, 0]]) + self.assertListEqual(TryteString(b'X').as_trytes(), [[ 0, -1, 0]]) + self.assertListEqual(TryteString(b'Y').as_trytes(), [[ 1, -1, 0]]) + self.assertListEqual(TryteString(b'Z').as_trytes(), [[-1, 0, 0]]) + + def test_as_trytes_mulitple_trytes(self): + """ + Converting a multiple-tryte TryteString into a sequence of + tryte values. + """ + self.assertListEqual( + TryteString(b'ZJVYUGTDRPDYFGFXMK').as_trytes(), + + [ + [-1, 0, 0], + [ 1, 0, 1], + [ 1, 1, -1], + [ 1, -1, 0], + [ 0, 1, -1], + [ 1, -1, 1], + [-1, 1, -1], + [ 1, 1, 0], + [ 0, 0, -1], + [ 1, -1, -1], + [ 1, 1, 0], + [ 1, -1, 0], + [ 0, -1, 1], + [ 1, -1, 1], + [ 0, -1, 1], + [ 0, -1, 0], + [ 1, 1, 1], + [-1, 1, 1], + ], + ) + + def test_as_trits_single_tryte(self): + """ + Converting a single-tryte TryteString into a sequence of trit + values. + """ + # Fortunately, there's only 27 possible tryte configurations, so + # it's not too painful to test them all. + self.assertListEqual(TryteString(b'9').as_trits(), [ 0, 0, 0]) + self.assertListEqual(TryteString(b'A').as_trits(), [ 1, 0, 0]) + self.assertListEqual(TryteString(b'B').as_trits(), [-1, 1, 0]) + self.assertListEqual(TryteString(b'C').as_trits(), [ 0, 1, 0]) + self.assertListEqual(TryteString(b'D').as_trits(), [ 1, 1, 0]) + self.assertListEqual(TryteString(b'E').as_trits(), [-1, -1, 1]) + self.assertListEqual(TryteString(b'F').as_trits(), [ 0, -1, 1]) + self.assertListEqual(TryteString(b'G').as_trits(), [ 1, -1, 1]) + self.assertListEqual(TryteString(b'H').as_trits(), [-1, 0, 1]) + self.assertListEqual(TryteString(b'I').as_trits(), [ 0, 0, 1]) + self.assertListEqual(TryteString(b'J').as_trits(), [ 1, 0, 1]) + self.assertListEqual(TryteString(b'K').as_trits(), [-1, 1, 1]) + self.assertListEqual(TryteString(b'L').as_trits(), [ 0, 1, 1]) + self.assertListEqual(TryteString(b'M').as_trits(), [ 1, 1, 1]) + self.assertListEqual(TryteString(b'N').as_trits(), [-1, -1, -1]) + self.assertListEqual(TryteString(b'O').as_trits(), [ 0, -1, -1]) + self.assertListEqual(TryteString(b'P').as_trits(), [ 1, -1, -1]) + self.assertListEqual(TryteString(b'Q').as_trits(), [-1, 0, -1]) + self.assertListEqual(TryteString(b'R').as_trits(), [ 0, 0, -1]) + self.assertListEqual(TryteString(b'S').as_trits(), [ 1, 0, -1]) + self.assertListEqual(TryteString(b'T').as_trits(), [-1, 1, -1]) + self.assertListEqual(TryteString(b'U').as_trits(), [ 0, 1, -1]) + self.assertListEqual(TryteString(b'V').as_trits(), [ 1, 1, -1]) + self.assertListEqual(TryteString(b'W').as_trits(), [-1, -1, 0]) + self.assertListEqual(TryteString(b'X').as_trits(), [ 0, -1, 0]) + self.assertListEqual(TryteString(b'Y').as_trits(), [ 1, -1, 0]) + self.assertListEqual(TryteString(b'Z').as_trits(), [-1, 0, 0]) + + def test_as_trits_multiple_trytes(self): + """ + Converting a multiple-tryte TryteString into a sequence of trit + values. + """ + self.assertListEqual( + TryteString(b'ZJVYUGTDRPDYFGFXMK').as_trits(), + [ + -1, 0, 0, + 1, 0, 1, + 1, 1, -1, + 1, -1, 0, + 0, 1, -1, + 1, -1, 1, + -1, 1, -1, + 1, 1, 0, + 0, 0, -1, + 1, -1, -1, + 1, 1, 0, + 1, -1, 0, + 0, -1, 1, + 1, -1, 1, + 0, -1, 1, + 0, -1, 0, + 1, 1, 1, + -1, 1, 1, + ], + ) + # noinspection SpellCheckingInspection class AddressTestCase(TestCase): def test_init_automatic_pad(self): - """Addresses are automatically padded to 81 trytes.""" + """ + Addresses are automatically padded to 81 trytes. + """ addy = Address( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' @@ -246,7 +391,9 @@ def test_init_automatic_pad(self): ) def test_init_error_too_long(self): - """Attempting to create an address longer than 81 trytes.""" + """ + Attempting to create an address longer than 81 trytes. + """ with self.assertRaises(ValueError): Address( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' @@ -257,13 +404,17 @@ def test_init_error_too_long(self): # noinspection SpellCheckingInspection class TagTestCase(TestCase): def test_init_automatic_pad(self): - """Tags are automatically padded to 27 trytes.""" + """ + Tags are automatically padded to 27 trytes. + """ tag = Tag(b'COLOREDCOINS') self.assertEqual(tag.trytes, b'COLOREDCOINS999999999999999') def test_init_error_too_long(self): - """Attempting to create a tag longer than 27 trytes.""" + """ + Attempting to create a tag longer than 27 trytes. + """ with self.assertRaises(ValueError): # 28 chars = no va. Tag(b'COLOREDCOINS9999999999999999') @@ -272,7 +423,9 @@ def test_init_error_too_long(self): # noinspection SpellCheckingInspection class TransactionIdTestCase(TestCase): def test_init_automatic_pad(self): - """Transaction IDs are automatically padded to 81 trytes.""" + """ + Transaction IDs are automatically padded to 81 trytes. + """ txn = TransactionId( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' @@ -287,7 +440,9 @@ def test_init_automatic_pad(self): ) def test_init_error_too_long(self): - """Attempting to create a transaction ID longer than 81 trytes.""" + """ + Attempting to create a transaction ID longer than 81 trytes. + """ with self.assertRaises(ValueError): TransactionId( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' From 246f4a325ff61790bd61a11c860a1a4efc7471e2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 13:40:28 -0500 Subject: [PATCH 110/239] General code cleanup. - Made `TryteString.trytes` protected. - Added public functions for converting ints to trytes and trits. - Minor speedup for tryte conversions. --- iota/types.py | 151 +++++++++++++++++++++++++++++---------------- test/types_test.py | 17 +++-- 2 files changed, 106 insertions(+), 62 deletions(-) diff --git a/iota/types.py b/iota/types.py index 8de8464..244c396 100644 --- a/iota/types.py +++ b/iota/types.py @@ -4,7 +4,7 @@ from codecs import encode, decode from itertools import chain -from typing import Generator, Optional, Text, Union, List +from typing import Dict, Generator, Optional, Text, Union, List from six import PY2, binary_type @@ -14,20 +14,71 @@ TrytesCompatible = Union[binary_type, bytearray, 'TryteString'] -class TryteString(object): +def trytes_from_int(n): + # type: (int) -> List[List[int]] """ - A string representation of a sequence of trytes. + Returns a tryte representation of an integer value. + """ + trytes = [] + + while True: + # divmod does weird things if ``n`` is negative. + quotient, remainder = divmod(abs(n), 27) + + sign = -1 if n < 0 else 1 + remainder *= sign + quotient *= sign + + if remainder not in _trytes_dict: + trits = trits_from_int(remainder) + # Pad the tryte out to 3 trits if necessary. + trits += [0] * (3 - len(trits)) + + _trytes_dict[remainder] = trits + + trytes.append(_trytes_dict[remainder]) + + if quotient == 0: + break + + n = quotient + + return trytes + +_trytes_dict = {} # type: Dict[int, List[int]] +""" +Caches tryte values for :py:func:`trytes_from_int`. +""" + + +def trits_from_int(n): + # type: (int) -> List[int] + """ + Returns a trit representation of an integer value. + """ + if n == 0: + return [] + + quotient, remainder = divmod(n, 3) - A trit can be thought of as the ternary version of a bit. It can - have one of three values: 1, 0 or unknown. + if remainder == 2: + # Lend 1 to the next place so we can make this trit negative. + quotient += 1 + remainder = -1 - A tryte can be thought of as the ternary version of a byte. It is a - sequence of 3 trits. + return [remainder] + trits_from_int(quotient) + + +class TryteString(object): + """ + A string representation of a sequence of trytes. A tryte string is similar in concept to Python's byte string, except it has a more limited alphabet. Byte strings are limited to ASCII (256 possible values), while the tryte string alphabet only has 27 characters (one for each possible tryte configuration). + + IMPORTANT: A TryteString does not represent a numeric value! """ @classmethod def from_bytes(cls, bytes_): @@ -46,19 +97,18 @@ def __init__(self, trytes, pad=None): :param pad: Ensure at least this many trytes. - If there are too few, additional ``Tryte([-1, -1, -1])`` values - will be appended to the TryteString. + If there are too few, null trytes will be appended to the + TryteString. Note: If the TryteString is too long, it will _not_ be truncated! """ super(TryteString, self).__init__() - if isinstance(trytes, int): - # This is potentially a valid use case, and we might support it - # at some point. + if isinstance(trytes, (int, float)): raise TypeError( - 'Converting {type} to {cls} is not supported.'.format( + 'Converting {type} is not supported; ' + '{cls} is not a numeric type.'.format( type = type(trytes).__name__, cls = type(self).__name__, ), @@ -67,7 +117,7 @@ def __init__(self, trytes, pad=None): if isinstance(trytes, TryteString): # Create a copy of the incoming TryteString's trytes, to ensure # we don't modify it when we apply padding. - trytes = bytearray(trytes.trytes) + trytes = bytearray(trytes._trytes) else: if not isinstance(trytes, bytearray): trytes = bytearray(trytes) @@ -85,11 +135,11 @@ def __init__(self, trytes, pad=None): if pad: trytes += b'9' * max(0, pad - len(trytes)) - self.trytes = trytes + self._trytes = trytes def __repr__(self): # type: () -> Text - return 'TryteString({trytes!r})'.format(trytes=binary_type(self.trytes)) + return 'TryteString({trytes!r})'.format(trytes=binary_type(self._trytes)) def __bytes__(self): # type: () -> binary_type @@ -97,9 +147,9 @@ def __bytes__(self): Converts the TryteString into a string representation. Note that this method will NOT convert the trytes back into bytes; - use :py:method:`as_bytes` for that. + use :py:meth:`as_bytes` for that. """ - return binary_type(self.trytes) + return binary_type(self._trytes) # :bc: Magic method has a different name in Python 2. if PY2: @@ -107,12 +157,12 @@ def __bytes__(self): def __len__(self): # type: () -> int - return len(self.trytes) + return len(self._trytes) def __iter__(self): # type: () -> Generator[binary_type] # :see: http://stackoverflow.com/a/14267935/ - return (self.trytes[i:i+1] for i in range(len(self))) + return (self._trytes[i:i + 1] for i in range(len(self))) def as_bytes(self, errors='strict'): # type: (Text) -> binary_type @@ -126,7 +176,7 @@ def as_bytes(self, errors='strict'): - 'ignore': omit the tryte from the byte string. """ # :bc: In Python 2, `decode` does not accept keyword arguments. - return decode(self.trytes, 'trytes', errors) + return decode(self._trytes, 'trytes', errors) def as_json(self): # type: () -> Text @@ -135,7 +185,7 @@ def as_json(self): See :py:class:`iota.json.JsonEncoder`. """ - return self.trytes.decode('ascii') + return self._trytes.decode('ascii') def as_trytes(self): """ @@ -143,11 +193,14 @@ def as_trytes(self): Each tryte is represented as a list with 3 trit values. - See :py:method:`as_trits` for more info. + See :py:meth:`as_trits` for more info. + + IMPORTANT: TryteString is not a numeric type, so the result of this + method should not be interpreted as an integer! """ return [ self._tryte_from_int(TrytesCodec.index[c]) - for c in self.trytes + for c in self._trytes ] def as_trits(self): @@ -158,48 +211,40 @@ def as_trits(self): References: - https://en.wikipedia.org/wiki/Balanced_ternary + + IMPORTANT: TryteString is not a numeric type, so the result of this + method should not be interpreted as an integer! """ # http://stackoverflow.com/a/952952/5568265#comment4204394_952952 return list(chain.from_iterable(self.as_trytes())) - def _tryte_from_int(self, n): + @staticmethod + def _tryte_from_int(n): """ - Converts an integer into a tryte. + Converts an integer into a single tryte. + + This method is specialized for TryteStrings: + - The value must fit inside a single tryte. + - If the value is greater than 13, it will trigger an overflow. """ + if n > 26: + raise ValueError('{n} cannot be represented by a single tryte.'.format( + n = n, + )) + # For values greater than 13, trigger an overflow. # E.g., 14 => -13, 15 => -12, etc. if n > 13: n -= 27 - trits = self._trits_from_int(n) - - # Pad the tryte out to 3 trits if necessary. - trits += [0] * (3 - len(trits)) - - return trits - - def _trits_from_int(self, n): - """ - Converts an integer into a sequence of trits. - """ - if n == 0: - return [] - - quotient, remainder = divmod(n, 3) - - if remainder == 2: - # Lend 1 to the next place so we can make this trit negative. - quotient += 1 - remainder = -1 - - return [remainder] + self._trits_from_int(quotient) + return trytes_from_int(n)[0] def __eq__(self, other): # type: (TrytesCompatible) -> bool if isinstance(other, TryteString): - return self.trytes == other.trytes + return self._trytes == other._trytes elif isinstance(other, (binary_type, bytearray)): - return self.trytes == other + return self._trytes == other else: raise TypeError( 'Invalid type for TryteString comparison ' @@ -226,7 +271,7 @@ def __init__(self, trytes): # type: (TrytesCompatible) -> None super(Address, self).__init__(trytes, pad=self.LEN) - if len(self.trytes) > self.LEN: + if len(self._trytes) > self.LEN: raise ValueError('Addresses must be 81 trytes long.') @@ -240,7 +285,7 @@ def __init__(self, trytes): # type: (TrytesCompatible) -> None super(Tag, self).__init__(trytes, pad=self.LEN) - if len(self.trytes) > self.LEN: + if len(self._trytes) > self.LEN: raise ValueError('Tags must be 27 trytes long.') @@ -254,7 +299,7 @@ def __init__(self, trytes): # type: (TrytesCompatible) -> None super(TransactionId, self).__init__(trytes, pad=self.LEN) - if len(self.trytes) > self.LEN: + if len(self._trytes) > self.LEN: raise ValueError('TransactionIds must be 81 trytes long.') diff --git a/test/types_test.py b/test/types_test.py index 1487b8b..6dae7ae 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -4,10 +4,9 @@ from unittest import TestCase -from six import binary_type - -from iota import TrytesCodec, TrytesDecodeError +from iota import TrytesDecodeError from iota.types import Address, Tag, TransactionId, TryteString +from six import binary_type # noinspection SpellCheckingInspection @@ -17,7 +16,7 @@ def test_from_bytes(self): Converting a sequence of bytes into a TryteString. """ self.assertEqual( - TryteString.from_bytes(b'Hello, IOTA!').trytes, + binary_type(TryteString.from_bytes(b'Hello, IOTA!')), b'RBTC9D9DCDQAEASBYBCCKBFA', ) @@ -100,7 +99,7 @@ def test_init_padding(self): ) self.assertEqual( - trytes.trytes, + binary_type(trytes), # Note the additional Tryte([-1, -1, -1]) values appended to the # end of the sequence (represented in ASCII as '9'). @@ -119,7 +118,7 @@ def test_init_from_tryte_string_with_padding(self): self.assertFalse(trytes1 is trytes2) self.assertFalse(trytes1 == trytes2) - self.assertEqual(trytes2.trytes, b'RBTC9D9DCDQAEASBYBCCKBFA999') + self.assertEqual(binary_type(trytes2), b'RBTC9D9DCDQAEASBYBCCKBFA999') def test_init_error_invalid_characters(self): """ @@ -383,7 +382,7 @@ def test_init_automatic_pad(self): ) self.assertEqual( - addy.trytes, + binary_type(addy), # Note the extra 9's added to the end. b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' @@ -409,7 +408,7 @@ def test_init_automatic_pad(self): """ tag = Tag(b'COLOREDCOINS') - self.assertEqual(tag.trytes, b'COLOREDCOINS999999999999999') + self.assertEqual(binary_type(tag), b'COLOREDCOINS999999999999999') def test_init_error_too_long(self): """ @@ -432,7 +431,7 @@ def test_init_automatic_pad(self): ) self.assertEqual( - txn.trytes, + binary_type(txn), # Note the extra 9's added to the end. b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' From b1f6c348c48b2c08814220e8b6a27cb203eefadf Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 13:49:10 -0500 Subject: [PATCH 111/239] Easier to detect patterns in trit/tryte test failures. --- test/types_test.py | 128 ++++++++++++++++++++++++++------------------- 1 file changed, 73 insertions(+), 55 deletions(-) diff --git a/test/types_test.py b/test/types_test.py index 6dae7ae..db9de60 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -4,7 +4,7 @@ from unittest import TestCase -from iota import TrytesDecodeError +from iota import TrytesDecodeError, TrytesCodec from iota.types import Address, Tag, TransactionId, TryteString from six import binary_type @@ -247,33 +247,42 @@ def test_as_trytes_single_tryte(self): """ # Fortunately, there's only 27 possible tryte configurations, so # it's not too painful to test them all. - self.assertListEqual(TryteString(b'9').as_trytes(), [[ 0, 0, 0]]) - self.assertListEqual(TryteString(b'A').as_trytes(), [[ 1, 0, 0]]) - self.assertListEqual(TryteString(b'B').as_trytes(), [[-1, 1, 0]]) - self.assertListEqual(TryteString(b'C').as_trytes(), [[ 0, 1, 0]]) - self.assertListEqual(TryteString(b'D').as_trytes(), [[ 1, 1, 0]]) - self.assertListEqual(TryteString(b'E').as_trytes(), [[-1, -1, 1]]) - self.assertListEqual(TryteString(b'F').as_trytes(), [[ 0, -1, 1]]) - self.assertListEqual(TryteString(b'G').as_trytes(), [[ 1, -1, 1]]) - self.assertListEqual(TryteString(b'H').as_trytes(), [[-1, 0, 1]]) - self.assertListEqual(TryteString(b'I').as_trytes(), [[ 0, 0, 1]]) - self.assertListEqual(TryteString(b'J').as_trytes(), [[ 1, 0, 1]]) - self.assertListEqual(TryteString(b'K').as_trytes(), [[-1, 1, 1]]) - self.assertListEqual(TryteString(b'L').as_trytes(), [[ 0, 1, 1]]) - self.assertListEqual(TryteString(b'M').as_trytes(), [[ 1, 1, 1]]) - self.assertListEqual(TryteString(b'N').as_trytes(), [[-1, -1, -1]]) - self.assertListEqual(TryteString(b'O').as_trytes(), [[ 0, -1, -1]]) - self.assertListEqual(TryteString(b'P').as_trytes(), [[ 1, -1, -1]]) - self.assertListEqual(TryteString(b'Q').as_trytes(), [[-1, 0, -1]]) - self.assertListEqual(TryteString(b'R').as_trytes(), [[ 0, 0, -1]]) - self.assertListEqual(TryteString(b'S').as_trytes(), [[ 1, 0, -1]]) - self.assertListEqual(TryteString(b'T').as_trytes(), [[-1, 1, -1]]) - self.assertListEqual(TryteString(b'U').as_trytes(), [[ 0, 1, -1]]) - self.assertListEqual(TryteString(b'V').as_trytes(), [[ 1, 1, -1]]) - self.assertListEqual(TryteString(b'W').as_trytes(), [[-1, -1, 0]]) - self.assertListEqual(TryteString(b'X').as_trytes(), [[ 0, -1, 0]]) - self.assertListEqual(TryteString(b'Y').as_trytes(), [[ 1, -1, 0]]) - self.assertListEqual(TryteString(b'Z').as_trytes(), [[-1, 0, 0]]) + self.assertDictEqual( + { + chr(c): TryteString(chr(c).encode('ascii')).as_trytes() + for c in TrytesCodec.alphabet.values() + }, + + { + '9': [[ 0, 0, 0]], + 'A': [[ 1, 0, 0]], + 'B': [[-1, 1, 0]], + 'C': [[ 0, 1, 0]], + 'D': [[ 1, 1, 0]], + 'E': [[-1, -1, 1]], + 'F': [[ 0, -1, 1]], + 'G': [[ 1, -1, 1]], + 'H': [[-1, 0, 1]], + 'I': [[ 0, 0, 1]], + 'J': [[ 1, 0, 1]], + 'K': [[-1, 1, 1]], + 'L': [[ 0, 1, 1]], + 'M': [[ 1, 1, 1]], + 'N': [[-1, -1, -1]], + 'O': [[ 0, -1, -1]], + 'P': [[ 1, -1, -1]], + 'Q': [[-1, 0, -1]], + 'R': [[ 0, 0, -1]], + 'S': [[ 1, 0, -1]], + 'T': [[-1, 1, -1]], + 'U': [[ 0, 1, -1]], + 'V': [[ 1, 1, -1]], + 'W': [[-1, -1, 0]], + 'X': [[ 0, -1, 0]], + 'Y': [[ 1, -1, 0]], + 'Z': [[-1, 0, 0]], + }, + ) def test_as_trytes_mulitple_trytes(self): """ @@ -312,33 +321,42 @@ def test_as_trits_single_tryte(self): """ # Fortunately, there's only 27 possible tryte configurations, so # it's not too painful to test them all. - self.assertListEqual(TryteString(b'9').as_trits(), [ 0, 0, 0]) - self.assertListEqual(TryteString(b'A').as_trits(), [ 1, 0, 0]) - self.assertListEqual(TryteString(b'B').as_trits(), [-1, 1, 0]) - self.assertListEqual(TryteString(b'C').as_trits(), [ 0, 1, 0]) - self.assertListEqual(TryteString(b'D').as_trits(), [ 1, 1, 0]) - self.assertListEqual(TryteString(b'E').as_trits(), [-1, -1, 1]) - self.assertListEqual(TryteString(b'F').as_trits(), [ 0, -1, 1]) - self.assertListEqual(TryteString(b'G').as_trits(), [ 1, -1, 1]) - self.assertListEqual(TryteString(b'H').as_trits(), [-1, 0, 1]) - self.assertListEqual(TryteString(b'I').as_trits(), [ 0, 0, 1]) - self.assertListEqual(TryteString(b'J').as_trits(), [ 1, 0, 1]) - self.assertListEqual(TryteString(b'K').as_trits(), [-1, 1, 1]) - self.assertListEqual(TryteString(b'L').as_trits(), [ 0, 1, 1]) - self.assertListEqual(TryteString(b'M').as_trits(), [ 1, 1, 1]) - self.assertListEqual(TryteString(b'N').as_trits(), [-1, -1, -1]) - self.assertListEqual(TryteString(b'O').as_trits(), [ 0, -1, -1]) - self.assertListEqual(TryteString(b'P').as_trits(), [ 1, -1, -1]) - self.assertListEqual(TryteString(b'Q').as_trits(), [-1, 0, -1]) - self.assertListEqual(TryteString(b'R').as_trits(), [ 0, 0, -1]) - self.assertListEqual(TryteString(b'S').as_trits(), [ 1, 0, -1]) - self.assertListEqual(TryteString(b'T').as_trits(), [-1, 1, -1]) - self.assertListEqual(TryteString(b'U').as_trits(), [ 0, 1, -1]) - self.assertListEqual(TryteString(b'V').as_trits(), [ 1, 1, -1]) - self.assertListEqual(TryteString(b'W').as_trits(), [-1, -1, 0]) - self.assertListEqual(TryteString(b'X').as_trits(), [ 0, -1, 0]) - self.assertListEqual(TryteString(b'Y').as_trits(), [ 1, -1, 0]) - self.assertListEqual(TryteString(b'Z').as_trits(), [-1, 0, 0]) + self.assertDictEqual( + { + chr(c): TryteString(chr(c).encode('ascii')).as_trits() + for c in TrytesCodec.alphabet.values() + }, + + { + '9': [ 0, 0, 0], + 'A': [ 1, 0, 0], + 'B': [-1, 1, 0], + 'C': [ 0, 1, 0], + 'D': [ 1, 1, 0], + 'E': [-1, -1, 1], + 'F': [ 0, -1, 1], + 'G': [ 1, -1, 1], + 'H': [-1, 0, 1], + 'I': [ 0, 0, 1], + 'J': [ 1, 0, 1], + 'K': [-1, 1, 1], + 'L': [ 0, 1, 1], + 'M': [ 1, 1, 1], + 'N': [-1, -1, -1], + 'O': [ 0, -1, -1], + 'P': [ 1, -1, -1], + 'Q': [-1, 0, -1], + 'R': [ 0, 0, -1], + 'S': [ 1, 0, -1], + 'T': [-1, 1, -1], + 'U': [ 0, 1, -1], + 'V': [ 1, 1, -1], + 'W': [-1, -1, 0], + 'X': [ 0, -1, 0], + 'Y': [ 1, -1, 0], + 'Z': [-1, 0, 0], + }, + ) def test_as_trits_multiple_trytes(self): """ From dba666cb5fda9f8288a082c94bb2c7809cc92979 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 13:55:06 -0500 Subject: [PATCH 112/239] Improved documentation. --- iota/types.py | 9 ++++ test/types_test.py | 108 ++++++++++++++++++++++----------------------- 2 files changed, 63 insertions(+), 54 deletions(-) diff --git a/iota/types.py b/iota/types.py index 244c396..972103c 100644 --- a/iota/types.py +++ b/iota/types.py @@ -48,6 +48,10 @@ def trytes_from_int(n): _trytes_dict = {} # type: Dict[int, List[int]] """ Caches tryte values for :py:func:`trytes_from_int`. + +There are only 27 possible tryte configurations, so it's a relatively +small amount of memory; the tradeoff is usually worth it for the +reduced CPU load. """ @@ -55,6 +59,11 @@ def trits_from_int(n): # type: (int) -> List[int] """ Returns a trit representation of an integer value. + + References: + - https://dev.to/buntine/the-balanced-ternary-machines-of-soviet-russia + - https://en.wikipedia.org/wiki/Balanced_ternary + - https://rosettacode.org/wiki/Balanced_ternary#Python """ if n == 0: return [] diff --git a/test/types_test.py b/test/types_test.py index db9de60..7832599 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -254,33 +254,33 @@ def test_as_trytes_single_tryte(self): }, { - '9': [[ 0, 0, 0]], - 'A': [[ 1, 0, 0]], - 'B': [[-1, 1, 0]], - 'C': [[ 0, 1, 0]], - 'D': [[ 1, 1, 0]], - 'E': [[-1, -1, 1]], - 'F': [[ 0, -1, 1]], - 'G': [[ 1, -1, 1]], - 'H': [[-1, 0, 1]], - 'I': [[ 0, 0, 1]], - 'J': [[ 1, 0, 1]], - 'K': [[-1, 1, 1]], - 'L': [[ 0, 1, 1]], - 'M': [[ 1, 1, 1]], - 'N': [[-1, -1, -1]], - 'O': [[ 0, -1, -1]], - 'P': [[ 1, -1, -1]], - 'Q': [[-1, 0, -1]], - 'R': [[ 0, 0, -1]], - 'S': [[ 1, 0, -1]], - 'T': [[-1, 1, -1]], - 'U': [[ 0, 1, -1]], - 'V': [[ 1, 1, -1]], - 'W': [[-1, -1, 0]], - 'X': [[ 0, -1, 0]], - 'Y': [[ 1, -1, 0]], - 'Z': [[-1, 0, 0]], + '9': [[ 0, 0, 0]], # 0 + 'A': [[ 1, 0, 0]], # 1 + 'B': [[-1, 1, 0]], # 2 + 'C': [[ 0, 1, 0]], # 3 + 'D': [[ 1, 1, 0]], # 4 + 'E': [[-1, -1, 1]], # 5 + 'F': [[ 0, -1, 1]], # 6 + 'G': [[ 1, -1, 1]], # 7 + 'H': [[-1, 0, 1]], # 8 + 'I': [[ 0, 0, 1]], # 9 + 'J': [[ 1, 0, 1]], # 10 + 'K': [[-1, 1, 1]], # 11 + 'L': [[ 0, 1, 1]], # 12 + 'M': [[ 1, 1, 1]], # 13 + 'N': [[-1, -1, -1]], # -13 (overflow) + 'O': [[ 0, -1, -1]], # -12 + 'P': [[ 1, -1, -1]], # -11 + 'Q': [[-1, 0, -1]], # -10 + 'R': [[ 0, 0, -1]], # -9 + 'S': [[ 1, 0, -1]], # -8 + 'T': [[-1, 1, -1]], # -7 + 'U': [[ 0, 1, -1]], # -6 + 'V': [[ 1, 1, -1]], # -5 + 'W': [[-1, -1, 0]], # -4 + 'X': [[ 0, -1, 0]], # -3 + 'Y': [[ 1, -1, 0]], # -2 + 'Z': [[-1, 0, 0]], # -1 }, ) @@ -328,33 +328,33 @@ def test_as_trits_single_tryte(self): }, { - '9': [ 0, 0, 0], - 'A': [ 1, 0, 0], - 'B': [-1, 1, 0], - 'C': [ 0, 1, 0], - 'D': [ 1, 1, 0], - 'E': [-1, -1, 1], - 'F': [ 0, -1, 1], - 'G': [ 1, -1, 1], - 'H': [-1, 0, 1], - 'I': [ 0, 0, 1], - 'J': [ 1, 0, 1], - 'K': [-1, 1, 1], - 'L': [ 0, 1, 1], - 'M': [ 1, 1, 1], - 'N': [-1, -1, -1], - 'O': [ 0, -1, -1], - 'P': [ 1, -1, -1], - 'Q': [-1, 0, -1], - 'R': [ 0, 0, -1], - 'S': [ 1, 0, -1], - 'T': [-1, 1, -1], - 'U': [ 0, 1, -1], - 'V': [ 1, 1, -1], - 'W': [-1, -1, 0], - 'X': [ 0, -1, 0], - 'Y': [ 1, -1, 0], - 'Z': [-1, 0, 0], + '9': [ 0, 0, 0], # 0 + 'A': [ 1, 0, 0], # 1 + 'B': [-1, 1, 0], # 2 + 'C': [ 0, 1, 0], # 3 + 'D': [ 1, 1, 0], # 4 + 'E': [-1, -1, 1], # 5 + 'F': [ 0, -1, 1], # 6 + 'G': [ 1, -1, 1], # 7 + 'H': [-1, 0, 1], # 8 + 'I': [ 0, 0, 1], # 9 + 'J': [ 1, 0, 1], # 10 + 'K': [-1, 1, 1], # 11 + 'L': [ 0, 1, 1], # 12 + 'M': [ 1, 1, 1], # 13 + 'N': [-1, -1, -1], # -13 (overflow) + 'O': [ 0, -1, -1], # -12 + 'P': [ 1, -1, -1], # -11 + 'Q': [-1, 0, -1], # -10 + 'R': [ 0, 0, -1], # -9 + 'S': [ 1, 0, -1], # -8 + 'T': [-1, 1, -1], # -7 + 'U': [ 0, 1, -1], # -6 + 'V': [ 1, 1, -1], # -5 + 'W': [-1, -1, 0], # -4 + 'X': [ 0, -1, 0], # -3 + 'Y': [ 1, -1, 0], # -2 + 'Z': [-1, 0, 0], # -1 }, ) From 4cab036c86740bd4076f97770033b7051a8c6eea Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 13:56:35 -0500 Subject: [PATCH 113/239] Improved documentation. --- iota/types.py | 1 + 1 file changed, 1 insertion(+) diff --git a/iota/types.py b/iota/types.py index 972103c..80987a3 100644 --- a/iota/types.py +++ b/iota/types.py @@ -23,6 +23,7 @@ def trytes_from_int(n): while True: # divmod does weird things if ``n`` is negative. + # :see: http://stackoverflow.com/q/10063546/ quotient, remainder = divmod(abs(n), 27) sign = -1 if n < 0 else 1 From 31662126319683510c77bb9988834289805ea263 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 14:32:55 -0500 Subject: [PATCH 114/239] Impl'd trit->TryteString conversion. --- iota/types.py | 77 +++++++++++++++++++++++++++- test/types_test.py | 122 +++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 189 insertions(+), 10 deletions(-) diff --git a/iota/types.py b/iota/types.py index 80987a3..02e7e99 100644 --- a/iota/types.py +++ b/iota/types.py @@ -4,7 +4,7 @@ from codecs import encode, decode from itertools import chain -from typing import Dict, Generator, Optional, Text, Union, List +from typing import Dict, Generator, Iterable, Optional, Text, Union, List from six import PY2, binary_type @@ -79,6 +79,16 @@ def trits_from_int(n): return [remainder] + trits_from_int(quotient) +def int_from_trits(trits): + # type: (Iterable[int]) -> int + """ + Converts a sequence of trits into an integer value. + """ + # Normally we'd have to wrap ``enumerate`` inside ``reversed``, but + # balanced ternary puts least significant digits first. + return sum(base * (3 ** power) for power, base in enumerate(trits)) + + class TryteString(object): """ A string representation of a sequence of trytes. @@ -98,6 +108,71 @@ def from_bytes(cls, bytes_): """ return cls(encode(bytes_, 'trytes')) + @classmethod + def from_trytes(cls, trytes): + # type: (Iterable[Iterable[int]]) -> TryteString + """ + Creates a TryteString from a sequence of trytes. + + References: + - :py:func:`trytes_from_int` + - :py:meth:`as_trytes` + """ + chars = bytearray() + + for t in trytes: + converted = int_from_trits(t) + + # :py:meth:`_tryte_from_int` + if converted < 0: + converted += 27 + + chars.append(TrytesCodec.alphabet[converted]) + + return cls(chars) + + @classmethod + def from_trits(cls, trits, pad=False): + # type: (Iterable[int], bool) -> TryteString + """ + Creates a TryteString from a sequence of trits. + + :param trits: + Iterable of trit values (-1, 0, 1). + + :param pad: + How to handle a sequence with length not divisible by 3: + + - ``False`` (default): raise a :py:class:`ValueError`. + - ``True``: pad to a valid length, using null trits. + + Note that this parameter behaves differently than in + :py:meth:`__init__`. + + References: + - :py:func:`int_from_trits` + - :py:meth:`as_trits` + """ + # Allow passing a generator or other non-Sized value to this + # method. + trits = list(trits) + + if pad: + trits += [0] * max(0, 3 - (len(trits) % 3)) + + if len(trits) % 3: + raise ValueError( + 'Cannot convert sequence with length {length} to trytes; ' + 'length must be divisible by 3.'.format( + length = len(trits), + ), + ) + + return cls.from_trytes([ + # :see: http://stackoverflow.com/a/1751478/ + trits[i:i+3] for i in range(0, len(trits), 3) + ]) + def __init__(self, trytes, pad=None): # type: (TrytesCompatible, int) -> None """ diff --git a/test/types_test.py b/test/types_test.py index 7832599..a65857a 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -11,15 +11,6 @@ # noinspection SpellCheckingInspection class TryteStringTestCase(TestCase): - def test_from_bytes(self): - """ - Converting a sequence of bytes into a TryteString. - """ - self.assertEqual( - binary_type(TryteString.from_bytes(b'Hello, IOTA!')), - b'RBTC9D9DCDQAEASBYBCCKBFA', - ) - def test_equality_comparison(self): """Comparing TryteStrings for equality.""" trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') @@ -387,6 +378,119 @@ def test_as_trits_multiple_trytes(self): ], ) + def test_from_bytes(self): + """ + Converting a sequence of bytes into a TryteString. + """ + self.assertEqual( + binary_type(TryteString.from_bytes(b'Hello, IOTA!')), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + def test_from_trytes(self): + """ + Converting a sequence of tryte values into a TryteString. + """ + trytes = [ + [0, 0, -1], + [-1, 1, 0], + [-1, 1, -1], + [0, 1, 0], + [0, 0, 0], + [1, 1, 0], + [0, 0, 0], + [1, 1, 0], + [0, 1, 0], + [1, 1, 0], + [-1, 0, -1], + [1, 0, 0], + [-1, -1, 1], + [1, 0, 0], + [1, 0, -1], + [-1, 1, 0], + [1, -1, 0], + [-1, 1, 0], + [0, 1, 0], + [0, 1, 0], + [-1, 1, 1], + [-1, 1, 0], + [0, -1, 1], + [1, 0, 0], + ] + + self.assertEqual( + binary_type(TryteString.from_trytes(trytes)), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + def test_from_trits(self): + """ + Converting a sequence of trit values into a TryteString. + """ + trits = [ + 0, 0, -1, + -1, 1, 0, + -1, 1, -1, + 0, 1, 0, + 0, 0, 0, + 1, 1, 0, + 0, 0, 0, + 1, 1, 0, + 0, 1, 0, + 1, 1, 0, + -1, 0, -1, + 1, 0, 0, + -1, -1, 1, + 1, 0, 0, + 1, 0, -1, + -1, 1, 0, + 1, -1, 0, + -1, 1, 0, + 0, 1, 0, + 0, 1, 0, + -1, 1, 1, + -1, 1, 0, + 0, -1, 1, + 1, 0, 0, + ] + + self.assertEqual( + binary_type(TryteString.from_trits(trits)), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + def test_from_trits_error_wrong_length(self): + """ + Converting a sequence of trit values with length not divisible by 3 + into a TryteString. + """ + trits = [ + 0, 0, -1, + -1, 1, 0, + -1, 1, -1, + 0, 1, # 0, <- Oops, did you lose something? + ] + + with self.assertRaises(ValueError): + TryteString.from_trits(trits) + + def test_from_trits_wrong_length_padded(self): + """ + Automatically padding a sequence of trit values with length not + divisible by 3 so that it can be converted into a TryteString. + """ + trits = [ + 0, 0, -1, + -1, 1, 0, + -1, 1, -1, + 0, 1, # 0, <- Oops, did you lose something? + ] + + self.assertEqual( + binary_type(TryteString.from_trits(trits, pad=True)), + b'RBTC', + ) + # noinspection SpellCheckingInspection class AddressTestCase(TestCase): From 8e87b6a28a6bcf6f0628570079841f04f1cfdce5 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 14:35:49 -0500 Subject: [PATCH 115/239] Cleaned up imports a bit. --- iota/types.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/iota/types.py b/iota/types.py index 02e7e99..3ea0302 100644 --- a/iota/types.py +++ b/iota/types.py @@ -10,8 +10,22 @@ from iota import TrytesCodec +__all__ = [ + 'Address', + 'Bundle', + 'Tag', + 'TransactionId', + 'TryteString', + 'TrytesCompatible', + 'int_from_trits', + 'trits_from_int', + 'trytes_from_int', +] -TrytesCompatible = Union[binary_type, bytearray, 'TryteString'] + +# Custom types for type hints and docstrings. +Bundle = Iterable['Transfer'] +TrytesCompatible = Union[binary_type, bytearray, 'TryteString'] def trytes_from_int(n): @@ -398,9 +412,3 @@ def __init__(self, recipient, value, message=None, tag=None): self.value = value, self.message = TryteString(message or b'') self.tag = Tag(tag or b'') - - -Bundle = List[Transfer] -""" -Placeholder for Bundle type in docstrings. -""" From 01d7ba3b511cc537f9fec98dda76eb2c4ab6e5a6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 14:41:46 -0500 Subject: [PATCH 116/239] Added types to package-level imports. --- iota/__init__.py | 2 ++ iota/api.py | 3 +-- iota/filters.py | 2 +- iota/types.py | 1 + test/adapter_test.py | 3 +-- test/commands/attach_to_tangle_test.py | 2 +- test/commands/broadcast_and_store_test.py | 3 +-- test/commands/broadcast_transactions_test.py | 2 +- test/commands/find_transactions_test.py | 2 +- test/commands/get_balances_test.py | 2 +- test/commands/get_inclusion_states_test.py | 2 +- test/commands/get_node_info_test.py | 2 +- test/commands/get_tips_test.py | 2 +- test/commands/get_transactions_to_approve_test.py | 2 +- test/commands/get_trytes_test.py | 2 +- test/commands/send_trytes_test.py | 3 +-- test/commands/store_transactions_test.py | 2 +- test/filters_test.py | 2 +- test/types_test.py | 10 ++++++++-- 19 files changed, 27 insertions(+), 22 deletions(-) diff --git a/iota/__init__.py b/iota/__init__.py index 7acdbdb..1b3facf 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -8,6 +8,8 @@ from .codecs import * # Make some imports accessible from the top level of the package. +# Note that order is important, to prevent circular imports. +from .types import * from .adapter import * from .api import * diff --git a/iota/api.py b/iota/api.py index 159cf3d..3c957e6 100644 --- a/iota/api.py +++ b/iota/api.py @@ -4,10 +4,9 @@ from typing import Iterable, List, Optional, Text, Union +from iota import Address, Bundle, Tag, TransactionId, Transfer, TryteString from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry -from iota.types import Address, Bundle, Tag, TransactionId, Transfer, \ - TryteString __all__ = [ 'Iota', diff --git a/iota/filters.py b/iota/filters.py index 8f59933..6ac2933 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -7,8 +7,8 @@ import filters as f from six import binary_type, text_type +from iota import TryteString from iota.adapter import resolve_adapter, InvalidUri -from iota.types import TryteString class NodeUri(f.BaseFilter): diff --git a/iota/types.py b/iota/types.py index 3ea0302..eb06553 100644 --- a/iota/types.py +++ b/iota/types.py @@ -15,6 +15,7 @@ 'Bundle', 'Tag', 'TransactionId', + 'Transfer', 'TryteString', 'TrytesCompatible', 'int_from_trits', diff --git a/test/adapter_test.py b/test/adapter_test.py index 0b2f04a..20ef70c 100644 --- a/test/adapter_test.py +++ b/test/adapter_test.py @@ -10,9 +10,8 @@ from mock import Mock, patch from six import BytesIO, text_type as text -from iota import BadApiResponse, DEFAULT_PORT, InvalidUri +from iota import BadApiResponse, DEFAULT_PORT, InvalidUri, TryteString from iota.adapter import HttpAdapter, resolve_adapter -from iota.types import TryteString class ResolveAdapterTestCase(TestCase): diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/attach_to_tangle_test.py index 8e651c9..e052a27 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/attach_to_tangle_test.py @@ -6,9 +6,9 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type +from iota import TransactionId, TryteString from iota.commands.attach_to_tangle import AttachToTangleCommand from iota.filters import Trytes -from iota.types import TransactionId, TryteString from test import MockAdapter diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/broadcast_and_store_test.py index ea77109..8278350 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/broadcast_and_store_test.py @@ -4,9 +4,8 @@ from unittest import TestCase -from iota import BadApiResponse +from iota import BadApiResponse, TryteString from iota.commands.broadcast_and_store import BroadcastAndStoreCommand -from iota.types import TryteString from six import text_type from test import MockAdapter diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/broadcast_transactions_test.py index 65a5157..efab5ce 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/broadcast_transactions_test.py @@ -6,10 +6,10 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type +from iota import TryteString from iota.commands.broadcast_transactions import \ BroadcastTransactionsCommand from iota.filters import Trytes -from iota.types import TryteString from test import MockAdapter diff --git a/test/commands/find_transactions_test.py b/test/commands/find_transactions_test.py index d483491..fdd850c 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/find_transactions_test.py @@ -6,10 +6,10 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type +from iota import Address, Tag, TransactionId, TryteString from iota.commands.find_transactions import FindTransactionsRequestFilter, \ FindTransactionsCommand from iota.filters import Trytes -from iota.types import Address, Tag, TransactionId, TryteString from test import MockAdapter diff --git a/test/commands/get_balances_test.py b/test/commands/get_balances_test.py index d79d3dc..87c4811 100644 --- a/test/commands/get_balances_test.py +++ b/test/commands/get_balances_test.py @@ -6,9 +6,9 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type +from iota import Address, TryteString from iota.commands.get_balances import GetBalancesCommand from iota.filters import Trytes -from iota.types import Address, TryteString from test import MockAdapter diff --git a/test/commands/get_inclusion_states_test.py b/test/commands/get_inclusion_states_test.py index 6a0cc42..c9ecc6d 100644 --- a/test/commands/get_inclusion_states_test.py +++ b/test/commands/get_inclusion_states_test.py @@ -6,9 +6,9 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type +from iota import TransactionId, TryteString from iota.commands.get_inclusion_states import GetInclusionStatesCommand from iota.filters import Trytes -from iota.types import TransactionId, TryteString from test import MockAdapter diff --git a/test/commands/get_node_info_test.py b/test/commands/get_node_info_test.py index 5d19313..044d074 100644 --- a/test/commands/get_node_info_test.py +++ b/test/commands/get_node_info_test.py @@ -5,8 +5,8 @@ import filters as f from filters.test import BaseFilterTestCase +from iota import TryteString from iota.commands.get_node_info import GetNodeInfoCommand -from iota.types import TryteString from test import MockAdapter diff --git a/test/commands/get_tips_test.py b/test/commands/get_tips_test.py index 0ca5370..d157390 100644 --- a/test/commands/get_tips_test.py +++ b/test/commands/get_tips_test.py @@ -5,8 +5,8 @@ import filters as f from filters.test import BaseFilterTestCase +from iota import Address from iota.commands.get_tips import GetTipsCommand -from iota.types import Address from test import MockAdapter diff --git a/test/commands/get_transactions_to_approve_test.py b/test/commands/get_transactions_to_approve_test.py index 1a14f59..3ea625b 100644 --- a/test/commands/get_transactions_to_approve_test.py +++ b/test/commands/get_transactions_to_approve_test.py @@ -5,9 +5,9 @@ import filters as f from filters.test import BaseFilterTestCase +from iota import TransactionId from iota.commands.get_transactions_to_approve import \ GetTransactionsToApproveCommand -from iota.types import TransactionId from test import MockAdapter diff --git a/test/commands/get_trytes_test.py b/test/commands/get_trytes_test.py index 7257c3f..cc44019 100644 --- a/test/commands/get_trytes_test.py +++ b/test/commands/get_trytes_test.py @@ -6,9 +6,9 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type +from iota import TransactionId, TryteString from iota.commands.get_trytes import GetTrytesCommand from iota.filters import Trytes -from iota.types import TransactionId, TryteString from test import MockAdapter diff --git a/test/commands/send_trytes_test.py b/test/commands/send_trytes_test.py index 34580c0..534613e 100644 --- a/test/commands/send_trytes_test.py +++ b/test/commands/send_trytes_test.py @@ -4,9 +4,8 @@ from unittest import TestCase -from iota import BadApiResponse +from iota import BadApiResponse, TransactionId, TryteString from iota.commands.send_trytes import SendTrytesCommand -from iota.types import TransactionId, TryteString from six import text_type from test import MockAdapter diff --git a/test/commands/store_transactions_test.py b/test/commands/store_transactions_test.py index d92f5c1..5b40de6 100644 --- a/test/commands/store_transactions_test.py +++ b/test/commands/store_transactions_test.py @@ -6,9 +6,9 @@ from filters.test import BaseFilterTestCase from six import binary_type, text_type +from iota import TryteString from iota.commands.store_transactions import StoreTransactionsCommand from iota.filters import Trytes -from iota.types import TryteString from test import MockAdapter diff --git a/test/filters_test.py b/test/filters_test.py index b353418..8ae23ab 100644 --- a/test/filters_test.py +++ b/test/filters_test.py @@ -5,8 +5,8 @@ import filters as f from filters.test import BaseFilterTestCase +from iota import TryteString, TransactionId from iota.filters import NodeUri, Trytes -from iota.types import TryteString, TransactionId class NodeUriTestCase(BaseFilterTestCase): diff --git a/test/types_test.py b/test/types_test.py index a65857a..eeaa272 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -4,8 +4,14 @@ from unittest import TestCase -from iota import TrytesDecodeError, TrytesCodec -from iota.types import Address, Tag, TransactionId, TryteString +from iota import ( + Address, + Tag, + TransactionId, + TryteString, + TrytesCodec, + TrytesDecodeError, +) from six import binary_type From 0443ff65fb439d1000bbbee47753b1078a1df7d3 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 14:49:05 -0500 Subject: [PATCH 117/239] Initial implementation of PyCurl. Note: needs unit tests. --- iota/__init__.py | 3 +++ iota/pycurl.py | 68 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+) create mode 100644 iota/pycurl.py diff --git a/iota/__init__.py b/iota/__init__.py index 1b3facf..0121756 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -13,5 +13,8 @@ from .adapter import * from .api import * +# Load Curl implementation. +import iota.pycurl as curl + # Don't forget to update version number in setup.py! __version__ = '1.0.0' diff --git a/iota/pycurl.py b/iota/pycurl.py new file mode 100644 index 0000000..40fabed --- /dev/null +++ b/iota/pycurl.py @@ -0,0 +1,68 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota import TryteString + + +TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] + + +class Curl(object): + """ + Python implementation of Curl. + + **IMPORTANT: Not thread-safe!** + """ + def __init__(self): + self.reset() + + def absorb(self, trytes): + # type: (TryteString) -> None + """ + Absorb trytes into the sponge. + """ + for i, trit in enumerate(trytes.as_trits()): + self._state[i] = trit + + self._dirty = True + + def squeeze(self): + # type: () -> TryteString + """ + Squeeze trytes from the sponge. + """ + self._transform() + return TryteString.from_trytes(self._state) + + # noinspection PyAttributeOutsideInit + def reset(self): + # type: () -> None + """ + Resets the internal state. + """ + self._state = [] + self._dirty = False + + def _transform(self): + # type: () -> None + """ + Prepares internal state. + """ + if self._dirty: + index = 0 + + for _ in range(27): + temp_state = list(self._state) + + for i in range(729): + self._state[i] = ( + TRUTH_TABLE[ + temp_state[index] + + temp_state[index + (364 if index < 365 else -365)] + ] + * 3 + + 4 + ) + + self._dirty = False From 956d378045405b8d84fe6456674e611f6900fff4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 14:49:49 -0500 Subject: [PATCH 118/239] Minor code cleanup. --- iota/pycurl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iota/pycurl.py b/iota/pycurl.py index 40fabed..1c954e4 100644 --- a/iota/pycurl.py +++ b/iota/pycurl.py @@ -15,7 +15,8 @@ class Curl(object): **IMPORTANT: Not thread-safe!** """ def __init__(self): - self.reset() + self._state = [] + self._dirty = False def absorb(self, trytes): # type: (TryteString) -> None @@ -35,7 +36,6 @@ def squeeze(self): self._transform() return TryteString.from_trytes(self._state) - # noinspection PyAttributeOutsideInit def reset(self): # type: () -> None """ From cea9060e76cc1f41b4dfd5840385544aead7815d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 15:01:32 -0500 Subject: [PATCH 119/239] Slight improvement to docstring. --- iota/pycurl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/pycurl.py b/iota/pycurl.py index 1c954e4..3d90026 100644 --- a/iota/pycurl.py +++ b/iota/pycurl.py @@ -47,7 +47,7 @@ def reset(self): def _transform(self): # type: () -> None """ - Prepares internal state. + Transforms internal state. """ if self._dirty: index = 0 From 954894530847ed1eef828c2f6f4f55a02a3b29d0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 15:13:42 -0500 Subject: [PATCH 120/239] Cleaned up redundant version number. --- iota/__init__.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/iota/__init__.py b/iota/__init__.py index 0121756..ded782e 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -16,5 +16,7 @@ # Load Curl implementation. import iota.pycurl as curl -# Don't forget to update version number in setup.py! -__version__ = '1.0.0' +# :see: http://stackoverflow.com/a/2073599/ +from pkg_resources import require +__version__ = require('PyOTA')[0].version +del require From 58d8829d170300c5749d6b4b533f1e1cd60c64c2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 15:15:06 -0500 Subject: [PATCH 121/239] Fixed fragile package reference. --- docs/conf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/conf.py b/docs/conf.py index 3be81fd..2e4e39b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -57,7 +57,11 @@ # |version| and |release|, also used in various other places throughout the # built documents. # -from iota import __version__ + +# :see: http://stackoverflow.com/a/2073599/ +from pkg_resources import require +__version__ = require('PyOTA')[0].version + # The short X.Y version. version = __version__ # The full version, including alpha/beta/rc tags. From 5b326d9aed86763dbea47d0bd4f7fd7b8188621e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 15:22:12 -0500 Subject: [PATCH 122/239] Enhanced setup.py. - Added Python version check. - Added comments. --- setup.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/setup.py b/setup.py index 3ffcb2b..8419087 100644 --- a/setup.py +++ b/setup.py @@ -9,9 +9,16 @@ from setuptools import setup -with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader - long_description = f.read() +## +# Check Python version. +if version_info[0:2] < (2, 7): + raise EnvironmentError('PyOTA requires Python 2.7 or greater.') + +if (version_info[0] == 3) and (version_info[1] < 5): + raise EnvironmentError('PyOTA requires Python 3.5 or greater.') +## +# Determine dependencies, depending on Python version. dependencies = [ 'filters', 'requests', @@ -21,6 +28,13 @@ if version_info[0:2] < (3, 5): dependencies.append('typing') +## +# Load long description for PyPi. +with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader + long_description = f.read() + +## +# Off we go! setup( name = 'PyOTA', description = 'IOTA API library for Python', From 881f55bf44f86dce981f4ff021761b68d239a5de Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 16:00:03 -0500 Subject: [PATCH 123/239] Removed readthedocs-breaking configuration. --- docs/conf.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/conf.py b/docs/conf.py index 2e4e39b..47fc243 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -58,14 +58,14 @@ # built documents. # -# :see: http://stackoverflow.com/a/2073599/ -from pkg_resources import require -__version__ = require('PyOTA')[0].version - -# The short X.Y version. -version = __version__ -# The full version, including alpha/beta/rc tags. -release = __version__ +# # :see: http://stackoverflow.com/a/2073599/ +# from pkg_resources import require +# __version__ = require('PyOTA')[0].version +# +# # The short X.Y version. +# version = __version__ +# # The full version, including alpha/beta/rc tags. +# release = __version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From c64a7e7d564222d7ba1e58195759bb2f62922b34 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 17 Dec 2016 16:00:41 -0500 Subject: [PATCH 124/239] 3.6 is not available on Travis yet. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 12ee707..7984900 100644 --- a/.travis.yml +++ b/.travis.yml @@ -2,6 +2,6 @@ language: python python: - "2.7" - "3.5" - - "3.6" + - "3.6-dev" install: "pip install ." script: "setup.py test" From e58263326fd44ee4c69e7403a48220db02e4904e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 10:28:20 -0500 Subject: [PATCH 125/239] Added Travis badge to README. Targeting develop brach for now, since we haven't pushed a release to master yet. --- README.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/README.rst b/README.rst index 4895bb0..9a7aa00 100644 --- a/README.rst +++ b/README.rst @@ -1,3 +1,6 @@ +.. image:: https://travis-ci.org/iotaledger/pyota.svg?branch=develop + :target: https://travis-ci.org/iotaledger/pyota + ===== PyOTA ===== From 9cc8fd51a6e6a3a8c9100aea01051eaf9fa42ae9 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 10:31:57 -0500 Subject: [PATCH 126/239] Fixed Travis configuration. --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 7984900..fcadc1c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,4 +4,4 @@ python: - "3.5" - "3.6-dev" install: "pip install ." -script: "setup.py test" +script: "nosetests" From d72110280ece703ac3983107aef246ed974e630b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 10:36:52 -0500 Subject: [PATCH 127/239] Improved compatibility with Python 2.7. --- iota/codecs.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/iota/codecs.py b/iota/codecs.py index d25d31d..9f6f892 100644 --- a/iota/codecs.py +++ b/iota/codecs.py @@ -4,7 +4,7 @@ from codecs import Codec, CodecInfo, register as lookup_function -from six import binary_type +from six import PY3, binary_type __all__ = [ 'TrytesCodec', @@ -46,11 +46,17 @@ def get_codec_info(cls): """ codec = cls() - return CodecInfo( - encode = codec.encode, - decode = codec.decode, - _is_text_encoding = False, - ) + codec_info = { + 'encode': codec.encode, + 'decode': codec.decode, + } + + # In Python 2, all codecs are made equal. + # In Python 3, some codecs are more equal than others. + if PY3: + codec_info['_is_text_encoding'] = False + + return CodecInfo(**codec_info) # noinspection PyShadowingBuiltins def encode(self, input, errors='strict'): From 39bc2be33b588f09f61500353f7a6b06a3dca0e6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 10:42:50 -0500 Subject: [PATCH 128/239] Added placeholder for address checksums. --- iota/types.py | 2 ++ test/types_test.py | 3 +++ 2 files changed, 5 insertions(+) diff --git a/iota/types.py b/iota/types.py index eb06553..12b2fc7 100644 --- a/iota/types.py +++ b/iota/types.py @@ -371,6 +371,8 @@ def __init__(self, trytes): # type: (TrytesCompatible) -> None super(Address, self).__init__(trytes, pad=self.LEN) + self.checksum = None # type: Optional[TryteString] + if len(self._trytes) > self.LEN: raise ValueError('Addresses must be 81 trytes long.') diff --git a/test/types_test.py b/test/types_test.py index eeaa272..5455f38 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -517,6 +517,9 @@ def test_init_automatic_pad(self): b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' ) + # Checksum is not generated automatically. + self.assertIsNone(addy.checksum) + def test_init_error_too_long(self): """ Attempting to create an address longer than 81 trytes. From 3c1e9eb4c79e2a06b363db98d063e80b79184919 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 11:01:52 -0500 Subject: [PATCH 129/239] Added support for taking slices of TryteStrings. --- iota/types.py | 15 ++++++++++++++- test/types_test.py | 13 +++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/iota/types.py b/iota/types.py index 12b2fc7..561b69b 100644 --- a/iota/types.py +++ b/iota/types.py @@ -235,7 +235,7 @@ def __init__(self, trytes, pad=None): if pad: trytes += b'9' * max(0, pad - len(trytes)) - self._trytes = trytes + self._trytes = trytes # type: bytearray def __repr__(self): # type: () -> Text @@ -264,6 +264,19 @@ def __iter__(self): # :see: http://stackoverflow.com/a/14267935/ return (self._trytes[i:i + 1] for i in range(len(self))) + def __getitem__(self, item): + # type: (Union[int, slice]) -> TryteString + new_trytes = bytearray() + + sliced = self._trytes[item] + + if isinstance(sliced, int): + new_trytes.append(sliced) + else: + new_trytes.extend(sliced) + + return TryteString(new_trytes) + def as_bytes(self, errors='strict'): # type: (Text) -> binary_type """ diff --git a/test/types_test.py b/test/types_test.py index 5455f38..c37e8d1 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -73,6 +73,19 @@ def test_equality_comparison_error_wrong_type(self): self.assertFalse(trytes is 'RBTC9D9DCDQAEASBYBCCKBFA') self.assertTrue(trytes is not 'RBTC9D9DCDQAEASBYBCCKBFA') + def test_slice(self): + """ + Taking slices of a TryteString. + """ + ts = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + + self.assertEqual(ts[4], TryteString(b'9')) + self.assertEqual(ts[:4], TryteString(b'RBTC')) + self.assertEqual(ts[:-4], TryteString(b'RBTC9D9DCDQAEASBYBCC')) + self.assertEqual(ts[4:], TryteString(b'9D9DCDQAEASBYBCCKBFA')) + self.assertEqual(ts[-4:], TryteString(b'KBFA')) + self.assertEqual(ts[4:-4:4], TryteString(b'9CEY')) + def test_init_from_tryte_string(self): """ Initializing a TryteString from another TryteString. From 97aad677b052c1259e73588c8b3b3db278132842 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 11:15:27 -0500 Subject: [PATCH 130/239] Extract checksum from address if provided. --- iota/types.py | 21 ++++++++++++++++----- test/types_test.py | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 5 deletions(-) diff --git a/iota/types.py b/iota/types.py index 561b69b..ef9f614 100644 --- a/iota/types.py +++ b/iota/types.py @@ -378,16 +378,27 @@ class Address(TryteString): A TryteString that acts as an address, with support for generating and validating checksums. """ - LEN = 81 + LEN_ADDRESS = 81 + LEN_CHECKSUM = 9 + + checksum = None def __init__(self, trytes): # type: (TrytesCompatible) -> None - super(Address, self).__init__(trytes, pad=self.LEN) + super(Address, self).__init__(trytes, pad=self.LEN_ADDRESS) - self.checksum = None # type: Optional[TryteString] + self.checksum = None + if len(self._trytes) == (self.LEN_ADDRESS + self.LEN_CHECKSUM): + self.checksum = self[self.LEN_ADDRESS:] # type: Optional[TryteString] - if len(self._trytes) > self.LEN: - raise ValueError('Addresses must be 81 trytes long.') + elif len(self._trytes) > self.LEN_ADDRESS: + raise ValueError( + 'Addresses must be either {len_no_checksum} trytes (no checksum), ' + 'or {len_with_checksum} trytes (with checksum).'.format( + len_no_checksum = self.LEN_ADDRESS, + len_with_checksum = self.LEN_ADDRESS + self.LEN_CHECKSUM, + ), + ) class Tag(TryteString): diff --git a/test/types_test.py b/test/types_test.py index c37e8d1..b03a25f 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -539,10 +539,47 @@ def test_init_error_too_long(self): """ with self.assertRaises(ValueError): Address( + # Extra padding at the end is not ignored. + # If it's an address (without checksum), then it must be 81 + # trytes exactly. b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' ) + def test_init_with_checksum(self): + """ + Creating an address with checksum already attached. + """ + addy = Address( + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX' + ) + + self.assertEqual( + binary_type(addy), + + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX', + ) + + self.assertEqual( + binary_type(addy.checksum), + b'FOXM9MUBX', + ) + + def test_init_error_checksum_too_long(self): + """ + Attempting to create an address longer than 90 trytes. + """ + with self.assertRaises(ValueError): + Address( + # Extra padding at the end is not ignored. + # If it's a checksummed address, then it must be 90 trytes + # exactly. + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX9' + ) + # noinspection SpellCheckingInspection class TagTestCase(TestCase): From ba2b3fef1517b37adac72e58549d5570570150c3 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 11:24:53 -0500 Subject: [PATCH 131/239] Added address checksum length validation. --- iota/types.py | 35 +++++++++++++++++++++++++---------- test/types_test.py | 44 +++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 68 insertions(+), 11 deletions(-) diff --git a/iota/types.py b/iota/types.py index ef9f614..bc03550 100644 --- a/iota/types.py +++ b/iota/types.py @@ -12,6 +12,7 @@ __all__ = [ 'Address', + 'AddressChecksum', 'Bundle', 'Tag', 'TransactionId', @@ -378,28 +379,42 @@ class Address(TryteString): A TryteString that acts as an address, with support for generating and validating checksums. """ - LEN_ADDRESS = 81 - LEN_CHECKSUM = 9 - - checksum = None + LEN = 81 def __init__(self, trytes): # type: (TrytesCompatible) -> None - super(Address, self).__init__(trytes, pad=self.LEN_ADDRESS) + super(Address, self).__init__(trytes, pad=self.LEN) self.checksum = None - if len(self._trytes) == (self.LEN_ADDRESS + self.LEN_CHECKSUM): - self.checksum = self[self.LEN_ADDRESS:] # type: Optional[TryteString] + if len(self._trytes) == (self.LEN + AddressChecksum.LEN): + self.checksum = AddressChecksum(self[self.LEN:]) # type: Optional[AddressChecksum] - elif len(self._trytes) > self.LEN_ADDRESS: + elif len(self._trytes) > self.LEN: raise ValueError( 'Addresses must be either {len_no_checksum} trytes (no checksum), ' 'or {len_with_checksum} trytes (with checksum).'.format( - len_no_checksum = self.LEN_ADDRESS, - len_with_checksum = self.LEN_ADDRESS + self.LEN_CHECKSUM, + len_no_checksum = self.LEN, + len_with_checksum = self.LEN + AddressChecksum.LEN, ), ) + # Make the address sans checksum accessible. + self.address = self[:self.LEN] # type: TryteString + + +class AddressChecksum(TryteString): + """ + A TryteString that acts as an address checksum. + """ + LEN = 9 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(AddressChecksum, self).__init__(trytes) + + if len(self._trytes) != self.LEN: + raise ValueError('Address checksums must be exactly 9 trytes.') + class Tag(TryteString): """ diff --git a/test/types_test.py b/test/types_test.py index b03a25f..beb3b42 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -6,6 +6,7 @@ from iota import ( Address, + AddressChecksum, Tag, TransactionId, TryteString, @@ -527,7 +528,16 @@ def test_init_automatic_pad(self): # Note the extra 9's added to the end. b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', + ) + + # This attribute will make more sense once we start working with + # address checksums. + self.assertEqual( + binary_type(addy.address), + + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999', ) # Checksum is not generated automatically. @@ -562,6 +572,13 @@ def test_init_with_checksum(self): b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX', ) + self.assertEqual( + binary_type(addy.address), + + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA', + ) + self.assertEqual( binary_type(addy.checksum), b'FOXM9MUBX', @@ -581,6 +598,31 @@ def test_init_error_checksum_too_long(self): ) +# noinspection SpellCheckingInspection +class AddressChecksumTestCase(TestCase): + def test_init_happy_path(self): + """ + Creating a valid address checksum. + """ + self.assertEqual(binary_type(AddressChecksum(b'FOXM9MUBX')), b'FOXM9MUBX') + + def test_init_error_too_short(self): + """ + Attempting to create an address checksum shorter than 9 trytes. + """ + with self.assertRaises(ValueError): + AddressChecksum(b'FOXM9MUB') + + def test_init_error_too_long(self): + """ + Attempting to create an address checksum longer than 9 trytes. + """ + with self.assertRaises(ValueError): + # Extra padding characters are not ignored. + # If it's an address checksum, it must be 9 trytes exactly. + AddressChecksum(b'FOXM9MUBX9') + + # noinspection SpellCheckingInspection class TagTestCase(TestCase): def test_init_automatic_pad(self): From cb8302ba166ddb435758467046d6c55d6d375754 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 13:31:58 -0500 Subject: [PATCH 132/239] Initial implementation of address checksumming. --- iota/__init__.py | 3 -- iota/curl/__init__.py | 9 ++++ iota/curl/pycurl.py | 99 ++++++++++++++++++++++++++++++++++ iota/pycurl.py | 68 ------------------------ iota/types.py | 87 +++++++++++++++++++++++------- test/types_test.py | 120 ++++++++++++++++++++++++++++++++++++++++-- 6 files changed, 290 insertions(+), 96 deletions(-) create mode 100644 iota/curl/__init__.py create mode 100644 iota/curl/pycurl.py delete mode 100644 iota/pycurl.py diff --git a/iota/__init__.py b/iota/__init__.py index ded782e..aff90ba 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -13,9 +13,6 @@ from .adapter import * from .api import * -# Load Curl implementation. -import iota.pycurl as curl - # :see: http://stackoverflow.com/a/2073599/ from pkg_resources import require __version__ = require('PyOTA')[0].version diff --git a/iota/curl/__init__.py b/iota/curl/__init__.py new file mode 100644 index 0000000..1d325fa --- /dev/null +++ b/iota/curl/__init__.py @@ -0,0 +1,9 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + + +# Load curl library. +# If a compiled c extension is available, we will prefer to load that +# (once implemented). +from .pycurl import * diff --git a/iota/curl/pycurl.py b/iota/curl/pycurl.py new file mode 100644 index 0000000..b88f06a --- /dev/null +++ b/iota/curl/pycurl.py @@ -0,0 +1,99 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Iterable, List, Optional, Union + +__all__ = [ + 'Curl', +] + + +TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] + + +class Curl(object): + """ + Python implementation of Curl. + + **IMPORTANT: Not thread-safe!** + """ + STATE_LEN = 729 + + def __init__(self, state=None): + # type: (Optional[Iterable[int]]) -> None + """ + :param state: + Initial state. + + Note: this has the same effect as calling :py:meth:`absorb` + immediately after initializing the object. + """ + self._state = [0] * self.STATE_LEN # type: List[int] + self._dirty = False + + if state is not None: + self.absorb(state) + + def __getitem__(self, item): + # type: (Union[int, slice]) -> List[int] + """ + Alias for :py:meth:`squeeze`. + """ + return self.squeeze(item) + + def absorb(self, trits): + # type: (Iterable[int]) -> None + """ + Absorb trits into the sponge. + """ + for i, trit in enumerate(trits): + self._state[i] = trit + + self._dirty = True + + def squeeze(self, slice_=None): + # type: (Optional[Union[int, slice]]) -> List[int] + """ + Squeeze trytes from the sponge. + """ + self._transform() + + return ( + list(self._state) + if slice_ is None + else self._state[slice_] + ) + + def reset(self): + # type: () -> None + """ + Resets the internal state. + """ + self._state = [] + self._dirty = False + + def _transform(self): + # type: () -> None + """ + Transforms internal state. + """ + if self._dirty: + index = 0 + + for _ in range(27): + temp_state = list(self._state) + + for pos in range(self.STATE_LEN): + prev_index = index + index += (364 if index < 365 else -365) + + self._state[pos] = ( + TRUTH_TABLE[ + temp_state[prev_index] + + (3 * temp_state[index]) + + 4 + ] + ) + + self._dirty = False diff --git a/iota/pycurl.py b/iota/pycurl.py deleted file mode 100644 index 3d90026..0000000 --- a/iota/pycurl.py +++ /dev/null @@ -1,68 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, \ - unicode_literals - -from iota import TryteString - - -TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] - - -class Curl(object): - """ - Python implementation of Curl. - - **IMPORTANT: Not thread-safe!** - """ - def __init__(self): - self._state = [] - self._dirty = False - - def absorb(self, trytes): - # type: (TryteString) -> None - """ - Absorb trytes into the sponge. - """ - for i, trit in enumerate(trytes.as_trits()): - self._state[i] = trit - - self._dirty = True - - def squeeze(self): - # type: () -> TryteString - """ - Squeeze trytes from the sponge. - """ - self._transform() - return TryteString.from_trytes(self._state) - - def reset(self): - # type: () -> None - """ - Resets the internal state. - """ - self._state = [] - self._dirty = False - - def _transform(self): - # type: () -> None - """ - Transforms internal state. - """ - if self._dirty: - index = 0 - - for _ in range(27): - temp_state = list(self._state) - - for i in range(729): - self._state[i] = ( - TRUTH_TABLE[ - temp_state[index] - + temp_state[index + (364 if index < 365 else -365)] - ] - * 3 - + 4 - ) - - self._dirty = False diff --git a/iota/types.py b/iota/types.py index bc03550..d7f8d54 100644 --- a/iota/types.py +++ b/iota/types.py @@ -9,6 +9,8 @@ from six import PY2, binary_type from iota import TrytesCodec +from iota.curl import Curl + __all__ = [ 'Address', @@ -278,6 +280,43 @@ def __getitem__(self, item): return TryteString(new_trytes) + def __add__(self, other): + # type: (TrytesCompatible) -> TryteString + if isinstance(other, TryteString): + return TryteString(self._trytes + other._trytes) + elif isinstance(other, (binary_type, bytearray)): + return TryteString(self._trytes + other) + else: + raise TypeError( + 'Invalid type for TryteString concatenation ' + '(expected Union[TryteString, {binary_type}, bytearray], ' + 'actual {type}).'.format( + binary_type = binary_type.__name__, + type = type(other).__name__, + ), + ) + + def __eq__(self, other): + # type: (TrytesCompatible) -> bool + if isinstance(other, TryteString): + return self._trytes == other._trytes + elif isinstance(other, (binary_type, bytearray)): + return self._trytes == other + else: + raise TypeError( + 'Invalid type for TryteString comparison ' + '(expected Union[TryteString, {binary_type}, bytearray], ' + 'actual {type}).'.format( + binary_type = binary_type.__name__, + type = type(other).__name__, + ), + ) + + # :bc: In Python 2 this must be defined explicitly. + def __ne__(self, other): + # type: (TrytesCompatible) -> bool + return not (self == other) + def as_bytes(self, errors='strict'): # type: (Text) -> binary_type """ @@ -353,26 +392,6 @@ def _tryte_from_int(n): return trytes_from_int(n)[0] - def __eq__(self, other): - # type: (TrytesCompatible) -> bool - if isinstance(other, TryteString): - return self._trytes == other._trytes - elif isinstance(other, (binary_type, bytearray)): - return self._trytes == other - else: - raise TypeError( - 'Invalid type for TryteString comparison ' - '(expected Union[TryteString, binary_type, bytearray], ' - 'actual {type}).'.format( - type = type(other).__name__, - ), - ) - - # :bc: In Python 2 this must be defined explicitly. - def __ne__(self, other): - # type: (TrytesCompatible) -> bool - return not (self == other) - class Address(TryteString): """ @@ -401,6 +420,34 @@ def __init__(self, trytes): # Make the address sans checksum accessible. self.address = self[:self.LEN] # type: TryteString + def is_valid(self): + # type: () -> bool + """ + Returns whether this address has a valid checksum. + """ + if self.checksum: + return self.checksum == self._generate_checksum() + + return False + + def with_checksum(self): + # type: () -> Address + """ + Returns the address with a valid checksum attached. + """ + return Address(self.address + self._generate_checksum()) + + def _generate_checksum(self): + # type: () -> TryteString + """ + Generates the actual checksum for this address. + """ + return TryteString.from_trits( + # Multiply by 3 because AddressChecksum.LEN is number of trytes, + # but Curl returns trits. + Curl(self.address.as_trits())[:AddressChecksum.LEN * 3] + ) + class AddressChecksum(TryteString): """ diff --git a/test/types_test.py b/test/types_test.py index beb3b42..d82e625 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -18,7 +18,7 @@ # noinspection SpellCheckingInspection class TryteStringTestCase(TestCase): - def test_equality_comparison(self): + def test_comparison(self): """Comparing TryteStrings for equality.""" trytes1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') trytes2 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') @@ -54,26 +54,76 @@ def test_equality_comparison(self): self.assertTrue(trytes3 != bytearray(b'RBTC9D9DCDQAEASBYBCCKBFA')) # noinspection PyTypeChecker - def test_equality_comparison_error_wrong_type(self): + def test_comparison_error_wrong_type(self): """ Attempting to compare a TryteString with something that is not a - TryteString. + TrytesCompatible. """ trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') with self.assertRaises(TypeError): # Comparing against unicode strings is not allowed because it is - # ambiguous how to encode the unicode string for comparison. + # ambiguous how to encode the unicode string into trits (should + # we treat the unicode string as an ASCII representation, or + # should we encode the unicode value into bytes and convert the + # result into trytes?). trytes == 'RBTC9D9DCDQAEASBYBCCKBFA' with self.assertRaises(TypeError): - # We might support this at some point, but not at the moment. + # TryteString is not a numeric type, so comparing against a + # numeric value doesn't make any sense. trytes == 42 # Identity comparison still works though. self.assertFalse(trytes is 'RBTC9D9DCDQAEASBYBCCKBFA') self.assertTrue(trytes is not 'RBTC9D9DCDQAEASBYBCCKBFA') + def test_concatenate(self): + """ + Concatenating TryteStrings with TrytesCompatibles. + """ + trytes1 = TryteString(b'RBTC9D9DCDQA') + trytes2 = TryteString(b'EASBYBCCKBFA') + + concat = trytes1 + trytes2 + self.assertIsInstance(concat, TryteString) + self.assertEqual(binary_type(concat), b'RBTC9D9DCDQAEASBYBCCKBFA') + + # You can also concatenate a TryteString with any TrytesCompatible. + self.assertEqual( + binary_type(trytes1 + b'EASBYBCCKBFA'), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + self.assertEqual( + binary_type(trytes1 + bytearray(b'EASBYBCCKBFA')), + b'RBTC9D9DCDQAEASBYBCCKBFA', + ) + + def test_concatenate_error_wrong_type(self): + """ + Attempting to concatenate a TryteString with something that is not + a TrytesCompatible. + """ + trytes = TryteString(b'RBTC9D9DCDQA') + + with self.assertRaises(TypeError): + # Concatenating unicode strings is not allowed because it is + # ambiguous how to encode the unicode string into trits (should + # we treat the unicode string as an ASCII representation, or + # should we encode the unicode value into bytes and convert the + # result into trytes?). + trytes += 'EASBYBCCKBFA' + + with self.assertRaises(TypeError): + # TryteString is not a numeric type, so adding a numeric value + # doesn't make any sense. + trytes += 42 + + with self.assertRaises(TypeError): + # What is this I don't even.. + trytes += None + def test_slice(self): """ Taking slices of a TryteString. @@ -597,6 +647,66 @@ def test_init_error_checksum_too_long(self): b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX9' ) + def test_checksum_valid(self): + """ + An address is created with a valid checksum. + """ + addy = Address( + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX' + ) + + self.assertTrue(addy.is_valid()) + + self.assertEqual( + binary_type(addy.with_checksum()), + + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX' + ) + + def test_checksum_invalid(self): + """ + An address is created with an invalid checksum. + """ + trytes = ( + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA' + ) + + addy = Address( + trytes + b'FOXM9MUBQ' # <- Last tryte s/b 'X'. + ) + + self.assertFalse(addy.is_valid()) + + self.assertEqual( + binary_type(addy.with_checksum()), + + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX', + ) + + def test_checksum_null(self): + """ + An address is created without a checksum. + """ + trytes = ( + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA' + ) + + addy = Address(trytes) + + self.assertFalse(addy.is_valid()) + + self.assertEqual( + binary_type(addy.with_checksum()), + + b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' + b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX', + ) + # noinspection SpellCheckingInspection class AddressChecksumTestCase(TestCase): From 1d99ce5e753fbb3f5069f6218dba76406813c403 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 14:29:04 -0500 Subject: [PATCH 133/239] Made address checksum tests more robust. --- test/types_test.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/test/types_test.py b/test/types_test.py index d82e625..27bfe2e 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -670,12 +670,12 @@ def test_checksum_invalid(self): An address is created with an invalid checksum. """ trytes = ( - b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' - b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA' + b'IGKUOZGEFNSVJXETLIBKRSUZAWMYSVDPMHGQPCETEFNZP' + b'XSJLZMBLAWDRLUBWPIPKFNEPADIWMXMYYRKQ' ) addy = Address( - trytes + b'FOXM9MUBQ' # <- Last tryte s/b 'X'. + trytes + b'IGUKNUNAX' # <- Last tryte s/b 'W'. ) self.assertFalse(addy.is_valid()) @@ -683,8 +683,8 @@ def test_checksum_invalid(self): self.assertEqual( binary_type(addy.with_checksum()), - b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' - b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX', + b'IGKUOZGEFNSVJXETLIBKRSUZAWMYSVDPMHGQPCETEFNZP' + b'XSJLZMBLAWDRLUBWPIPKFNEPADIWMXMYYRKQIGUKNUNAW', ) def test_checksum_null(self): @@ -692,8 +692,8 @@ def test_checksum_null(self): An address is created without a checksum. """ trytes = ( - b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' - b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA' + b'ZKIUDZXQYQAWSHPKSAATJXPAQZPGYCDCQDRSMWWCGQJNI' + b'PCOORMDRNREDUDKBMUYENYTFVUNEWDBAKXMV' ) addy = Address(trytes) @@ -703,8 +703,8 @@ def test_checksum_null(self): self.assertEqual( binary_type(addy.with_checksum()), - b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' - b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX', + b'ZKIUDZXQYQAWSHPKSAATJXPAQZPGYCDCQDRSMWWCGQJNI' + b'PCOORMDRNREDUDKBMUYENYTFVUNEWDBAKXMVSDPEKQPMM', ) From 366f57abe94e26f80286b4504e0ad68ca6a9e04b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 14:32:41 -0500 Subject: [PATCH 134/239] Cleaned up README. --- README.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/README.rst b/README.rst index 9a7aa00..9a92a37 100644 --- a/README.rst +++ b/README.rst @@ -31,12 +31,10 @@ To install the latest stable version:: Installing from Source ====================== -PyOTA uses the `curl extension`_, which requires `SWIG`_ in order to build. 1. `Create virtualenv`_ (recommended, but not required). -2. ``git clone https://github.com/iotaledger/pyota.git`` -3. ``git submodule init --recursive`` -4. ``pip install -e .`` +2. ``git clone https://github.com/iotaledger/iota.lib.py.git`` +3. ``pip install -e .`` Running Unit Tests ------------------ @@ -57,9 +55,8 @@ For the full documentation of this library, please refer to the .. _Create virtualenv: https://virtualenvwrapper.readthedocs.io/ -.. _curl extension: https://github.com/iotaledger/ccurl +.. _SWIG: http://www.swig.org/download.html +.. _Slack: http://slack.iotatoken.com/ .. _dedicated forum: http://forum.iotatoken.com/ .. _official API: https://iota.readme.io/ -.. _Slack: http://slack.iotatoken.com/ -.. _SWIG: http://www.swig.org/download.html .. _tox: https://tox.readthedocs.io/ From 34238d94a0893f380ba95124bc71303258dc8c81 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 18 Dec 2016 15:00:33 -0500 Subject: [PATCH 135/239] Renamed ambiguous function. --- iota/types.py | 2 +- test/types_test.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/iota/types.py b/iota/types.py index d7f8d54..a6b6b14 100644 --- a/iota/types.py +++ b/iota/types.py @@ -119,7 +119,7 @@ class TryteString(object): IMPORTANT: A TryteString does not represent a numeric value! """ @classmethod - def from_bytes(cls, bytes_): + def from_ascii(cls, bytes_): # type: (Union[binary_type, bytearray]) -> TryteString """ Creates a TryteString from an ASCII representation. diff --git a/test/types_test.py b/test/types_test.py index 27bfe2e..971c193 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -448,12 +448,12 @@ def test_as_trits_multiple_trytes(self): ], ) - def test_from_bytes(self): + def test_from_ascii(self): """ - Converting a sequence of bytes into a TryteString. + Converting a sequence of ASCII chars into a TryteString. """ self.assertEqual( - binary_type(TryteString.from_bytes(b'Hello, IOTA!')), + binary_type(TryteString.from_ascii(b'Hello, IOTA!')), b'RBTC9D9DCDQAEASBYBCCKBFA', ) From 5d4dac7d907fd168e8b886c2a359b1172f399f89 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 19 Dec 2016 09:25:45 -0500 Subject: [PATCH 136/239] Renamed `replay_transfer` to `replay_bundle`. For parity with the JS library. --- iota/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/api.py b/iota/api.py index 3c957e6..6e1f6b0 100644 --- a/iota/api.py +++ b/iota/api.py @@ -444,7 +444,7 @@ def get_transfers(self, indexes=None, inclusion_states=False): """ raise NotImplementedError('Not implemented yet.') - def replay_transfer(self, transaction): + def replay_bundle(self, transaction): # type: (TransactionId) -> Bundle """ Takes a tail transaction hash as input, gets the bundle associated From cd511f2c57b5060bdd7d6182cb575d794b38fd57 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 19 Dec 2016 09:41:10 -0500 Subject: [PATCH 137/239] Reduced cognitive load in comments. --- iota/api.py | 95 ++++++++++++++++++++++++++++++++++------------------- 1 file changed, 61 insertions(+), 34 deletions(-) diff --git a/iota/api.py b/iota/api.py index 6e1f6b0..46fe337 100644 --- a/iota/api.py +++ b/iota/api.py @@ -27,7 +27,8 @@ class StrictIota(object): def __init__(self, adapter): # type: (Union[Text, BaseAdapter]) -> None """ - :param adapter: URI string or BaseAdapter instance. + :param adapter: + URI string or BaseAdapter instance. """ super(StrictIota, self).__init__() @@ -44,7 +45,8 @@ def __getattr__(self, command): This method is useful for invoking undocumented or experimental methods, or if you just want to troll your node for awhile. - :param command: The name of the command to send. + :param command: + The name of the command to send. References: - https://iota.readme.io/docs/making-requests @@ -85,8 +87,8 @@ def attach_to_tangle( you'll get through the getTransactionsToApprove API call. The returned value is a different set of tryte values which you can - input into :py:method:`broadcast_transactions` and - :py:method:`store_transactions`. + input into :py:meth:`broadcast_transactions` and + :py:meth:`store_transactions`. References: - https://iota.readme.io/docs/attachtotangle @@ -104,7 +106,7 @@ def broadcast_transactions(self, trytes): Broadcast a list of transactions to all neighbors. The input trytes for this call are provided by - :py:method:`attach_to_tangle`. + :py:meth:`attach_to_tangle`. References: - https://iota.readme.io/docs/broadcasttransactions @@ -129,10 +131,17 @@ def find_transactions( Using multiple of these input fields returns the intersection of the values. - :param bundles: List of transaction IDs. - :param addresses: List of addresses. - :param tags: List of tags. Each tag must be 27 trytes. - :param approvees: List of approvee transaction IDs. + :param bundles: + List of transaction IDs. + + :param addresses: + List of addresses. + + :param tags: + List of tags. + + :param approvees: + List of approvee transaction IDs. References: - https://iota.readme.io/docs/findtransactions @@ -147,8 +156,9 @@ def find_transactions( def get_balances(self, addresses, threshold=100): # type: (Iterable[Address], int) -> dict """ - Similar to `get_inclusion_states`. Returns the confirmed balance - which a list of addresses have at the latest confirmed milestone. + Similar to :py:meth:`get_inclusion_states`. Returns the confirmed + balance which a list of addresses have at the latest confirmed + milestone. In addition to the balances, it also returns the milestone as well as the index with which the confirmed balance was determined. @@ -158,7 +168,8 @@ def get_balances(self, addresses, threshold=100): :param addresses: List of addresses to get the confirmed balance for. - :param threshold: Confirmation threshold. + :param threshold: + Confirmation threshold. References: - https://iota.readme.io/docs/getbalances @@ -259,7 +270,7 @@ def get_trytes(self, hashes): def interrupt_attaching_to_tangle(self): # type: () -> dict """ - Interrupts and completely aborts the :py:method:`attach_to_tangle` + Interrupts and completely aborts the :py:meth:`attach_to_tangle` process. References: @@ -288,7 +299,7 @@ def store_transactions(self, trytes): Store transactions into local storage. The input trytes for this call are provided by - :py:method:`attach_to_tangle`. + :py:meth:`attach_to_tangle`. References: - https://iota.readme.io/docs/storetransactions @@ -325,13 +336,18 @@ def get_inputs(self, start=None, end=None, threshold=None): balance. This is either done deterministically (by generating all addresses - until :py:method:`find_transactions` returns an empty - result and then doing :py:method:`get_balances`), or by providing a + until :py:meth:`find_transactions` returns an empty + result and then doing :py:meth:`get_balances`), or by providing a key range to search. - :param start: Starting key index. - :param end: Starting key index. - :param threshold: Minimum required balance of accumulated inputs. + :param start: + Starting key index. + + :param end: + Starting key index. + + :param threshold: + Minimum required balance of accumulated inputs. :return: Dict with the following keys:: @@ -368,8 +384,8 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): :return: Array containing the trytes of the new bundle. - This value can be provided to :py:method:`broadcastTransaction` - and/or :py:method:`storeTransaction`. + This value can be provided to :py:meth:`broadcastTransaction` + and/or :py:meth:`storeTransaction`. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers @@ -381,19 +397,21 @@ def get_new_address(self, index=None, count=1): """ Generates one or more new addresses from a seed. - Note that this method always returns a list of addresses, even if - only one address is generated. - :param index: Specify the index of the new address. If not provided, the address will generated deterministically. :param count: Number of addresses to generate. - This is more efficient than calling :py:method:`get_new_address` + + Note: This is more efficient than calling ``get_new_address`` inside a loop. - :return: List of generated addresses. + :return: + List of generated addresses. + + Note that this method always returns a list, even if only one + address is generated. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress @@ -437,7 +455,8 @@ def get_transfers(self, indexes=None, inclusion_states=False): This requires an additional API call to the node, so it is disabled by default. - :return: List of bundles. + :return: + List of bundles. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers @@ -451,9 +470,11 @@ def replay_bundle(self, transaction): with the transaction and then replays the bundle by attaching it to the tangle. - :param transaction: Transaction hash. Must be a tail. + :param transaction: + Transaction hash. Must be a tail. - :return: The bundle containing the replayed transfer. + :return: + The bundle containing the replayed transfer. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#replaytransfer @@ -474,8 +495,11 @@ def send_transfer( the bundle to the Tangle, and broadcasts and stores the transactions. - :param depth: Depth at which to attach the bundle. - :param transfers: Transfers to include in the bundle. + :param depth: + Depth at which to attach the bundle. + + :param transfers: + Transfers to include in the bundle. :param inputs: List of inputs used to fund the transfer. @@ -492,7 +516,8 @@ def send_transfer( Min weight magnitude, used by the node to calibrate Proof of Work. - :return: The newly-attached bundle. + :return: + The newly-attached bundle. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtransfer @@ -508,13 +533,15 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=18): :param trytes: Transaction encoded as a tryte sequence. - :param depth: Depth at which to attach the bundle. + :param depth: + Depth at which to attach the bundle. :param min_weight_magnitude: Min weight magnitude, used by the node to calibrate Proof of Work. - :return: The trytes that were attached to the Tangle. + :return: + The trytes that were attached to the Tangle. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes From c56e783afdda8d1fe4312dd4ce9e9fe0ac238637 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 19 Dec 2016 09:43:03 -0500 Subject: [PATCH 138/239] Fixed incorrect usage of `:py:method:`. See http://www.sphinx-doc.org/en/stable/domains.html#cross-referencing-python-objects for more info. --- iota/commands/add_neighbors.py | 2 +- iota/commands/attach_to_tangle.py | 2 +- iota/commands/broadcast_and_store.py | 2 +- iota/commands/broadcast_transactions.py | 2 +- iota/commands/find_transactions.py | 2 +- iota/commands/get_balances.py | 2 +- iota/commands/get_inclusion_states.py | 2 +- iota/commands/get_neighbors.py | 2 +- iota/commands/get_node_info.py | 2 +- iota/commands/get_tips.py | 2 +- iota/commands/get_transactions_to_approve.py | 2 +- iota/commands/get_trytes.py | 2 +- iota/commands/interrupt_attaching_to_tangle.py | 2 +- iota/commands/remove_neighbors.py | 2 +- iota/commands/send_trytes.py | 2 +- iota/commands/store_transactions.py | 2 +- 16 files changed, 16 insertions(+), 16 deletions(-) diff --git a/iota/commands/add_neighbors.py b/iota/commands/add_neighbors.py index 6662b09..5943b4e 100644 --- a/iota/commands/add_neighbors.py +++ b/iota/commands/add_neighbors.py @@ -16,7 +16,7 @@ class AddNeighborsCommand(FilterCommand): """ Executes `addNeighbors` command. - See :py:method:`iota.api.StrictIota.add_neighbors`. + See :py:meth:`iota.api.StrictIota.add_neighbors`. """ command = 'addNeighbors' diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/attach_to_tangle.py index abe6d72..59f845d 100644 --- a/iota/commands/attach_to_tangle.py +++ b/iota/commands/attach_to_tangle.py @@ -17,7 +17,7 @@ class AttachToTangleCommand(FilterCommand): """ Executes `attachToTangle` command. - See :py:method:`iota.api.StrictIota.attach_to_tangle`. + See :py:meth:`iota.api.StrictIota.attach_to_tangle`. """ command = 'attachToTangle' diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/broadcast_and_store.py index 258124d..1307377 100644 --- a/iota/commands/broadcast_and_store.py +++ b/iota/commands/broadcast_and_store.py @@ -15,7 +15,7 @@ class BroadcastAndStoreCommand(FilterCommand): """ Executes `broadcastAndStore` extended API command. - See :py:method:`iota.api.IotaApi.broadcast_and_store` for more info. + See :py:meth:`iota.api.IotaApi.broadcast_and_store` for more info. """ command = 'broadcastAndStore' diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/broadcast_transactions.py index 894ae07..03e7458 100644 --- a/iota/commands/broadcast_transactions.py +++ b/iota/commands/broadcast_transactions.py @@ -16,7 +16,7 @@ class BroadcastTransactionsCommand(FilterCommand): """ Executes `broadcastTransactions` command. - See :py:method:`iota.api.StrictIota.broadcast_transactions`. + See :py:meth:`iota.api.StrictIota.broadcast_transactions`. """ command = 'broadcastTransactions' diff --git a/iota/commands/find_transactions.py b/iota/commands/find_transactions.py index 65a9a17..2d53a24 100644 --- a/iota/commands/find_transactions.py +++ b/iota/commands/find_transactions.py @@ -17,7 +17,7 @@ class FindTransactionsCommand(FilterCommand): """ Executes `findTransactions` command. - See :py:method:`iota.api.StrictIota.find_transactions`. + See :py:meth:`iota.api.StrictIota.find_transactions`. """ command = 'findTransactions' diff --git a/iota/commands/get_balances.py b/iota/commands/get_balances.py index 556ff86..3b497f7 100644 --- a/iota/commands/get_balances.py +++ b/iota/commands/get_balances.py @@ -17,7 +17,7 @@ class GetBalancesCommand(FilterCommand): """ Executes `getBalances` command. - See :py:method:`iota.api.StrictIota.get_balances`. + See :py:meth:`iota.api.StrictIota.get_balances`. """ def get_request_filter(self): return GetBalancesRequestFilter() diff --git a/iota/commands/get_inclusion_states.py b/iota/commands/get_inclusion_states.py index aba31ab..5433d76 100644 --- a/iota/commands/get_inclusion_states.py +++ b/iota/commands/get_inclusion_states.py @@ -17,7 +17,7 @@ class GetInclusionStatesCommand(FilterCommand): """ Executes ``getInclusionStates`` command. - See :py:method:`iota.api.StrictIota.get_inclusion_states`. + See :py:meth:`iota.api.StrictIota.get_inclusion_states`. """ command = 'getInclusionStates' diff --git a/iota/commands/get_neighbors.py b/iota/commands/get_neighbors.py index c96b373..d717bb9 100644 --- a/iota/commands/get_neighbors.py +++ b/iota/commands/get_neighbors.py @@ -13,7 +13,7 @@ class GetNeighborsCommand(FilterCommand): """ Executes ``getNeighbors`` command. - See :py:method:`iota.api.StrictIota.get_neighbors`. + See :py:meth:`iota.api.StrictIota.get_neighbors`. """ command = 'getNeighbors' diff --git a/iota/commands/get_node_info.py b/iota/commands/get_node_info.py index ed79d37..f398ecf 100644 --- a/iota/commands/get_node_info.py +++ b/iota/commands/get_node_info.py @@ -16,7 +16,7 @@ class GetNodeInfoCommand(FilterCommand): """ Executes `getNodeInfo` command. - See :py:method:`iota.api.StrictIota.get_node_info`. + See :py:meth:`iota.api.StrictIota.get_node_info`. """ command = 'getNodeInfo' diff --git a/iota/commands/get_tips.py b/iota/commands/get_tips.py index 0c6ea5e..41d0c00 100644 --- a/iota/commands/get_tips.py +++ b/iota/commands/get_tips.py @@ -17,7 +17,7 @@ class GetTipsCommand(FilterCommand): """ Executes ``getTips`` command. - See :py:method:`iota.api.StrictIota.get_tips`. + See :py:meth:`iota.api.StrictIota.get_tips`. """ command = 'getTips' diff --git a/iota/commands/get_transactions_to_approve.py b/iota/commands/get_transactions_to_approve.py index 6b7c689..9131c64 100644 --- a/iota/commands/get_transactions_to_approve.py +++ b/iota/commands/get_transactions_to_approve.py @@ -17,7 +17,7 @@ class GetTransactionsToApproveCommand(FilterCommand): """ Executes ``getTransactionsToApprove`` command. - See :py:method:`iota.api.StrictIota.get_transactions_to_approve`. + See :py:meth:`iota.api.StrictIota.get_transactions_to_approve`. """ command = 'getTransactionsToApprove' diff --git a/iota/commands/get_trytes.py b/iota/commands/get_trytes.py index c3643a2..b0eda14 100644 --- a/iota/commands/get_trytes.py +++ b/iota/commands/get_trytes.py @@ -17,7 +17,7 @@ class GetTrytesCommand(FilterCommand): """ Executes ``getTrytes`` command. - See :py:method:`iota.api.StrictIota.get_trytes`. + See :py:meth:`iota.api.StrictIota.get_trytes`. """ command = 'getTrytes' diff --git a/iota/commands/interrupt_attaching_to_tangle.py b/iota/commands/interrupt_attaching_to_tangle.py index c252dc3..d1fffe3 100644 --- a/iota/commands/interrupt_attaching_to_tangle.py +++ b/iota/commands/interrupt_attaching_to_tangle.py @@ -13,7 +13,7 @@ class InterruptAttachingToTangleCommand(FilterCommand): """ Executes ``interruptAttachingToTangle`` command. - See :py:method:`iota.api.StrictIota.interrupt_attaching_to_tangle`. + See :py:meth:`iota.api.StrictIota.interrupt_attaching_to_tangle`. """ command = 'interruptAttachingToTangle' diff --git a/iota/commands/remove_neighbors.py b/iota/commands/remove_neighbors.py index 392ec45..ab84c58 100644 --- a/iota/commands/remove_neighbors.py +++ b/iota/commands/remove_neighbors.py @@ -12,7 +12,7 @@ class RemoveNeighborsCommand(FilterCommand): """ Executes ``removeNeighbors`` command. - See :py:method:`iota.api.StrictIota.remove_neighbors`. + See :py:meth:`iota.api.StrictIota.remove_neighbors`. """ command = 'removeNeighbors' diff --git a/iota/commands/send_trytes.py b/iota/commands/send_trytes.py index 7dc4d28..e2973ec 100644 --- a/iota/commands/send_trytes.py +++ b/iota/commands/send_trytes.py @@ -19,7 +19,7 @@ class SendTrytesCommand(FilterCommand): """ Executes `sendTrytes` extended API command. - See :py:method:`iota.api.IotaApi.send_trytes` for more info. + See :py:meth:`iota.api.IotaApi.send_trytes` for more info. """ command = 'sendTrytes' diff --git a/iota/commands/store_transactions.py b/iota/commands/store_transactions.py index 033cb90..e6344a3 100644 --- a/iota/commands/store_transactions.py +++ b/iota/commands/store_transactions.py @@ -16,7 +16,7 @@ class StoreTransactionsCommand(FilterCommand): """ Executes ``storeTransactions`` command. - See :py:method:`iota.api.StrictIota.store_transactions`. + See :py:meth:`iota.api.StrictIota.store_transactions`. """ command = 'storeTransactions' From ef9258123d08c79e4d4b4d3d2683b3969862b58a Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 19 Dec 2016 09:48:36 -0500 Subject: [PATCH 139/239] Organized commands into core and extended packages. --- iota/commands/core/__init__.py | 10 ++++++++++ iota/commands/{ => core}/add_neighbors.py | 0 iota/commands/{ => core}/attach_to_tangle.py | 0 iota/commands/{ => core}/broadcast_transactions.py | 0 iota/commands/{ => core}/find_transactions.py | 0 iota/commands/{ => core}/get_balances.py | 0 iota/commands/{ => core}/get_inclusion_states.py | 0 iota/commands/{ => core}/get_neighbors.py | 0 iota/commands/{ => core}/get_node_info.py | 0 iota/commands/{ => core}/get_tips.py | 0 .../{ => core}/get_transactions_to_approve.py | 0 iota/commands/{ => core}/get_trytes.py | 0 .../{ => core}/interrupt_attaching_to_tangle.py | 0 iota/commands/{ => core}/remove_neighbors.py | 0 iota/commands/{ => core}/store_transactions.py | 0 iota/commands/extended/__init__.py | 11 +++++++++++ iota/commands/{ => extended}/broadcast_and_store.py | 5 +++-- iota/commands/{ => extended}/send_trytes.py | 6 +++--- test/api_test.py | 2 +- test/commands/core/__init__.py | 3 +++ test/commands/{ => core}/add_neighbors_test.py | 3 +-- test/commands/{ => core}/attach_to_tangle_test.py | 5 ++--- .../{ => core}/broadcast_transactions_test.py | 5 ++--- test/commands/{ => core}/find_transactions_test.py | 5 ++--- test/commands/{ => core}/get_balances_test.py | 5 ++--- test/commands/{ => core}/get_inclusion_states_test.py | 5 ++--- test/commands/{ => core}/get_neighbors_test.py | 3 +-- test/commands/{ => core}/get_node_info_test.py | 3 +-- test/commands/{ => core}/get_tips_test.py | 3 +-- .../{ => core}/get_transactions_to_approve_test.py | 3 +-- test/commands/{ => core}/get_trytes_test.py | 5 ++--- .../{ => core}/interrupt_attaching_to_tangle_test.py | 3 +-- test/commands/{ => core}/remove_neighbors_test.py | 3 +-- test/commands/{ => core}/store_transactions_test.py | 5 ++--- test/commands/extended/__init__.py | 3 +++ .../{ => extended}/broadcast_and_store_test.py | 2 +- test/commands/{ => extended}/send_trytes_test.py | 2 +- 37 files changed, 57 insertions(+), 43 deletions(-) create mode 100644 iota/commands/core/__init__.py rename iota/commands/{ => core}/add_neighbors.py (100%) rename iota/commands/{ => core}/attach_to_tangle.py (100%) rename iota/commands/{ => core}/broadcast_transactions.py (100%) rename iota/commands/{ => core}/find_transactions.py (100%) rename iota/commands/{ => core}/get_balances.py (100%) rename iota/commands/{ => core}/get_inclusion_states.py (100%) rename iota/commands/{ => core}/get_neighbors.py (100%) rename iota/commands/{ => core}/get_node_info.py (100%) rename iota/commands/{ => core}/get_tips.py (100%) rename iota/commands/{ => core}/get_transactions_to_approve.py (100%) rename iota/commands/{ => core}/get_trytes.py (100%) rename iota/commands/{ => core}/interrupt_attaching_to_tangle.py (100%) rename iota/commands/{ => core}/remove_neighbors.py (100%) rename iota/commands/{ => core}/store_transactions.py (100%) create mode 100644 iota/commands/extended/__init__.py rename iota/commands/{ => extended}/broadcast_and_store.py (85%) rename iota/commands/{ => extended}/send_trytes.py (90%) create mode 100644 test/commands/core/__init__.py rename test/commands/{ => core}/add_neighbors_test.py (97%) rename test/commands/{ => core}/attach_to_tangle_test.py (99%) rename test/commands/{ => core}/broadcast_transactions_test.py (98%) rename test/commands/{ => core}/find_transactions_test.py (99%) rename test/commands/{ => core}/get_balances_test.py (99%) rename test/commands/{ => core}/get_inclusion_states_test.py (98%) rename test/commands/{ => core}/get_neighbors_test.py (92%) rename test/commands/{ => core}/get_node_info_test.py (97%) rename test/commands/{ => core}/get_tips_test.py (97%) rename test/commands/{ => core}/get_transactions_to_approve_test.py (98%) rename test/commands/{ => core}/get_trytes_test.py (98%) rename test/commands/{ => core}/interrupt_attaching_to_tangle_test.py (93%) rename test/commands/{ => core}/remove_neighbors_test.py (97%) rename test/commands/{ => core}/store_transactions_test.py (98%) create mode 100644 test/commands/extended/__init__.py rename test/commands/{ => extended}/broadcast_and_store_test.py (97%) rename test/commands/{ => extended}/send_trytes_test.py (99%) diff --git a/iota/commands/core/__init__.py b/iota/commands/core/__init__.py new file mode 100644 index 0000000..0da3d5e --- /dev/null +++ b/iota/commands/core/__init__.py @@ -0,0 +1,10 @@ +# coding=utf-8 +""" +Core commands are defined by the node API. + +References: + - https://iota.readme.io/docs/getting-started +""" + +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/iota/commands/add_neighbors.py b/iota/commands/core/add_neighbors.py similarity index 100% rename from iota/commands/add_neighbors.py rename to iota/commands/core/add_neighbors.py diff --git a/iota/commands/attach_to_tangle.py b/iota/commands/core/attach_to_tangle.py similarity index 100% rename from iota/commands/attach_to_tangle.py rename to iota/commands/core/attach_to_tangle.py diff --git a/iota/commands/broadcast_transactions.py b/iota/commands/core/broadcast_transactions.py similarity index 100% rename from iota/commands/broadcast_transactions.py rename to iota/commands/core/broadcast_transactions.py diff --git a/iota/commands/find_transactions.py b/iota/commands/core/find_transactions.py similarity index 100% rename from iota/commands/find_transactions.py rename to iota/commands/core/find_transactions.py diff --git a/iota/commands/get_balances.py b/iota/commands/core/get_balances.py similarity index 100% rename from iota/commands/get_balances.py rename to iota/commands/core/get_balances.py diff --git a/iota/commands/get_inclusion_states.py b/iota/commands/core/get_inclusion_states.py similarity index 100% rename from iota/commands/get_inclusion_states.py rename to iota/commands/core/get_inclusion_states.py diff --git a/iota/commands/get_neighbors.py b/iota/commands/core/get_neighbors.py similarity index 100% rename from iota/commands/get_neighbors.py rename to iota/commands/core/get_neighbors.py diff --git a/iota/commands/get_node_info.py b/iota/commands/core/get_node_info.py similarity index 100% rename from iota/commands/get_node_info.py rename to iota/commands/core/get_node_info.py diff --git a/iota/commands/get_tips.py b/iota/commands/core/get_tips.py similarity index 100% rename from iota/commands/get_tips.py rename to iota/commands/core/get_tips.py diff --git a/iota/commands/get_transactions_to_approve.py b/iota/commands/core/get_transactions_to_approve.py similarity index 100% rename from iota/commands/get_transactions_to_approve.py rename to iota/commands/core/get_transactions_to_approve.py diff --git a/iota/commands/get_trytes.py b/iota/commands/core/get_trytes.py similarity index 100% rename from iota/commands/get_trytes.py rename to iota/commands/core/get_trytes.py diff --git a/iota/commands/interrupt_attaching_to_tangle.py b/iota/commands/core/interrupt_attaching_to_tangle.py similarity index 100% rename from iota/commands/interrupt_attaching_to_tangle.py rename to iota/commands/core/interrupt_attaching_to_tangle.py diff --git a/iota/commands/remove_neighbors.py b/iota/commands/core/remove_neighbors.py similarity index 100% rename from iota/commands/remove_neighbors.py rename to iota/commands/core/remove_neighbors.py diff --git a/iota/commands/store_transactions.py b/iota/commands/core/store_transactions.py similarity index 100% rename from iota/commands/store_transactions.py rename to iota/commands/core/store_transactions.py diff --git a/iota/commands/extended/__init__.py b/iota/commands/extended/__init__.py new file mode 100644 index 0000000..0711703 --- /dev/null +++ b/iota/commands/extended/__init__.py @@ -0,0 +1,11 @@ +# coding=utf-8 +""" +Extended API commands encapsulate the core commands and provide +additional functionality such as address generation and signatures. + +References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md +""" + +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/iota/commands/broadcast_and_store.py b/iota/commands/extended/broadcast_and_store.py similarity index 85% rename from iota/commands/broadcast_and_store.py rename to iota/commands/extended/broadcast_and_store.py index 1307377..5093a04 100644 --- a/iota/commands/broadcast_and_store.py +++ b/iota/commands/extended/broadcast_and_store.py @@ -3,8 +3,9 @@ unicode_literals from iota.commands import FilterCommand -from iota.commands.broadcast_transactions import BroadcastTransactionsCommand -from iota.commands.store_transactions import StoreTransactionsCommand +from iota.commands.core.broadcast_transactions import \ + BroadcastTransactionsCommand +from iota.commands.core.store_transactions import StoreTransactionsCommand __all__ = [ 'BroadcastAndStoreCommand', diff --git a/iota/commands/send_trytes.py b/iota/commands/extended/send_trytes.py similarity index 90% rename from iota/commands/send_trytes.py rename to iota/commands/extended/send_trytes.py index e2973ec..77e692c 100644 --- a/iota/commands/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -4,10 +4,10 @@ import filters as f from iota.commands import FilterCommand, RequestFilter -from iota.commands.attach_to_tangle import AttachToTangleCommand -from iota.commands.broadcast_and_store import BroadcastAndStoreCommand -from iota.commands.get_transactions_to_approve import \ +from iota.commands.core.attach_to_tangle import AttachToTangleCommand +from iota.commands.core.get_transactions_to_approve import \ GetTransactionsToApproveCommand +from iota.commands.extended.broadcast_and_store import BroadcastAndStoreCommand from iota.filters import Trytes __all__ = [ diff --git a/test/api_test.py b/test/api_test.py index 10430f7..0475a5a 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -6,7 +6,7 @@ from iota import StrictIota from iota.commands import CustomCommand -from iota.commands.get_node_info import GetNodeInfoCommand +from iota.commands.core.get_node_info import GetNodeInfoCommand from test import MockAdapter diff --git a/test/commands/core/__init__.py b/test/commands/core/__init__.py new file mode 100644 index 0000000..3f3d02d --- /dev/null +++ b/test/commands/core/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/test/commands/add_neighbors_test.py b/test/commands/core/add_neighbors_test.py similarity index 97% rename from test/commands/add_neighbors_test.py rename to test/commands/core/add_neighbors_test.py index da018fd..b7dc65e 100644 --- a/test/commands/add_neighbors_test.py +++ b/test/commands/core/add_neighbors_test.py @@ -4,8 +4,7 @@ import filters as f from filters.test import BaseFilterTestCase - -from iota.commands.add_neighbors import AddNeighborsCommand +from iota.commands.core.add_neighbors import AddNeighborsCommand from iota.filters import NodeUri from test import MockAdapter diff --git a/test/commands/attach_to_tangle_test.py b/test/commands/core/attach_to_tangle_test.py similarity index 99% rename from test/commands/attach_to_tangle_test.py rename to test/commands/core/attach_to_tangle_test.py index e052a27..d2a8921 100644 --- a/test/commands/attach_to_tangle_test.py +++ b/test/commands/core/attach_to_tangle_test.py @@ -4,11 +4,10 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type - from iota import TransactionId, TryteString -from iota.commands.attach_to_tangle import AttachToTangleCommand +from iota.commands.core.attach_to_tangle import AttachToTangleCommand from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter diff --git a/test/commands/broadcast_transactions_test.py b/test/commands/core/broadcast_transactions_test.py similarity index 98% rename from test/commands/broadcast_transactions_test.py rename to test/commands/core/broadcast_transactions_test.py index efab5ce..58192d7 100644 --- a/test/commands/broadcast_transactions_test.py +++ b/test/commands/core/broadcast_transactions_test.py @@ -4,12 +4,11 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type - from iota import TryteString -from iota.commands.broadcast_transactions import \ +from iota.commands.core.broadcast_transactions import \ BroadcastTransactionsCommand from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter diff --git a/test/commands/find_transactions_test.py b/test/commands/core/find_transactions_test.py similarity index 99% rename from test/commands/find_transactions_test.py rename to test/commands/core/find_transactions_test.py index fdd850c..7b7b1a1 100644 --- a/test/commands/find_transactions_test.py +++ b/test/commands/core/find_transactions_test.py @@ -4,12 +4,11 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type - from iota import Address, Tag, TransactionId, TryteString -from iota.commands.find_transactions import FindTransactionsRequestFilter, \ +from iota.commands.core.find_transactions import FindTransactionsRequestFilter, \ FindTransactionsCommand from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter diff --git a/test/commands/get_balances_test.py b/test/commands/core/get_balances_test.py similarity index 99% rename from test/commands/get_balances_test.py rename to test/commands/core/get_balances_test.py index 87c4811..1881436 100644 --- a/test/commands/get_balances_test.py +++ b/test/commands/core/get_balances_test.py @@ -4,11 +4,10 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type - from iota import Address, TryteString -from iota.commands.get_balances import GetBalancesCommand +from iota.commands.core.get_balances import GetBalancesCommand from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter diff --git a/test/commands/get_inclusion_states_test.py b/test/commands/core/get_inclusion_states_test.py similarity index 98% rename from test/commands/get_inclusion_states_test.py rename to test/commands/core/get_inclusion_states_test.py index c9ecc6d..db513db 100644 --- a/test/commands/get_inclusion_states_test.py +++ b/test/commands/core/get_inclusion_states_test.py @@ -4,11 +4,10 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type - from iota import TransactionId, TryteString -from iota.commands.get_inclusion_states import GetInclusionStatesCommand +from iota.commands.core.get_inclusion_states import GetInclusionStatesCommand from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter diff --git a/test/commands/get_neighbors_test.py b/test/commands/core/get_neighbors_test.py similarity index 92% rename from test/commands/get_neighbors_test.py rename to test/commands/core/get_neighbors_test.py index f4754ca..5fb85c9 100644 --- a/test/commands/get_neighbors_test.py +++ b/test/commands/core/get_neighbors_test.py @@ -4,8 +4,7 @@ import filters as f from filters.test import BaseFilterTestCase - -from iota.commands.get_neighbors import GetNeighborsCommand +from iota.commands.core.get_neighbors import GetNeighborsCommand from test import MockAdapter diff --git a/test/commands/get_node_info_test.py b/test/commands/core/get_node_info_test.py similarity index 97% rename from test/commands/get_node_info_test.py rename to test/commands/core/get_node_info_test.py index 044d074..42bf4ba 100644 --- a/test/commands/get_node_info_test.py +++ b/test/commands/core/get_node_info_test.py @@ -4,9 +4,8 @@ import filters as f from filters.test import BaseFilterTestCase - from iota import TryteString -from iota.commands.get_node_info import GetNodeInfoCommand +from iota.commands.core.get_node_info import GetNodeInfoCommand from test import MockAdapter diff --git a/test/commands/get_tips_test.py b/test/commands/core/get_tips_test.py similarity index 97% rename from test/commands/get_tips_test.py rename to test/commands/core/get_tips_test.py index d157390..9536c2f 100644 --- a/test/commands/get_tips_test.py +++ b/test/commands/core/get_tips_test.py @@ -4,9 +4,8 @@ import filters as f from filters.test import BaseFilterTestCase - from iota import Address -from iota.commands.get_tips import GetTipsCommand +from iota.commands.core.get_tips import GetTipsCommand from test import MockAdapter diff --git a/test/commands/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py similarity index 98% rename from test/commands/get_transactions_to_approve_test.py rename to test/commands/core/get_transactions_to_approve_test.py index 3ea625b..4904ee9 100644 --- a/test/commands/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -4,9 +4,8 @@ import filters as f from filters.test import BaseFilterTestCase - from iota import TransactionId -from iota.commands.get_transactions_to_approve import \ +from iota.commands.core.get_transactions_to_approve import \ GetTransactionsToApproveCommand from test import MockAdapter diff --git a/test/commands/get_trytes_test.py b/test/commands/core/get_trytes_test.py similarity index 98% rename from test/commands/get_trytes_test.py rename to test/commands/core/get_trytes_test.py index cc44019..c6a99d7 100644 --- a/test/commands/get_trytes_test.py +++ b/test/commands/core/get_trytes_test.py @@ -4,11 +4,10 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type - from iota import TransactionId, TryteString -from iota.commands.get_trytes import GetTrytesCommand +from iota.commands.core.get_trytes import GetTrytesCommand from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter diff --git a/test/commands/interrupt_attaching_to_tangle_test.py b/test/commands/core/interrupt_attaching_to_tangle_test.py similarity index 93% rename from test/commands/interrupt_attaching_to_tangle_test.py rename to test/commands/core/interrupt_attaching_to_tangle_test.py index 8429fa3..c1fa325 100644 --- a/test/commands/interrupt_attaching_to_tangle_test.py +++ b/test/commands/core/interrupt_attaching_to_tangle_test.py @@ -4,8 +4,7 @@ import filters as f from filters.test import BaseFilterTestCase - -from iota.commands.interrupt_attaching_to_tangle import \ +from iota.commands.core.interrupt_attaching_to_tangle import \ InterruptAttachingToTangleCommand from test import MockAdapter diff --git a/test/commands/remove_neighbors_test.py b/test/commands/core/remove_neighbors_test.py similarity index 97% rename from test/commands/remove_neighbors_test.py rename to test/commands/core/remove_neighbors_test.py index dce7bb8..78879a1 100644 --- a/test/commands/remove_neighbors_test.py +++ b/test/commands/core/remove_neighbors_test.py @@ -4,8 +4,7 @@ import filters as f from filters.test import BaseFilterTestCase - -from iota.commands.remove_neighbors import RemoveNeighborsCommand +from iota.commands.core.remove_neighbors import RemoveNeighborsCommand from iota.filters import NodeUri from test import MockAdapter diff --git a/test/commands/store_transactions_test.py b/test/commands/core/store_transactions_test.py similarity index 98% rename from test/commands/store_transactions_test.py rename to test/commands/core/store_transactions_test.py index 5b40de6..0797712 100644 --- a/test/commands/store_transactions_test.py +++ b/test/commands/core/store_transactions_test.py @@ -4,11 +4,10 @@ import filters as f from filters.test import BaseFilterTestCase -from six import binary_type, text_type - from iota import TryteString -from iota.commands.store_transactions import StoreTransactionsCommand +from iota.commands.core.store_transactions import StoreTransactionsCommand from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter diff --git a/test/commands/extended/__init__.py b/test/commands/extended/__init__.py new file mode 100644 index 0000000..3f3d02d --- /dev/null +++ b/test/commands/extended/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/test/commands/broadcast_and_store_test.py b/test/commands/extended/broadcast_and_store_test.py similarity index 97% rename from test/commands/broadcast_and_store_test.py rename to test/commands/extended/broadcast_and_store_test.py index 8278350..b2ade2f 100644 --- a/test/commands/broadcast_and_store_test.py +++ b/test/commands/extended/broadcast_and_store_test.py @@ -5,7 +5,7 @@ from unittest import TestCase from iota import BadApiResponse, TryteString -from iota.commands.broadcast_and_store import BroadcastAndStoreCommand +from iota.commands.extended.broadcast_and_store import BroadcastAndStoreCommand from six import text_type from test import MockAdapter diff --git a/test/commands/send_trytes_test.py b/test/commands/extended/send_trytes_test.py similarity index 99% rename from test/commands/send_trytes_test.py rename to test/commands/extended/send_trytes_test.py index 534613e..6ad8582 100644 --- a/test/commands/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -5,7 +5,7 @@ from unittest import TestCase from iota import BadApiResponse, TransactionId, TryteString -from iota.commands.send_trytes import SendTrytesCommand +from iota.commands.extended.send_trytes import SendTrytesCommand from six import text_type from test import MockAdapter From 47784b269e1737a4f0e7ee459034ae5bc1737fe8 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 19 Dec 2016 10:45:38 -0500 Subject: [PATCH 140/239] Request validation for `getNewAddresses` + internal tightening-up. --- iota/adapter.py | 12 +- iota/api.py | 21 +- iota/commands/__init__.py | 11 + iota/commands/extended/get_new_addresses.py | 43 +++ test/adapter_test.py | 92 +++++-- .../extended/get_new_addresses_test.py | 253 ++++++++++++++++++ 6 files changed, 399 insertions(+), 33 deletions(-) create mode 100644 iota/commands/extended/get_new_addresses.py create mode 100644 test/commands/extended/get_new_addresses_test.py diff --git a/iota/adapter.py b/iota/adapter.py index 7b95aea..3cf8abc 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -6,7 +6,7 @@ from abc import ABCMeta, abstractmethod as abstract_method from inspect import isabstract as is_abstract from socket import getdefaulttimeout as get_default_timeout -from typing import Dict, Text, Tuple +from typing import Dict, Text, Tuple, Union import requests from six import with_metaclass @@ -15,11 +15,16 @@ from iota.json import JsonEncoder __all__ = [ + 'AdapterSpec', 'BadApiResponse', 'InvalidUri', ] +# Custom types for type hints and docstrings. +AdapterSpec = Union[Text, 'BaseAdapter'] + + class BadApiResponse(ValueError): """ Indicates that a non-success response was received from the node. @@ -44,8 +49,11 @@ class InvalidUri(ValueError): def resolve_adapter(uri): - # type: (Text) -> BaseAdapter + # type: (AdapterSpec) -> BaseAdapter """Given a URI, returns a properly-configured adapter instance.""" + if isinstance(uri, BaseAdapter): + return uri + try: protocol, _ = uri.split('://', 1) except ValueError: diff --git a/iota/api.py b/iota/api.py index 46fe337..910e693 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,10 +2,10 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Iterable, List, Optional, Text, Union +from typing import Iterable, List, Optional, Text from iota import Address, Bundle, Tag, TransactionId, Transfer, TryteString -from iota.adapter import BaseAdapter, resolve_adapter +from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry __all__ = [ @@ -25,7 +25,7 @@ class StrictIota(object): - https://iota.readme.io/docs/getting-started """ def __init__(self, adapter): - # type: (Union[Text, BaseAdapter]) -> None + # type: (AdapterSpec) -> None """ :param adapter: URI string or BaseAdapter instance. @@ -317,7 +317,7 @@ class Iota(StrictIota): - https://github.com/iotaledger/wiki/blob/master/api-proposal.md """ def __init__(self, adapter, seed=None): - # type: (Union[Text, BaseAdapter], Optional[TryteString]) -> None + # type: (AdapterSpec, Optional[TryteString]) -> None """ :param seed: Seed used to generate new addresses. @@ -392,17 +392,18 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): """ raise NotImplementedError('Not implemented yet.') - def get_new_address(self, index=None, count=1): + def get_new_addresses(self, index=None, count=1): # type: (Optional[int], Optional[int]) -> List[Address] """ - Generates one or more new addresses from a seed. + Generates one or more new addresses from the seed. :param index: - Specify the index of the new address. - If not provided, the address will generated deterministically. + Specify the index of the new address (must be >= 1). + + If not provided, the address will be generated deterministically. :param count: - Number of addresses to generate. + Number of addresses to generate (must be >= 1). Note: This is more efficient than calling ``get_new_address`` inside a loop. @@ -416,7 +417,7 @@ def get_new_address(self, index=None, count=1): References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress """ - raise NotImplementedError('Not implemented yet.') + return self.getNewAddresses(seed=self.seed, index=index, count=count) def get_bundle(self, transaction): # type: (TransactionId) -> List[Bundle] diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 215da08..816c53c 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -207,6 +207,11 @@ def __init__( | f.FilterMapper(filter_map, allow_missing_keys, allow_extra_keys) ) + def _apply_none(self): + # Some commands do accept/require empty requests, but in those + # cases, the request must be an empty object, not ``None``. + return self._filter(None, f.Required) + class ResponseFilter(f.FilterChain): """Template for filter applied to API responses.""" @@ -223,6 +228,12 @@ def __init__( | f.FilterMapper(filter_map, allow_missing_keys, allow_extra_keys) ) + def _apply_none(self): + # If for some reason we don't get a response from the node, and the + # adapter didn't complain, pretend like the response was an empty + # object. + return self._apply({}) + class FilterCommand(with_metaclass(ABCMeta, BaseCommand)): """Uses filters to manipulate request/response values.""" diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py new file mode 100644 index 0000000..ddc7846 --- /dev/null +++ b/iota/commands/extended/get_new_addresses.py @@ -0,0 +1,43 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f + +from iota.commands import FilterCommand, RequestFilter +from iota.filters import Trytes + +__all__ = [ + 'GetNewAddressesCommand', +] + + +class GetNewAddressesCommand(FilterCommand): + """ + Executes ``getNewAddresses`` extended API command. + + See :py:meth:`iota.api.Iota.get_new_addresses` for more info. + """ + command = 'getNewAddresses' + + def get_request_filter(self): + return GetNewAddressesRequestFilter() + + def get_response_filter(self): + pass + + +class GetNewAddressesRequestFilter(RequestFilter): + def __init__(self): + super(GetNewAddressesRequestFilter, self).__init__( + { + 'count': f.Type(int) | f.Min(1) | f.Optional(1), + 'index': f.Type(int) | f.Min(1), + 'seed': f.Required | Trytes, + }, + + allow_missing_keys = { + 'count', + 'index', + } + ) diff --git a/test/adapter_test.py b/test/adapter_test.py index 20ef70c..7430b47 100644 --- a/test/adapter_test.py +++ b/test/adapter_test.py @@ -12,34 +12,54 @@ from iota import BadApiResponse, DEFAULT_PORT, InvalidUri, TryteString from iota.adapter import HttpAdapter, resolve_adapter +from test import MockAdapter class ResolveAdapterTestCase(TestCase): - """Unit tests for the `resolve_adapter` function.""" + """ + Unit tests for the `resolve_adapter` function. + """ + def test_adapter_instance(self): + """ + Resolving an adapter instance. + """ + adapter = MockAdapter() + self.assertIs(resolve_adapter(adapter), adapter) + def test_udp(self): - """Resolving a valid udp:// URI.""" + """ + Resolving a valid udp:// URI. + """ adapter = resolve_adapter('udp://localhost:14265/') self.assertIsInstance(adapter, HttpAdapter) def test_http(self): - """Resolving a valid http:// URI.""" + """ + Resolving a valid http:// URI. + """ adapter = resolve_adapter('http://localhost:14265/') self.assertIsInstance(adapter, HttpAdapter) def test_missing_protocol(self): - """The URI does not include a protocol.""" + """ + The URI does not include a protocol. + """ with self.assertRaises(InvalidUri): resolve_adapter('localhost:14265') def test_unknown_protocol(self): - """The URI references a protocol that has no associated adapter.""" + """ + The URI references a protocol that has no associated adapter. + """ with self.assertRaises(InvalidUri): resolve_adapter('foobar://localhost:14265') class HttpAdapterTestCase(TestCase): def test_configure_udp(self): - """Configuring an HttpAdapter using a valid udp:// URI.""" + """ + Configuring an HttpAdapter using a valid udp:// URI. + """ adapter = HttpAdapter.configure('udp://localhost:14265/') self.assertEqual(adapter.host, 'localhost') @@ -47,7 +67,9 @@ def test_configure_udp(self): self.assertEqual(adapter.path, '/') def test_configure_http(self): - """Configuring HttpAdapter using a valid http:// URI.""" + """ + Configuring HttpAdapter using a valid http:// URI. + """ adapter = HttpAdapter.configure('http://localhost:14265/') self.assertEqual(adapter.host, 'localhost') @@ -55,7 +77,9 @@ def test_configure_http(self): self.assertEqual(adapter.path, '/') def test_configure_ipv4_address(self): - """Configuring an HttpAdapter using an IPv4 address.""" + """ + Configuring an HttpAdapter using an IPv4 address. + """ adapter = HttpAdapter.configure('udp://127.0.0.1:8080/') self.assertEqual(adapter.host, '127.0.0.1') @@ -63,7 +87,9 @@ def test_configure_ipv4_address(self): self.assertEqual(adapter.path, '/') def test_configure_default_port_udp(self): - """Implicitly use default UDP port for HttpAdapter.""" + """ + Implicitly use default UDP port for HttpAdapter. + """ adapter = HttpAdapter.configure('udp://iotatoken.com/') self.assertEqual(adapter.host, 'iotatoken.com') @@ -71,7 +97,9 @@ def test_configure_default_port_udp(self): self.assertEqual(adapter.path, '/') def test_configure_default_port_http(self): - """Implicitly use default HTTP port for HttpAdapter.""" + """ + Implicitly use default HTTP port for HttpAdapter. + """ adapter = HttpAdapter.configure('http://iotatoken.com/') self.assertEqual(adapter.host, 'iotatoken.com') @@ -79,7 +107,9 @@ def test_configure_default_port_http(self): self.assertEqual(adapter.path, '/') def test_configure_path(self): - """Specifying a different path for HttpAdapter.""" + """ + Specifying a different path for HttpAdapter. + """ adapter = HttpAdapter.configure('http://iotatoken.com:443/node') self.assertEqual(adapter.host, 'iotatoken.com') @@ -98,7 +128,9 @@ def test_configure_custom_path_default_port(self): self.assertEqual(adapter.path, '/node') def test_configure_default_path(self): - """Implicitly use default path for HttpAdapter.""" + """ + Implicitly use default path for HttpAdapter. + """ adapter = HttpAdapter.configure('udp://example.com:8000') self.assertEqual(adapter.host, 'example.com') @@ -106,7 +138,9 @@ def test_configure_default_path(self): self.assertEqual(adapter.path, '/') def test_configure_default_port_and_path(self): - """Implicitly use default port and path for HttpAdapter.""" + """ + Implicitly use default port and path for HttpAdapter. + """ adapter = HttpAdapter.configure('udp://localhost') self.assertEqual(adapter.host, 'localhost') @@ -114,7 +148,9 @@ def test_configure_default_port_and_path(self): self.assertEqual(adapter.path, '/') def test_configure_error_missing_protocol(self): - """Forgetting to add the protocol to the URI.""" + """ + Forgetting to add the protocol to the URI. + """ with self.assertRaises(InvalidUri): HttpAdapter.configure('localhost:14265') @@ -126,12 +162,16 @@ def test_configure_error_invalid_protocol(self): HttpAdapter.configure('ftp://localhost:14265/') def test_configure_error_empty_host(self): - """Attempting to configure HttpAdapter with empty host.""" + """ + Attempting to configure HttpAdapter with empty host. + """ with self.assertRaises(InvalidUri): HttpAdapter.configure('udp://:14265') def test_configure_error_non_numeric_port(self): - """Attempting to configure HttpAdapter with non-numeric port.""" + """ + Attempting to configure HttpAdapter with non-numeric port. + """ with self.assertRaises(InvalidUri): HttpAdapter.configure('udp://localhost:iota/') @@ -202,7 +242,9 @@ def test_exception_response(self): self.assertEqual(text(context.exception), expected_result) def test_empty_response(self): - """The response is empty.""" + """ + The response is empty. + """ adapter = HttpAdapter('localhost') mocked_response = self._create_response('') @@ -217,7 +259,9 @@ def test_empty_response(self): self.assertEqual(text(context.exception), 'Empty response from node.') def test_non_json_response(self): - """The response is not JSON.""" + """ + The response is not JSON. + """ adapter = HttpAdapter('localhost') invalid_response = 'EHLO iotatoken.com' # Erm... @@ -236,7 +280,9 @@ def test_non_json_response(self): ) def test_non_object_response(self): - """The response is valid JSON, but it's not an object.""" + """ + The response is valid JSON, but it's not an object. + """ adapter = HttpAdapter('localhost') invalid_response = '["message", "Hello, IOTA!"]' @@ -256,7 +302,9 @@ def test_non_object_response(self): # noinspection SpellCheckingInspection def test_trytes_in_request(self): - """Sending a request that includes trytes.""" + """ + Sending a request that includes trytes. + """ adapter = HttpAdapter('localhost') # Response is not important for this test; we just need to make @@ -291,7 +339,9 @@ def test_trytes_in_request(self): @staticmethod def _create_response(content): # type: (Text) -> requests.Response - """Creates a Response object for a test.""" + """ + Creates a Response object for a test. + """ # :see: requests.adapters.HTTPAdapter.build_response response = requests.Response() diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py new file mode 100644 index 0000000..4c31671 --- /dev/null +++ b/test/commands/extended/get_new_addresses_test.py @@ -0,0 +1,253 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase +from iota import TryteString +from iota.commands.extended.get_new_addresses import GetNewAddressesCommand +from iota.filters import Trytes +from six import binary_type, text_type +from test import MockAdapter + + +class GetNewAddressesRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetNewAddressesCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(GetNewAddressesRequestFilterTestCase, self).setUp() + + # Define a few tryte sequences that we can re-use between tests. + self.seed = b'HELLOIOTA' + + def test_pass_happy_path(self): + """ + Request is valid. + """ + request = { + 'seed': TryteString(self.seed), + 'index': 1, + 'count': 1, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_optional_parameters_excluded(self): + """ + Request omits ``index`` and ``count``. + """ + filter_ = self._filter({ + 'seed': TryteString(self.seed), + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'seed': TryteString(self.seed), + 'index': None, + 'count': 1, + }, + ) + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + # ``seed`` can be any value that is convertible to TryteString. + 'seed': binary_type(self.seed), + + # These values must be integers, however. + 'index': 100, + 'count': 8, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'seed': TryteString(self.seed), + 'index': 100, + 'count': 8, + }, + ) + + def test_fail_empty(self): + """ + Request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'seed': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'seed': TryteString(self.seed), + 'index': None, + 'count': 1, + + # Some men just want to watch the world burn. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_seed_null(self): + """ + ``seed`` is null. + """ + self.assertFilterErrors( + { + 'seed': None, + }, + + { + 'seed': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_seed_wrong_type(self): + """ + ``seed`` cannot be converted into a TryteString. + """ + self.assertFilterErrors( + { + 'seed': text_type(self.seed, 'ascii'), + }, + + { + 'seed': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_seed_malformed(self): + """ + ``seed`` has the correct type, but it contains invalid characters. + """ + self.assertFilterErrors( + { + 'seed': b'not valid; seeds can only contain uppercase and "9".', + }, + + { + 'seed': [Trytes.CODE_NOT_TRYTES], + }, + ) + + def test_fail_count_string(self): + """ + ``count`` is a string value. + """ + self.assertFilterErrors( + { + # Not valid; it must be an int. + 'count': '42', + + 'seed': TryteString(self.seed), + }, + + { + 'count': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_count_float(self): + """ + ``count`` is a float value. + """ + self.assertFilterErrors( + { + # Not valid, even with an empty fpart; it must be an int. + 'count': 42.0, + + 'seed': TryteString(self.seed), + }, + + { + 'count': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_count_too_small(self): + """ + ``count`` is less than 1. + """ + self.assertFilterErrors( + { + 'count': 0, + 'seed': TryteString(self.seed), + }, + + { + 'count': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_index_string(self): + """ + ``index`` is a string value. + """ + self.assertFilterErrors( + { + # Not valid; it must be an int. + 'index': '42', + + 'seed': TryteString(self.seed), + }, + + { + 'index': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_index_float(self): + """ + ``index`` is a float value. + """ + self.assertFilterErrors( + { + # Not valid, even with an empty fpart; it must be an int. + 'index': 42.0, + + 'seed': TryteString(self.seed), + }, + + { + 'index': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_index_too_small(self): + """ + ``index`` is less than 1. + """ + self.assertFilterErrors( + { + 'index': 0, + 'seed': TryteString(self.seed), + }, + + { + 'index': [f.Min.CODE_TOO_SMALL], + }, + ) From ef51f510fd0e7f71b73df1a0f1380aa7a4e28ce4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 19 Dec 2016 15:29:51 -0500 Subject: [PATCH 141/239] Major crypto overhaul. - Made Curl interface better match other implementations. - Fixed Curl behavior when absorbing/squeezing more than 1 hash at a time. - Very early implementation of signing key generation. Needs lots more unit tests and vetting! - `getNewAddresses` request can have `index: 0`. - Fixed a couple of type hints and improved documentation. --- iota/commands/extended/get_new_addresses.py | 5 +- iota/{curl => crypto}/__init__.py | 0 iota/crypto/pycurl.py | 92 ++++++++ iota/crypto/signing.py | 205 ++++++++++++++++++ iota/curl/pycurl.py | 99 --------- iota/types.py | 47 ++-- .../extended/get_new_addresses_test.py | 8 +- test/crypto/__init__.py | 3 + test/crypto/signing_test.py | 151 +++++++++++++ test/types_test.py | 4 +- 10 files changed, 493 insertions(+), 121 deletions(-) rename iota/{curl => crypto}/__init__.py (100%) create mode 100644 iota/crypto/pycurl.py create mode 100644 iota/crypto/signing.py delete mode 100644 iota/curl/pycurl.py create mode 100644 test/crypto/__init__.py create mode 100644 test/crypto/signing_test.py diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index ddc7846..77daa38 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -26,13 +26,16 @@ def get_request_filter(self): def get_response_filter(self): pass + def _send_request(self, request): + pass + class GetNewAddressesRequestFilter(RequestFilter): def __init__(self): super(GetNewAddressesRequestFilter, self).__init__( { 'count': f.Type(int) | f.Min(1) | f.Optional(1), - 'index': f.Type(int) | f.Min(1), + 'index': f.Type(int) | f.Min(0), 'seed': f.Required | Trytes, }, diff --git a/iota/curl/__init__.py b/iota/crypto/__init__.py similarity index 100% rename from iota/curl/__init__.py rename to iota/crypto/__init__.py diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py new file mode 100644 index 0000000..2e6e57d --- /dev/null +++ b/iota/crypto/pycurl.py @@ -0,0 +1,92 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from math import ceil +from typing import List, MutableSequence, Optional, Sequence + +__all__ = [ + 'Curl', +] + + +class Curl(object): + """ + Python implementation of Curl. + + **IMPORTANT: Not thread-safe!** + """ + HASH_LENGTH = 243 + STATE_LENGTH = 3 * HASH_LENGTH + + NUMBER_OF_ROUNDS = 27 + + TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] + + def __init__(self): + # type: (Optional[Sequence[int]]) -> None + self.reset() + + # noinspection PyAttributeOutsideInit + def reset(self): + # type: () -> None + """ + Resets internal state. + """ + self._state = [0] * self.STATE_LENGTH # type: List[int] + + def absorb(self, trits): + # type: (Sequence[int], Optional[int]) -> None + """ + Absorb trits into the sponge. + + :param trits: + Sequence of trits to absorb. + Note: Only the first 729 trits will be absorbed. + """ + self._copy_and_transform(trits, self._state, len(trits)) + + def squeeze(self, trits): + # type: (MutableSequence[int]) -> None + """ + Squeeze trits from the sponge. + + :param trits: + Sequence that the squeezed trits will be copied to. + Note: this object will be modified! + """ + self._copy_and_transform(self._state, trits, len(trits)) + + def _copy_and_transform(self, source, target, length): + """ + Copies trits from ``source`` to ``target`` one hash at a time, + transforming in between hashes. + """ + for i in range(int(ceil(length / self.HASH_LENGTH))): + start = i * self.HASH_LENGTH + stop = min(len(target), len(source), start + self.HASH_LENGTH) + + target[start:stop] = source[start:stop] + self._transform() + + def _transform(self): + # type: () -> None + """ + Transforms internal state. + """ + index = 0 + + for _ in range(self.NUMBER_OF_ROUNDS): + temp_state = list(self._state) + + for pos in range(self.STATE_LENGTH): + prev_index = index + index += (364 if index < 365 else -365) + + self._state[pos] = ( + self.TRUTH_TABLE[ + temp_state[prev_index] + + (3 * temp_state[index]) + + 4 + ] + ) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py new file mode 100644 index 0000000..b75cbe6 --- /dev/null +++ b/iota/crypto/signing.py @@ -0,0 +1,205 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Generator, List, MutableSequence, Optional, Union + +from iota import TryteString, TrytesCompatible +from iota.crypto import Curl + +__all__ = [ + 'KeyGenerator', + 'SigningKey', +] + + +class Seed(TryteString): + """ + A TryteString that acts as a seed for generating new keys. + """ + LEN = 81 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Seed, self).__init__(trytes, pad=self.LEN) + + if len(self._trytes) > self.LEN: + raise ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )) + + +class SigningKey(TryteString): + """ + A TryteString that acts as a signing key, e.g., for generating + message signatures, new addresses, etc. + """ + LEN = 6561 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(SigningKey, self).__init__(trytes, pad=self.LEN) + + if len(self._trytes) > self.LEN: + raise ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )) + + +class KeyGenerator(object): + """ + Generates signing keys for messages. + """ + def __init__(self, seed): + # type: (TrytesCompatible) -> None + super(KeyGenerator, self).__init__() + + self.seed = Seed(seed).as_trits() + + def __getitem__(self, slice_): + # type: (Union[int, slice]) -> Union[SigningKey, List[SigningKey]] + """ + Generates and returns one or more keys at the specified index(es). + + :param slice_: + Index of key to generate, or a slice. + + Warning: This method may take awhile to run if the requested + index(es) is a large number! + + :return: + Behavior matches slicing behavior of other collections: + + - If an int is provided, a single key will be returned. + - If a slice is provided, a list of keys will be returned. + """ + return ( + self.get_keys(slice_.start, slice_.stop, slice_.step) + if isinstance(slice_, slice) + else self.get_keys(slice_)[0] + ) + + def get_keys(self, start, stop=None, step=1, iterations=1): + # type: (int, Optional[int], int, int) -> List[SigningKey] + """ + Generates and returns one or more keys at the specified index(es). + + This is a one-time operation; if you want to create lots of keys + across multiple contexts, consider invoking + :py:meth:`create_generator` and sharing the resulting generator + object instead. + + Warning: This method may take awhile to run if the starting index + and/or the number of requested keys is a large number! + + :param start: + Starting index. + + :param stop: + Stop before this index. + If ``None``, only generate a single key. + + :param step: + Number of indexes to advance after each key. + + :param iterations: + Number of _transform iterations to apply to each key. + Must be >= 1. + + Increasing this value makes key generation slower, but more + resistant to brute-forcing. + + :return: + Always returns a list, even if only one key is generated. + """ + generator = self.create_generator(start, step, iterations) + interval = range(start, start+1 if stop is None else stop, step) + + keys = [] + for _ in interval: + try: + next_key = next(generator) + except StopIteration: + break + else: + keys.append(next_key) + + return keys + + def create_generator(self, start, step=1, iterations=1): + # type: (int, int) -> Generator[SigningKey] + """ + Creates a generator that can be used to progressively generate new + keys. + + :param start: + Starting index. + + Warning: This method may take awhile to reset if ``start`` + is a large number! + + :param step: + Number of indexes to advance after each key. + + This value can be negative; the generator will exit if it + reaches an index < 0. + + Warning: The generator may take awhile to advance between + iterations if ``step`` is a large number! + + :param iterations: + Number of _transform iterations to apply to each key. + Must be >= 1. + + Increasing this value makes key generation slower, but more + resistant to brute-forcing. + """ + current = start + + while current >= 0: + sponge = self._create_sponge(current) + + key = [] + + for i in range(iterations): + for j in range(27): + # Multiply by 3 because sponge works with trits, but + # ``Seed.LEN`` is a quantity of trytes. + buffer = [0] * (Seed.LEN * 3) # type: MutableSequence[int] + sponge.squeeze(buffer) + key += buffer + + yield SigningKey.from_trits(key) + + current += step + + def _create_sponge(self, index): + # type: (int) -> Curl + """ + Prepares the Curl sponge for the generator. + """ + seed = list(self.seed) # type: MutableSequence[int] + + for i in range(index): + # Increment each tryte unless/until we overflow. + for j in range(len(seed)): + seed[j] += 1 + + if seed[j] > 1: + seed[j] = -1 + else: + break + + sponge = Curl() + sponge.absorb(seed) + + # Squeeze all of the trits out of the sponge and re-absorb them. + # Note that Curl transforms several times per operation, so this + # sequence is not as redundant as it looks at first glance. + sponge.squeeze(seed) + sponge.reset() + sponge.absorb(seed) + + return sponge diff --git a/iota/curl/pycurl.py b/iota/curl/pycurl.py deleted file mode 100644 index b88f06a..0000000 --- a/iota/curl/pycurl.py +++ /dev/null @@ -1,99 +0,0 @@ -# coding=utf-8 -from __future__ import absolute_import, division, print_function, \ - unicode_literals - -from typing import Iterable, List, Optional, Union - -__all__ = [ - 'Curl', -] - - -TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] - - -class Curl(object): - """ - Python implementation of Curl. - - **IMPORTANT: Not thread-safe!** - """ - STATE_LEN = 729 - - def __init__(self, state=None): - # type: (Optional[Iterable[int]]) -> None - """ - :param state: - Initial state. - - Note: this has the same effect as calling :py:meth:`absorb` - immediately after initializing the object. - """ - self._state = [0] * self.STATE_LEN # type: List[int] - self._dirty = False - - if state is not None: - self.absorb(state) - - def __getitem__(self, item): - # type: (Union[int, slice]) -> List[int] - """ - Alias for :py:meth:`squeeze`. - """ - return self.squeeze(item) - - def absorb(self, trits): - # type: (Iterable[int]) -> None - """ - Absorb trits into the sponge. - """ - for i, trit in enumerate(trits): - self._state[i] = trit - - self._dirty = True - - def squeeze(self, slice_=None): - # type: (Optional[Union[int, slice]]) -> List[int] - """ - Squeeze trytes from the sponge. - """ - self._transform() - - return ( - list(self._state) - if slice_ is None - else self._state[slice_] - ) - - def reset(self): - # type: () -> None - """ - Resets the internal state. - """ - self._state = [] - self._dirty = False - - def _transform(self): - # type: () -> None - """ - Transforms internal state. - """ - if self._dirty: - index = 0 - - for _ in range(27): - temp_state = list(self._state) - - for pos in range(self.STATE_LEN): - prev_index = index - index += (364 if index < 365 else -365) - - self._state[pos] = ( - TRUTH_TABLE[ - temp_state[prev_index] - + (3 * temp_state[index]) - + 4 - ] - ) - - self._dirty = False diff --git a/iota/types.py b/iota/types.py index a6b6b14..065e81a 100644 --- a/iota/types.py +++ b/iota/types.py @@ -4,12 +4,12 @@ from codecs import encode, decode from itertools import chain -from typing import Dict, Generator, Iterable, Optional, Text, Union, List - -from six import PY2, binary_type +from typing import Dict, Generator, Iterable, List, MutableSequence, \ + Optional, Text, Union from iota import TrytesCodec -from iota.curl import Curl +from iota.crypto import Curl +from six import PY2, binary_type __all__ = [ @@ -192,7 +192,7 @@ def from_trits(cls, trits, pad=False): ]) def __init__(self, trytes, pad=None): - # type: (TrytesCompatible, int) -> None + # type: (TrytesCompatible, Optional[int]) -> None """ :param trytes: Byte string or bytearray. @@ -410,7 +410,7 @@ def __init__(self, trytes): elif len(self._trytes) > self.LEN: raise ValueError( - 'Addresses must be either {len_no_checksum} trytes (no checksum), ' + 'Address values must be {len_no_checksum} trytes (no checksum), ' 'or {len_with_checksum} trytes (with checksum).'.format( len_no_checksum = self.LEN, len_with_checksum = self.LEN + AddressChecksum.LEN, @@ -440,13 +440,17 @@ def with_checksum(self): def _generate_checksum(self): # type: () -> TryteString """ - Generates the actual checksum for this address. + Generates the correct checksum for this address. """ - return TryteString.from_trits( - # Multiply by 3 because AddressChecksum.LEN is number of trytes, - # but Curl returns trits. - Curl(self.address.as_trits())[:AddressChecksum.LEN * 3] - ) + # Multiply by 3 because AddressChecksum.LEN is number of trytes, + # but Curl returns trits. + checksum_trits = [0] * (AddressChecksum.LEN * 3) # type: MutableSequence[int] + + sponge = Curl() + sponge.absorb(self.address.as_trits()) + sponge.squeeze(checksum_trits) + + return TryteString.from_trits(checksum_trits) class AddressChecksum(TryteString): @@ -457,10 +461,15 @@ class AddressChecksum(TryteString): def __init__(self, trytes): # type: (TrytesCompatible) -> None - super(AddressChecksum, self).__init__(trytes) + super(AddressChecksum, self).__init__(trytes, pad=None) if len(self._trytes) != self.LEN: - raise ValueError('Address checksums must be exactly 9 trytes.') + raise ValueError( + '{cls} values must be exactly {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN, + ), + ) class Tag(TryteString): @@ -474,7 +483,10 @@ def __init__(self, trytes): super(Tag, self).__init__(trytes, pad=self.LEN) if len(self._trytes) > self.LEN: - raise ValueError('Tags must be 27 trytes long.') + raise ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )) class TransactionId(TryteString): @@ -488,7 +500,10 @@ def __init__(self, trytes): super(TransactionId, self).__init__(trytes, pad=self.LEN) if len(self._trytes) > self.LEN: - raise ValueError('TransactionIds must be 81 trytes long.') + raise ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )) class Transfer(object): diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index 4c31671..830e78c 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TryteString +from iota import Address, TryteString from iota.commands.extended.get_new_addresses import GetNewAddressesCommand from iota.filters import Trytes from six import binary_type, text_type @@ -239,11 +241,11 @@ def test_fail_index_float(self): def test_fail_index_too_small(self): """ - ``index`` is less than 1. + ``index`` is less than 0. """ self.assertFilterErrors( { - 'index': 0, + 'index': -1, 'seed': TryteString(self.seed), }, diff --git a/test/crypto/__init__.py b/test/crypto/__init__.py new file mode 100644 index 0000000..3f3d02d --- /dev/null +++ b/test/crypto/__init__.py @@ -0,0 +1,3 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py new file mode 100644 index 0000000..20d1911 --- /dev/null +++ b/test/crypto/signing_test.py @@ -0,0 +1,151 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota.crypto.signing import KeyGenerator, SigningKey + + +# noinspection SpellCheckingInspection +class KeyGeneratorTestCase(TestCase): + def test_generate_single_key(self): + """ + Generating a single key. + """ + ag = KeyGenerator( + seed = b'ITJVZTRFNBTRBSDIHWKOWCFBOQYQTENWLRUVHIBCBRTXYGDCCLLMM9DI9OQO', + ) + + self.assertListEqual( + ag.get_keys(0), + + [ + SigningKey( + b'JFIBLUBLSESJFHGXQIXDLAQDWYVASWQIUNKDIWKTLRXXPICK9FLYDGGNFFVVCPMXQJ' + b'9GRHMZGEPXFJZITGJMSWBESYGGHLZCDUECMRZWD9AZTFCDYREMWZBQNROIHFANCCKT' + b'YCYNUBTDBZZ9FJXABLDK9RDMQTDGDXKDXIVVQWKL9AX9UONTPMHFMNNLNRRWIVZQWC' + b'N9YQMLOOCBWCPODBTLLAZKMVAGTUNVDQXNAMGOTNEYPBWHQEJGMJAGTPUFK9WHVJBC' + b'UWZNFACTAWRRUTJPCEMRDMUJBJUWOHSWNXRECJDZFSDHLCNGHKQC9SBVCLX9XYDXBL' + b'LGHWLYVMHUCKMGONXOAPZHFEPKDOFGVPUVJHZ9MKARJOBFHM9XYDZXDVPMMHRFOAO9' + b'HTFLYM9OXWOHOFDLVYVHFJMMBWHIMUZDTJ9HHPXDCCPZCSDJQG9FXTNSGJFRJFXDUF' + b'BRMCKHNWMHMGEGIGPRXIWLLAIBOWRJWDXN9SNDHYUUZUEA9KFFGLIUOD9ISSZF99AW' + b'SMPDGAIUO9IKWYTHPOHT9IZVJRP9KUSPLKJTQVLOKYUI9ZJWSYRNDKCISDPGCKC9OB' + b'ZJQRQWSUGEJQOJCMFPQT9VPBGIYTOILO9NDEPBUMMMUNCSDXAKRNDITYHZEABXFJXN' + b'AWDFFRQMECVUFNWAUDBAMKWGHYQQQVOPAUMGZYQEMZPIDALOYSFPRGRDAIXQXTBVKH' + b'XL9XKSZEUULZNYYNBESJJPC9UZAQAAMIYBSRP9DSCUCWUUNABNOXPVNUKLOBAYYTAF' + b'ZILWBYPCOMACRQV9CAZMWSRNSTYFHWPQNPRPBPSSNOCIUQDUBDKQRACBIWFBDSL9ZK' + b'OLFWA9VD9QWLZCDBR9WTKOROWNNKXMMLYULBFPBHIJRBRAJ9KTHOLTURRYSCMOKYEJ' + b'VVXNLXFDQVVTDBITCWAHANCEZMWOARWCIDIHYXBTCKFNBJDSOMTK9ZWYHLRHXTRYHV' + b'AOJFKUQODOXWOJBDRFSZIEOOOARIZWHHFMWIBO99JVTR9YFFYAJOIKWZEQIR9ZJS9S' + b'JJBWBWWIKRCUS9ZZJYDQPUGKIGHWXCQ9KTKZIXXQLHJKIGJFLJGYGZYSGCLMUVEUHI' + b'QFKFBDITMAHGOLSSEAUAMSERCDAVHLKWZSBSIYUWNQWL9WNHZOTYINOFISCG9HSCHJ' + b'JGHPVEETEXHJEVZPK9VMUQTRJXNZSUEBDMWMTPYVHINJWZYSSJDQZDIHAILHNSLWVT' + b'IOSH9L9BOWTALZHJTQVUEMZJCKCWDKPLBZWIBJBRXFGKRJELVDZOIUAEARPSLQXYCB' + b'VUOEQTVMBAYYPIAREVYXQNODJWJRLOTSO9QAGZXFSASKMR9RKRJZEEVR9TOBXAMZCG' + b'VFOAGPOJHWCLTYDPQMDRNRRFETTJEOFBIO9DQEQSYFWFUSUWLUUETHKHNUJMTEZGZO' + b'BXBMVLDVKVPAT9QCOEODDMDCKBWRJJJQTLETCJDIIBQADDHRYNMF9KFQO9SCMWLUKL' + b'9WSVOBDDEMEXIDXXCZLHR9DDAVESEQMUUIJHABWAPXSHCOEQOPCRZSTARLMXHCOGJP' + b'GOMGEIJ9UEJJPHHCODIEGQUDADINVUZW9CPYOD9CZVKRCIKXJLIXYCIRCYTGHTMF9G' + b'KPEATOTLHHOBVGJHKYFKGEOPVYFZBXPOGLWJVGU9LZFMCEXQGRFAY9UGSPNHMLPCCM' + b'TFYEOYFZFQSAGBGYZYTYPYROTIQRZOL9GYZWWZH9NYCSILWSPASDHMKHXJTZJESRJU' + b'SLJJGUSRXNN9SQSJECMPVUVPKLODUMVTYFIISPNFZAVIXKKLPQOWEPPVWOCXA9I9LA' + b'LPIOCDQKHPPGUABZDLDNUIBHMU9VCUPHKRTVK99IUBHLYCWDZCBMCZMGOWEXLKUQJF' + b'DX9HWXMOPLNEHOWBDAGZKLYGDLGXYAMT9WNHU9FS9UITGNZPTFGZZVJROGSWHEVCHG' + b'BDVAZQKPOMZKVZ9YHZGDMILQIGSRRDLQZMRIUYTWQUPNSCHKPLOMIGE9NZFGWJAPZW' + b'LLOJLXLSLZMYJONJPHIPTCNRE9AXDXKDQTGEVPHQWWCOBTCYLFCCQ9EGVLEARJZMNK' + b'IIALUYZNYBHNEUKZLNHQVJPAVLWOXLMGBUDMXAMFOHUBOTBDPDCSDBQLYDKATA9OBI' + b'JNYAPUZEF' + ), + ], + ) + + self.assertListEqual( + ag.get_keys(1), + + [ + SigningKey( + b'DZRXZABCDULCRMZFVHYJOUB9DSBPVQKGMXTIREWKPYPVRCVIXVILNJTXIOPOXQHDN9' + b'S9M9TACACAODTITIBNCDYOFKPMZV9WBGMWUHXOLEJDATUSUVPBUZMSBCWRHSVCPUOZ' + b'KBEWCVFGHSUFPKRSENQQEKXLOKOMA9XZXFZIPPVBTHL9OPCJZSMAOFGERLDITUVIPL' + b'YFTPZXDFWYRAATUFWWHTLC9MINHQQPGGMBDDFGJOZTSVLN9MANFRICSUKYJWYTACCJ' + b'IBZC9N9AOX9U9RUNUIKE9PERDFKYGVYCWGKPDMLH9TXVXJUOBXHJZRVOKTYUYUTKVU' + b'KZJ9TXULXGKTZHJXGVS9YHEUQXC9HSEDIYVOSCZQNBVEFPUJKIZVHRLRIINETNBTBZ' + b'OYJITWXFTXWPOEDQ9GEMBVI9UTVO9CTTIMTMWSBPNECUYHTUGVSAPLVDEAEWMQW9MC' + b'LIKNOFQUIIYQWNCZWKNXEKTVT9HSZDTFKXHJHWZBY9N9RY9TTMAIBGHIHQS9AAAUFY' + b'BRVBMHCVFXPRVFSLBKUIZFKIPDCJQYGHOHQMMBMFIARQYZSZZP9JVAVYVVJCLFNEJJ' + b'ADDPUVNZIAPCSQIYWHVGCHB9QWICLW9LZVKPMGEFJAICUGWQEPUMOWZHGBIJZ9EJHI' + b'LMCNLQV9OKEYDGGSJJAGPLFYWQVTHAWHEOQMBALJJCVJS9HLMMIVOSERMDCSKWXNRG' + b'PZTCGCOUJNWNERTBRMCAEGHREDQDHPV9HWNUJVXNRFDEMU9XHFW9YNJHXAGCZPIEGY' + b'ZZATBKATSJ9WUVUKPBLQBFROLHXRRRJECEYJBCYVS9LJXBLQTXRFTJOFKSB9IAWBEM' + b'EPATZKNVSSZLMR9WEKXFQOYKPGNULLMOJLOMLRXPWRWDUGRFIRMWXNQJMJKPMNRHQP' + b'GTKSWRML9JGPVMAOFNPCIBBIULYMWEGRGEU9GDXEILWNOEPE9BOADDINZCTJBEEICM' + b'WKBKACYKKLBFEHTNZIEYQVKFIOMUCSFZJYYYSJCOQPXZFKCGOOFQVYTGA9LZY9XKWL' + b'RTGYUNCREOEOCKNFNWPRCQX9TOXIOWYCIUDXEH9YSSPQB9XRPZZFUHQBSMQRFYQTSO' + b'Z9PJLWCITLTYAWKXYVJEUATOUB9RJUHTJSRLXBKFFN9DBLGWRJDMOWQVYAOFXWTGPB' + b'LOFOYILKVBYBGEIFDY9MMMOEVMIFRJGTFARXGUMRQFLETGPEXXGPBHNVIKPWUTVULF' + b'O9XHGHRO9BBDXFTHGMEGPGFUPEHBXRRJYTCTTJMIHUYPKVGJANAEHFDMNLDJTXJJUS' + b'IITNDUTQNE9NCSCESCWQQTDEKPVMCRTNAGLYIRSBXBRBSTKDOTXOJFSOPIZJDJAQHV' + b'UNPUPHJFXRE9VPJUNKMMTLBRJOJHINNWKRJTDEHXEWVFOCUINLOW9FYLMOL9PIXYSE' + b'RNMERMD9Q9RCX9RGCVOUQWOYKEN9VFQDJXUQAVEGPHUIGFTFUSQPFVQMDAOHEUKWWO' + b'NXSDPVLQQHTOTFU9HLBKZIGRLQKVNPONQQPATNBVQJJUZFGJOKKNWGX9LNFSVWUNG9' + b'LPZIQTUCEYXBO99ZCFJIFGTNQMIZ9TYIOBUJAURIFCIDHVNI9BZEOJIQBNVZCZBDTV' + b'NWJMGPQBFIWRHWVUUGXCHDY9KDTMPEBVLOH9NTTXGZSPMJBXXBSWXTGJWCRDFEKRWP' + b'VOWYWEMOKUCAIFQXSJHERTZDIHOGAHCBU9VET9VWJXEJHEJSZMBLCFKKRHQVJJLJ9V' + b'BBSCEYLTNDOIWWCHFG9CWEWAASSDCIKJYCQZMGULSNDEDGQLN9YEXWX9PICDAY9SGH' + b'EYGTVHZTQCKHHO9RUYWQNPUKZRREAXXTHOOKZAAJFHMERHBTMTGZQBCNTWHGUYQOBT' + b'AQEOXRFPDDWEVMKEGVY9FSYRLQUCGFQLPVEQ9WHRHNOVIBXPINZEUCRQDDVVESGGWV' + b'GPTAQBJ9ECJBGAFIPFVVYFDJRUZYMQHOKFNCLFAWFCDQBGRWKLGWH9QXCUSSEECU9Y' + b'OQJWPRUJNQJEYPGYBZUHQDIOUJHYUFQDFPMMJVABHTWVESLBAEIVGQHSRXPXDKLAKL' + b'JMPE9WCNWAJKNUVODEALTUHLYWMBMZHHHNYRZHVKJGDEYSKDRKDSFZTLG9ECVNMJCE' + b'DBPIYONHA' + ), + ], + ) + + # You can request a key at any arbitrary index, and the result will + # always be consistent (assuming the seed doesn't change). + # Note: this can be a slow process, so we'll keep the numbers small + # so that tests don't take too long. + self.assertListEqual( + ag.get_keys(13), + + [ + SigningKey( + b'YKEKPCPHHCDVLTSTIZIZ9IEPNLNFOH9KUKWPOWYVOBYJMAZOXMLLIPEGV9EFTGTZIN' + b'QQNWLKHJFHPPNFG9FVSGHXHNFAEZPGOKDHWSUEJYRHRQBQUT9ZTDOIWLRUDKEQWAVK' + b'HPGOCUCXMJDXVUHTSBDR9QQSTAOUZVMQAKSCGMZEONTUHHBIYPJLEHVOERXBOUUBBA' + b'VNMOKNUGVULVWIXOWPSBTGWADVDPXMA99UCYLAOMCIIQLASFDCH9ELGZOBMZQIVXLK' + b'OTN9CHKIKGYP9MURJDMQBEWDYRYYKZHHEBPFKWSX9CJPVWFMXXVRKRSXKARJHOQWAT' + b'MDYUZW9HWXFWRFZNNAAZWOJDAU9Z9TFPN9GAVKLKOCYBXRWOQYVXUKQBASZYVWALLQ' + b'JFUNVDNAERSUIVUBQEIZRKWSFSQNIUBS9CV9BGJVELACHBRXYIIUJPEMK9UYXATOLY' + b'CPIVPBHFNZRSVFONJCWAQHDTWVRX9OXPGXRIQCOG9ABYHLVYVGBZOAXALIQUDZWDU9' + b'WOPLLCIJIOSBWVIDHKTXOBMP9PMDPBRNYHGFESMFXVCUTIJICEPFMPQ9OZ9RFSQBDJ' + b'ZUIDBONHSVYSGGNKNCJNTHWSGLPNKCPJWEEJOSKUEWTWLVXCQPQJOYSGWQPQSRDWBQ' + b'TZMVXHBYPEKUGQEEAWDIWQGEANMSCCJGBBGJMHC9JPVIA9OXDLYNNSOSMAIAR9KIW9' + b'OGNPWFHYBOYUXBVUZOTNCLOTTDNXYKWTXCQZZXJAUQMKMKOWH9LODZCTTOBXWSHASL' + b'ROJUUGJFBBYQEIUVEAWZNHBRVIYJEEMCVFBGUDURGFTGAKTZXZLTNL99MLYPSCHMIT' + b'WMRRBKTHZBBXXBHTSYL9WTQOWWUENRNIMPPGWJCDVBDAQJQRNLBZBVDLWQHLJYQYHS' + b'R9OXTMZUURLJWVB9VLSDD9QWUTBLYRNZFIEYECJYSIKBST9ANUXMAFABIOVUOYCCMW' + b'HABCWLLFTXPLSSGPBIIAVWUQOIKMSDNU9YNGACVDLVSIMFLNJVOODKDCGPLMSBDQVP' + b'UAGXCJBEVNCTGJBKZWYBBWMBMEFAGPKHGIKVPUVTHSXVA9FUMBTFDZPDSZPJTA9RQF' + b'GTHGVFSZ9RCKT9XDLPKUILGFAGBY9UXIGSVPMYGLHNXDRVPI9FRKTYVKXXF9ZWHJNO' + b'JUYPUPLTVPLUMDMGGXMUPD9HHNHORYLOKPCLDZYJCUBNIXDMSLPAIOWCRIETT9PNNB' + b'VKZDHI9UQQQUFQJTTONTGOLH9RMOVMIAQMUKPUSKEGGBIUXISILL9V99PIGJXJVTSN' + b'OK9OCSTGDRGF9VFWJRSN9SUTDC9IOPBCMOLUYJIIKWOONQ9IKYQZLGCCFDNOAUMNDY' + b'DVXDBJNKJYPETMXYUOFWQAICTWSLLCQVGZTPGLVZIWVCDKEYJSKGS9YFVUVMKJWREJ' + b'IAZPZOHHDBRVUTSJJUNZEOFAASHHXRQRZGAZIVUYDNWSIYKZMUDJWHCJF9DUBULOPS' + b'NFLRUZVCELFNQSGFXRZVCUGBVBLLFVAERLCTHWUNLFDQFVRSZMFJYWJBDIJP9NBJWB' + b'DCKGRGTDC9VJGKFFVGNSAIXGONKTBYDBBVKNWOIRW9POJPY9ALBPYESXDDNTWSGRFS' + b'KTPHMDXH9WHNRDBYWDHVUOFJ9DDMSQDTORHSQYRWOXYWVCTMTPFFEZKGGDTMKSYVOP' + b'OKRQSUNHQSQ9TVKVXE9HYGVBREVVXAKPUPKES9NKTWBCUAVHUKC9OTWVMERMMIDHDP' + b'CREDKX9MEJJLCFCUPGUP9JFKDRUF9NAUXPGCCRNA9VBHJBE9FFHFJERFWRUQVAANQM' + b'ILUEW9DVBPFCKNHXKAMCZLIFQBTQUHJZSVNNJWSJMFDXKXYCPTAEJYQMSUBCYJMBFJ' + b'KDHVZN9XTCSHXXNAO9PFQAPTJJGPARTJZLJ9NZPVAHEJIGWEZPFHCGJGPOBPGBMZRM' + b'MUTICVGUMLNCONHOAFTKDHPYZEFCFTDNIMQCBWMRLJPFMFMNUDITECHUCGQXMLKZRL' + b'VQCQXXKKX9MOMDTPFBI9JQVNRQFRIRIEAYTXDBYWPLYTRHAFYF9LQAVVFOTNUPOTSA' + b'9TCNZCDVIFQWITACKXQYHBDJURGJFRLVCSNDWAOQNZIN9KBXDIXKROW9BWYZBLSSSC' + b'LFFUZRVSM' + ), + ], + ) diff --git a/test/types_test.py b/test/types_test.py index 971c193..c525efc 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -183,7 +183,7 @@ def test_init_from_tryte_string_with_padding(self): def test_init_error_invalid_characters(self): """ - Attempting to initialize a TryteString with a value that contains + Attempting to reset a TryteString with a value that contains invalid characters. """ with self.assertRaises(ValueError): @@ -192,7 +192,7 @@ def test_init_error_invalid_characters(self): # noinspection PyTypeChecker def test_init_error_int(self): """ - Attempting to initialize a TryteString from an int. + Attempting to reset a TryteString from an int. """ with self.assertRaises(TypeError): TryteString(42) From 002fc07b1072d98064643c0a2c840ecb5f5aec31 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 19 Dec 2016 16:41:58 -0500 Subject: [PATCH 142/239] Added more unit tests for KeyGenerator. --- iota/crypto/signing.py | 46 +-- test/crypto/signing_test.py | 672 ++++++++++++++++++++++++++++++------ 2 files changed, 584 insertions(+), 134 deletions(-) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index b75cbe6..35a7d8e 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -9,6 +9,7 @@ __all__ = [ 'KeyGenerator', + 'Seed', 'SigningKey', ] @@ -35,17 +36,23 @@ class SigningKey(TryteString): A TryteString that acts as a signing key, e.g., for generating message signatures, new addresses, etc. """ - LEN = 6561 + LEN_MULTIPLE = 2187 + """ + Similar to RSA keys, SigningKeys must have a length that is divisible + by a certain number of trytes. + """ def __init__(self, trytes): # type: (TrytesCompatible) -> None - super(SigningKey, self).__init__(trytes, pad=self.LEN) + super(SigningKey, self).__init__(trytes) - if len(self._trytes) > self.LEN: - raise ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )) + if len(self._trytes) % self.LEN_MULTIPLE: + raise ValueError( + 'Length of {cls} values must be a multiple of {len} trytes.'.format( + cls = type(self).__name__, + len = self.LEN_MULTIPLE + ), + ) class KeyGenerator(object): @@ -58,29 +65,6 @@ def __init__(self, seed): self.seed = Seed(seed).as_trits() - def __getitem__(self, slice_): - # type: (Union[int, slice]) -> Union[SigningKey, List[SigningKey]] - """ - Generates and returns one or more keys at the specified index(es). - - :param slice_: - Index of key to generate, or a slice. - - Warning: This method may take awhile to run if the requested - index(es) is a large number! - - :return: - Behavior matches slicing behavior of other collections: - - - If an int is provided, a single key will be returned. - - If a slice is provided, a list of keys will be returned. - """ - return ( - self.get_keys(slice_.start, slice_.stop, slice_.step) - if isinstance(slice_, slice) - else self.get_keys(slice_)[0] - ) - def get_keys(self, start, stop=None, step=1, iterations=1): # type: (int, Optional[int], int, int) -> List[SigningKey] """ @@ -128,7 +112,7 @@ def get_keys(self, start, stop=None, step=1, iterations=1): return keys - def create_generator(self, start, step=1, iterations=1): + def create_generator(self, start=0, step=1, iterations=1): # type: (int, int) -> Generator[SigningKey] """ Creates a generator that can be used to progressively generate new diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 20d1911..760a277 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -9,53 +9,71 @@ # noinspection SpellCheckingInspection class KeyGeneratorTestCase(TestCase): + """ + Unit tests for KeyGenerator. + + Strap in, folks; SigningKeys are multiples of 2187 trytes long! + + Note to developers: to pretty-format signing keys so that this file + doesn't exceed 80 chars per line, run the byte string through this + code and then copy-paste the result into your test: + + .. code-block:: python + + print('\n'.join(repr(s[i:i+66]) for i in range(0, len(s), 66))) + + References: + - http://stackoverflow.com/a/1751478/ + """ def test_generate_single_key(self): """ Generating a single key. """ ag = KeyGenerator( - seed = b'ITJVZTRFNBTRBSDIHWKOWCFBOQYQTENWLRUVHIBCBRTXYGDCCLLMM9DI9OQO', + seed = b'TESTSEED9DONTUSEINPRODUCTION99999ZTRFNBTRBSDIHWKOWCFBOQYQTENWL', ) self.assertListEqual( ag.get_keys(0), + # Note that the result is always a list, even when generating a + # single key. [ SigningKey( - b'JFIBLUBLSESJFHGXQIXDLAQDWYVASWQIUNKDIWKTLRXXPICK9FLYDGGNFFVVCPMXQJ' - b'9GRHMZGEPXFJZITGJMSWBESYGGHLZCDUECMRZWD9AZTFCDYREMWZBQNROIHFANCCKT' - b'YCYNUBTDBZZ9FJXABLDK9RDMQTDGDXKDXIVVQWKL9AX9UONTPMHFMNNLNRRWIVZQWC' - b'N9YQMLOOCBWCPODBTLLAZKMVAGTUNVDQXNAMGOTNEYPBWHQEJGMJAGTPUFK9WHVJBC' - b'UWZNFACTAWRRUTJPCEMRDMUJBJUWOHSWNXRECJDZFSDHLCNGHKQC9SBVCLX9XYDXBL' - b'LGHWLYVMHUCKMGONXOAPZHFEPKDOFGVPUVJHZ9MKARJOBFHM9XYDZXDVPMMHRFOAO9' - b'HTFLYM9OXWOHOFDLVYVHFJMMBWHIMUZDTJ9HHPXDCCPZCSDJQG9FXTNSGJFRJFXDUF' - b'BRMCKHNWMHMGEGIGPRXIWLLAIBOWRJWDXN9SNDHYUUZUEA9KFFGLIUOD9ISSZF99AW' - b'SMPDGAIUO9IKWYTHPOHT9IZVJRP9KUSPLKJTQVLOKYUI9ZJWSYRNDKCISDPGCKC9OB' - b'ZJQRQWSUGEJQOJCMFPQT9VPBGIYTOILO9NDEPBUMMMUNCSDXAKRNDITYHZEABXFJXN' - b'AWDFFRQMECVUFNWAUDBAMKWGHYQQQVOPAUMGZYQEMZPIDALOYSFPRGRDAIXQXTBVKH' - b'XL9XKSZEUULZNYYNBESJJPC9UZAQAAMIYBSRP9DSCUCWUUNABNOXPVNUKLOBAYYTAF' - b'ZILWBYPCOMACRQV9CAZMWSRNSTYFHWPQNPRPBPSSNOCIUQDUBDKQRACBIWFBDSL9ZK' - b'OLFWA9VD9QWLZCDBR9WTKOROWNNKXMMLYULBFPBHIJRBRAJ9KTHOLTURRYSCMOKYEJ' - b'VVXNLXFDQVVTDBITCWAHANCEZMWOARWCIDIHYXBTCKFNBJDSOMTK9ZWYHLRHXTRYHV' - b'AOJFKUQODOXWOJBDRFSZIEOOOARIZWHHFMWIBO99JVTR9YFFYAJOIKWZEQIR9ZJS9S' - b'JJBWBWWIKRCUS9ZZJYDQPUGKIGHWXCQ9KTKZIXXQLHJKIGJFLJGYGZYSGCLMUVEUHI' - b'QFKFBDITMAHGOLSSEAUAMSERCDAVHLKWZSBSIYUWNQWL9WNHZOTYINOFISCG9HSCHJ' - b'JGHPVEETEXHJEVZPK9VMUQTRJXNZSUEBDMWMTPYVHINJWZYSSJDQZDIHAILHNSLWVT' - b'IOSH9L9BOWTALZHJTQVUEMZJCKCWDKPLBZWIBJBRXFGKRJELVDZOIUAEARPSLQXYCB' - b'VUOEQTVMBAYYPIAREVYXQNODJWJRLOTSO9QAGZXFSASKMR9RKRJZEEVR9TOBXAMZCG' - b'VFOAGPOJHWCLTYDPQMDRNRRFETTJEOFBIO9DQEQSYFWFUSUWLUUETHKHNUJMTEZGZO' - b'BXBMVLDVKVPAT9QCOEODDMDCKBWRJJJQTLETCJDIIBQADDHRYNMF9KFQO9SCMWLUKL' - b'9WSVOBDDEMEXIDXXCZLHR9DDAVESEQMUUIJHABWAPXSHCOEQOPCRZSTARLMXHCOGJP' - b'GOMGEIJ9UEJJPHHCODIEGQUDADINVUZW9CPYOD9CZVKRCIKXJLIXYCIRCYTGHTMF9G' - b'KPEATOTLHHOBVGJHKYFKGEOPVYFZBXPOGLWJVGU9LZFMCEXQGRFAY9UGSPNHMLPCCM' - b'TFYEOYFZFQSAGBGYZYTYPYROTIQRZOL9GYZWWZH9NYCSILWSPASDHMKHXJTZJESRJU' - b'SLJJGUSRXNN9SQSJECMPVUVPKLODUMVTYFIISPNFZAVIXKKLPQOWEPPVWOCXA9I9LA' - b'LPIOCDQKHPPGUABZDLDNUIBHMU9VCUPHKRTVK99IUBHLYCWDZCBMCZMGOWEXLKUQJF' - b'DX9HWXMOPLNEHOWBDAGZKLYGDLGXYAMT9WNHU9FS9UITGNZPTFGZZVJROGSWHEVCHG' - b'BDVAZQKPOMZKVZ9YHZGDMILQIGSRRDLQZMRIUYTWQUPNSCHKPLOMIGE9NZFGWJAPZW' - b'LLOJLXLSLZMYJONJPHIPTCNRE9AXDXKDQTGEVPHQWWCOBTCYLFCCQ9EGVLEARJZMNK' - b'IIALUYZNYBHNEUKZLNHQVJPAVLWOXLMGBUDMXAMFOHUBOTBDPDCSDBQLYDKATA9OBI' - b'JNYAPUZEF' + b'BWFTZWBZVFOSQYHQFXOPYTZ9SWB9RYYHBOUA9NOYSWGALF9MSVNEDW9A9FLGBRWKED' + b'MPEIPRKBMRXRLLFJCAGVIMXPISRGXIJQ9BOBHKJEUKDEUUWYXJGCGAWHYBQHBPMRTZ' + b'FPBGNLMKPZYXZPXFSPFUWZNRWYXUEWMP9URKVVJOSWEPJKSMPLWZPIZGOTVAA9QQOC' + b'YISMGHSBU9YCXZCMSTPJVASDKEVZCSPNSPYOUUWWFTNWZTTZBKGZ9PDNAKNSGNODSB' + b'IRKUGFYCZXIFHQCDTXQNLMKRVKIFJS9XARBNMJQOTDL9CAOKEXQTMWCKWRNHLLMLYP' + b'QGTDFNTDBHNAFRBEUWTKPKPECAADKRPEAFDHABMYYXQPQYDSGFRSRFNHFHHHTAH9YF' + b'OXKRZOTKAHZPRISHZRR9YBVSOZUSKKU9HTCXPTPZFAHFMOQJBKZIACZB9ZRXFPPPMY' + b'RBCWPBAPRFXLQZOTGXJGMZUUEZIAVWXUEN9UIFLEESVCCGNKDISMEPYWTXDQHOSUWZ' + b'OEHOCZQJKDCJJNRZVODVNNUOV9FZQEXFGAMDKMV9PVUYMWTFNISYGYKQG9OKNOQUEK' + b'YDEJ9EGHUXQFCPHTTVBCRTZJLOAWHGDEQHLPLHWTWVOBCCQTCWCNLYDGUV9FKFZENU' + b'NOCOYNU9CYQDSAQDSMZGRQYB9YOCFSOHQXANMSPYFVCTPTZKUGIGUZMPJIJRZVN9VX' + b'ADLIVJYJGQWXBOBBAROGNIOIJVRUHWMFLVCGTMZISADLTXVEZLQDYAVQ9OCYQDPCYL' + b'DD9SUPNTUMUESC9VRSCAYBPXAPTYXZODUUMBNCSLOWJYJA9JBITZLZNHPZFGSPRURJ' + b'HYFLSFTMEPAEG9DWRFOTFWWPGGDGZWFVEPOHDGNMOXUSR9AVQLNDUMGPYWVN9LKEIZ' + b'Q9MNIUPJXPTRYMSXRA9GTSZFMNZZT9Z9HJOKVBCHRRIZZN9UYTVRDNHOXYSFRO9FRK' + b'HWNNZ9DXTLV9D9PNJLGWJXAFUOJZTRVJOLYGSPNVCXYWMTOEEUBLNRGAJPK9HIWGZM' + b'HMBTHLABTACRYLQIDPOYEFNSYQ9YAQPOYYDCJAAAVDUWCHSS9OKQYH9CQNUPYRTCWK' + b'CXYDKTAIJKWOQIHSGBZMFJXGOQDODBDZNBOPFYCBLSU9RJYVGXINUDODGHNAGFDAEG' + b'LPDVSCJPCIZHOFNHCZUTRLQXEZUFDZVROFXVHUNWMYRSZHBZAFCWIY9ULTBTEDSKEC' + b'CLDGAU9MZXYRAXVY9NIQUYHATJCZXDSAELMCXQMALHNFMEAWHIQAZMQOO9QPEPDOYC' + b'OJXTUWEJHMPXGZBXFPNXUPOSDINZJNIREYDFZMESFQUPBKSDGTAJHEZSCOVSLYUAUK' + b'DIWNNLQJTPYYTPRGGN9IXIORWXHJBPYINQZIUXKLKXCTQZYJIRH9MHYBQIFQZCFAKZ' + b'DUUZTYIMTNNVNKVMFIW9UYRTVRQHMYR9Y9VYFTPBJGSB9VINGTMBKVZJEZUE9RMBDZ' + b'CQGDHNPW9YIJHLGFOG9YAPXZECSFVXAMPILBIHC9DBGMIE99YEPGTAALOHBUKXSGFZ' + b'YHHWFOIHMEDXFHIYSUHADOCKFNGHKTNPZHINAWG9YGRJBQGECRCVPXXOG9CNVJNFLC' + b'LMGC9I9HAGTAGGVRKCXDJWDNHYZBFNQSKH99MFAMLGRSBMIBBMHBDJTVSQ99ZHYPSS' + b'XLUNFCNOJXITUETNBHIGXFLLEHUKEXGJLO9BBALXMNGJKTETFIZHSSKLOQXPXOSZRW' + b'QP9A9RIHWEHATMSVMZEQPGAUQBCIAQXZSUUFSU9HYK9RAVASYCVNALKJJXAJF9RLTD' + b'ZEIIYCFLQVMHBPBFHHQNXVEKPHOOFTQEIVB9IXZMTOFBHTGLPWDYVPO9HHBPVWZYEG' + b'IDMK9UPWEJDLIPJSIGFKKCZFRJVDN9ENWADNOTFWZGUDJRRUMFPVXHNAJBBCI9WEDK' + b'RKCQUHRQTCFYFHXPOFBC9BCENMI9HRSIUAKLWEAOUXRBWMHWLGEOCP9NWIJAXODJDS' + b'P9SKEXEDVUGHZAFPNMR9PXD9THOWNWWTDTWTYMDINGC9EBBVUYZRUQDVSIOXAEVGFP' + b'XS9CLTHUESMTDWUJNCZSOIOEJG9WKNAZMDJGMRBXGVMLUAN9IGDVFAESJMXNTMNFND' + b'CAXEBRAU9' ), ], ) @@ -65,40 +83,40 @@ def test_generate_single_key(self): [ SigningKey( - b'DZRXZABCDULCRMZFVHYJOUB9DSBPVQKGMXTIREWKPYPVRCVIXVILNJTXIOPOXQHDN9' - b'S9M9TACACAODTITIBNCDYOFKPMZV9WBGMWUHXOLEJDATUSUVPBUZMSBCWRHSVCPUOZ' - b'KBEWCVFGHSUFPKRSENQQEKXLOKOMA9XZXFZIPPVBTHL9OPCJZSMAOFGERLDITUVIPL' - b'YFTPZXDFWYRAATUFWWHTLC9MINHQQPGGMBDDFGJOZTSVLN9MANFRICSUKYJWYTACCJ' - b'IBZC9N9AOX9U9RUNUIKE9PERDFKYGVYCWGKPDMLH9TXVXJUOBXHJZRVOKTYUYUTKVU' - b'KZJ9TXULXGKTZHJXGVS9YHEUQXC9HSEDIYVOSCZQNBVEFPUJKIZVHRLRIINETNBTBZ' - b'OYJITWXFTXWPOEDQ9GEMBVI9UTVO9CTTIMTMWSBPNECUYHTUGVSAPLVDEAEWMQW9MC' - b'LIKNOFQUIIYQWNCZWKNXEKTVT9HSZDTFKXHJHWZBY9N9RY9TTMAIBGHIHQS9AAAUFY' - b'BRVBMHCVFXPRVFSLBKUIZFKIPDCJQYGHOHQMMBMFIARQYZSZZP9JVAVYVVJCLFNEJJ' - b'ADDPUVNZIAPCSQIYWHVGCHB9QWICLW9LZVKPMGEFJAICUGWQEPUMOWZHGBIJZ9EJHI' - b'LMCNLQV9OKEYDGGSJJAGPLFYWQVTHAWHEOQMBALJJCVJS9HLMMIVOSERMDCSKWXNRG' - b'PZTCGCOUJNWNERTBRMCAEGHREDQDHPV9HWNUJVXNRFDEMU9XHFW9YNJHXAGCZPIEGY' - b'ZZATBKATSJ9WUVUKPBLQBFROLHXRRRJECEYJBCYVS9LJXBLQTXRFTJOFKSB9IAWBEM' - b'EPATZKNVSSZLMR9WEKXFQOYKPGNULLMOJLOMLRXPWRWDUGRFIRMWXNQJMJKPMNRHQP' - b'GTKSWRML9JGPVMAOFNPCIBBIULYMWEGRGEU9GDXEILWNOEPE9BOADDINZCTJBEEICM' - b'WKBKACYKKLBFEHTNZIEYQVKFIOMUCSFZJYYYSJCOQPXZFKCGOOFQVYTGA9LZY9XKWL' - b'RTGYUNCREOEOCKNFNWPRCQX9TOXIOWYCIUDXEH9YSSPQB9XRPZZFUHQBSMQRFYQTSO' - b'Z9PJLWCITLTYAWKXYVJEUATOUB9RJUHTJSRLXBKFFN9DBLGWRJDMOWQVYAOFXWTGPB' - b'LOFOYILKVBYBGEIFDY9MMMOEVMIFRJGTFARXGUMRQFLETGPEXXGPBHNVIKPWUTVULF' - b'O9XHGHRO9BBDXFTHGMEGPGFUPEHBXRRJYTCTTJMIHUYPKVGJANAEHFDMNLDJTXJJUS' - b'IITNDUTQNE9NCSCESCWQQTDEKPVMCRTNAGLYIRSBXBRBSTKDOTXOJFSOPIZJDJAQHV' - b'UNPUPHJFXRE9VPJUNKMMTLBRJOJHINNWKRJTDEHXEWVFOCUINLOW9FYLMOL9PIXYSE' - b'RNMERMD9Q9RCX9RGCVOUQWOYKEN9VFQDJXUQAVEGPHUIGFTFUSQPFVQMDAOHEUKWWO' - b'NXSDPVLQQHTOTFU9HLBKZIGRLQKVNPONQQPATNBVQJJUZFGJOKKNWGX9LNFSVWUNG9' - b'LPZIQTUCEYXBO99ZCFJIFGTNQMIZ9TYIOBUJAURIFCIDHVNI9BZEOJIQBNVZCZBDTV' - b'NWJMGPQBFIWRHWVUUGXCHDY9KDTMPEBVLOH9NTTXGZSPMJBXXBSWXTGJWCRDFEKRWP' - b'VOWYWEMOKUCAIFQXSJHERTZDIHOGAHCBU9VET9VWJXEJHEJSZMBLCFKKRHQVJJLJ9V' - b'BBSCEYLTNDOIWWCHFG9CWEWAASSDCIKJYCQZMGULSNDEDGQLN9YEXWX9PICDAY9SGH' - b'EYGTVHZTQCKHHO9RUYWQNPUKZRREAXXTHOOKZAAJFHMERHBTMTGZQBCNTWHGUYQOBT' - b'AQEOXRFPDDWEVMKEGVY9FSYRLQUCGFQLPVEQ9WHRHNOVIBXPINZEUCRQDDVVESGGWV' - b'GPTAQBJ9ECJBGAFIPFVVYFDJRUZYMQHOKFNCLFAWFCDQBGRWKLGWH9QXCUSSEECU9Y' - b'OQJWPRUJNQJEYPGYBZUHQDIOUJHYUFQDFPMMJVABHTWVESLBAEIVGQHSRXPXDKLAKL' - b'JMPE9WCNWAJKNUVODEALTUHLYWMBMZHHHNYRZHVKJGDEYSKDRKDSFZTLG9ECVNMJCE' - b'DBPIYONHA' + b'WEVFRAOIRJBSBKQWF9JOTQXWUDLOIJRAC9WOOJNW99UAXVMUMSCMAABBXI99PRTAQL' + b'UWJKVMM9DPSZSU9SAUN9URDWGGXIHWJJCDBAY9OQMQURNHZBD9E9CGERZC9RSUQVMZ' + b'VYUTYXLH9CCEQPQLVDICQD9UCH9RPP9NSNZEBYERXHDEBEZUZOKNNEMXBYZPAYIIOB' + b'ED9LRBWBBKGEPSONFKHAZPKSUKRDZABHEJNNWXAVTVVENGQRQSH9Y9XMGMAU9BDPBT' + b'MLTVKCIQESSKRDYKELSAPDODV9HY9YKSWGKHOQGMOQUDYOXKDXERZ9EWCZPLCBRCRE' + b'AGOZHPYMUTZEOODHUGROGONAAGAETOIEJVRLXXQMXYAZZNKUBQEOMPNYREMGWTAWSX' + b'XJX9DWXLMCGJM99SGKRLSJTXHBCFQRDGHCVSXNSDTDKP99KLZRJLRWTTYXA9CLROYZ' + b'OONLHBPVLECRWSVVLJPKPAAOICLSRBZIVSNTQGTXRYYDWEBIOLVOXCVFWYIKAQMNJT' + b'BLGRBYWMNK9MD9FAAKDWGMRQQ9ZELXGMYJF9WH9HEZEYKPHGDCVCPEIEXMDQMGIOSS' + b'9KCVPCQHSHALBYJRSRNJNJXMSBOIURZAABSRESLVLYHJINSNGDGQHGOLLMEBR9PQND' + b'KZCYRTQTPZQHPZLVDCHIKEIARRUYREQBUXYHVZGTPNAUZCVCTDQUDAZHBRCWXKBIIN' + b'IAMU9MUEIDDZRVYPOAKWYUFULQGYXMC9PCBLATKZROJSKVSQIXZOTKWIWBII9XHZYU' + b'PJVPEHCCOPUFNTYDMEEJOEYB9RYZAO9TTDKUMKBQGIQDTSKQLDXB9ADYOQ9KTIQLNE' + b'XVO9QOGLNEPNYKUUKDPHBABJLODZXBKUCUUXDPHA9L9ZCCDUMWOJIWRLDAO9YISWMB' + b'GGUKKMNTLBUUASGSNYOUTDNSZJTOKZPLZXBUYTLNVLIXVDKPSVUK9GYIPYMXALZLWN' + b'TVQHOYRTCNQQWYAUVSVAUYXXJMBESFVIJZTLEOICAUHZTOSJLSSLRELXZXIQYMQIG9' + b'OSZFTTYAWIRZYWBBVAZDERDFMVDOCSCXMHJQYWPD9N9MKRZTZVTSTDETHMOACATA9V' + b'UIFBALVQILCEZZIHPQGYUVHUQVGERPFFTITFEISHIRPLEPQBNNNWFUXVBZSDNMOPGA' + b'ETRVNI99YDQBYMUUHGGXDTHTKTQUGKXBELCTDGCQRRAPKEFQJUTXDO9QDYWJCYPAQW' + b'GDAOCNVEGLEICLXPTCSFUGYPNBBBABFSJ9JKTPEXQFAXIBSLHTKWSS9KYPFFDKMZZX' + b'CWKDJQZ9ZTWAKYHTVFPENX9EJBYLC9PJCNKYFMJJSIA9NNEQVTLCPDESHEUTKZBHDF' + b'SGBNRAWFVYXHXQKNOTSPULDRSQEZHVDCIOMJB9XWDUFJMBHMXQRHNWHKYYDDPOLC9Z' + b'XASGTHOEVRJVESTRZZLJQNYKCNHBHIHCAZCAFHEGXUDFCBAJLLWLJPZFXMHXBXUOCN' + b'GCIRTIASUQTDZQUAXHOOYICWFDK9VTBQOMUXJICSXUQQPFSTDYITFAXWFHMVPMCUWZ' + b'ZJAV9HIKFWXHCFXNNNXIREOBYDELGZTZTOKXVEQTMKSMQGXHEPWYOBASQGYCDZYCZX' + b'IJRPVGPTXOCRPAPPMBNQDVQBPSWHSQVALOBQGILEAUWIPCCXPTZRRQHAGW9SRCODAH' + b'FVCW9TYQLVTPMUDWYBXKGFFDBMDIYOICLBRJPAUKQVTVQEJUWMCMBJVZZFVUUJQKW9' + b'ADITOSRWTUHUBWMRZFBLV9VV9TYLOKVLJXVPNEOMQFFVKN9AFGREGUXFNVBZPAMRLT' + b'RRUNOPMYCWJLTMVJDEHJEUAQPFMZLDJSUHOYVISZTYGQN9ZHUBMMKFKSFGI9XXVBRJ' + b'JUHPEAQZANTOTTSQRPWRVLORFC99ATMGSOBLIV9JLDWPOHCIYLLFYKSLCVEXHNPXFG' + b'JUPNCKZEVL9JXMCGAJJDATXWSLHILXC9MWHMBWTIJG9ZWXFGMDAZOFCNMMBGPJLMRS' + b'MIZSVNFVNWGZHIMCXRPEIGIZ9RPNYVSAF9HTDX9BUDPIJTKYCBMAJPAYHG9OMOUYEE' + b'ZT9WPVUWXCIONWGPQAEWAMHQFDWVSHACZIEJOSDTEVRYAPZCHFHSYKEAHTPAGABIHV' + b'TLMWMCVUE' ), ], ) @@ -112,40 +130,488 @@ def test_generate_single_key(self): [ SigningKey( - b'YKEKPCPHHCDVLTSTIZIZ9IEPNLNFOH9KUKWPOWYVOBYJMAZOXMLLIPEGV9EFTGTZIN' - b'QQNWLKHJFHPPNFG9FVSGHXHNFAEZPGOKDHWSUEJYRHRQBQUT9ZTDOIWLRUDKEQWAVK' - b'HPGOCUCXMJDXVUHTSBDR9QQSTAOUZVMQAKSCGMZEONTUHHBIYPJLEHVOERXBOUUBBA' - b'VNMOKNUGVULVWIXOWPSBTGWADVDPXMA99UCYLAOMCIIQLASFDCH9ELGZOBMZQIVXLK' - b'OTN9CHKIKGYP9MURJDMQBEWDYRYYKZHHEBPFKWSX9CJPVWFMXXVRKRSXKARJHOQWAT' - b'MDYUZW9HWXFWRFZNNAAZWOJDAU9Z9TFPN9GAVKLKOCYBXRWOQYVXUKQBASZYVWALLQ' - b'JFUNVDNAERSUIVUBQEIZRKWSFSQNIUBS9CV9BGJVELACHBRXYIIUJPEMK9UYXATOLY' - b'CPIVPBHFNZRSVFONJCWAQHDTWVRX9OXPGXRIQCOG9ABYHLVYVGBZOAXALIQUDZWDU9' - b'WOPLLCIJIOSBWVIDHKTXOBMP9PMDPBRNYHGFESMFXVCUTIJICEPFMPQ9OZ9RFSQBDJ' - b'ZUIDBONHSVYSGGNKNCJNTHWSGLPNKCPJWEEJOSKUEWTWLVXCQPQJOYSGWQPQSRDWBQ' - b'TZMVXHBYPEKUGQEEAWDIWQGEANMSCCJGBBGJMHC9JPVIA9OXDLYNNSOSMAIAR9KIW9' - b'OGNPWFHYBOYUXBVUZOTNCLOTTDNXYKWTXCQZZXJAUQMKMKOWH9LODZCTTOBXWSHASL' - b'ROJUUGJFBBYQEIUVEAWZNHBRVIYJEEMCVFBGUDURGFTGAKTZXZLTNL99MLYPSCHMIT' - b'WMRRBKTHZBBXXBHTSYL9WTQOWWUENRNIMPPGWJCDVBDAQJQRNLBZBVDLWQHLJYQYHS' - b'R9OXTMZUURLJWVB9VLSDD9QWUTBLYRNZFIEYECJYSIKBST9ANUXMAFABIOVUOYCCMW' - b'HABCWLLFTXPLSSGPBIIAVWUQOIKMSDNU9YNGACVDLVSIMFLNJVOODKDCGPLMSBDQVP' - b'UAGXCJBEVNCTGJBKZWYBBWMBMEFAGPKHGIKVPUVTHSXVA9FUMBTFDZPDSZPJTA9RQF' - b'GTHGVFSZ9RCKT9XDLPKUILGFAGBY9UXIGSVPMYGLHNXDRVPI9FRKTYVKXXF9ZWHJNO' - b'JUYPUPLTVPLUMDMGGXMUPD9HHNHORYLOKPCLDZYJCUBNIXDMSLPAIOWCRIETT9PNNB' - b'VKZDHI9UQQQUFQJTTONTGOLH9RMOVMIAQMUKPUSKEGGBIUXISILL9V99PIGJXJVTSN' - b'OK9OCSTGDRGF9VFWJRSN9SUTDC9IOPBCMOLUYJIIKWOONQ9IKYQZLGCCFDNOAUMNDY' - b'DVXDBJNKJYPETMXYUOFWQAICTWSLLCQVGZTPGLVZIWVCDKEYJSKGS9YFVUVMKJWREJ' - b'IAZPZOHHDBRVUTSJJUNZEOFAASHHXRQRZGAZIVUYDNWSIYKZMUDJWHCJF9DUBULOPS' - b'NFLRUZVCELFNQSGFXRZVCUGBVBLLFVAERLCTHWUNLFDQFVRSZMFJYWJBDIJP9NBJWB' - b'DCKGRGTDC9VJGKFFVGNSAIXGONKTBYDBBVKNWOIRW9POJPY9ALBPYESXDDNTWSGRFS' - b'KTPHMDXH9WHNRDBYWDHVUOFJ9DDMSQDTORHSQYRWOXYWVCTMTPFFEZKGGDTMKSYVOP' - b'OKRQSUNHQSQ9TVKVXE9HYGVBREVVXAKPUPKES9NKTWBCUAVHUKC9OTWVMERMMIDHDP' - b'CREDKX9MEJJLCFCUPGUP9JFKDRUF9NAUXPGCCRNA9VBHJBE9FFHFJERFWRUQVAANQM' - b'ILUEW9DVBPFCKNHXKAMCZLIFQBTQUHJZSVNNJWSJMFDXKXYCPTAEJYQMSUBCYJMBFJ' - b'KDHVZN9XTCSHXXNAO9PFQAPTJJGPARTJZLJ9NZPVAHEJIGWEZPFHCGJGPOBPGBMZRM' - b'MUTICVGUMLNCONHOAFTKDHPYZEFCFTDNIMQCBWMRLJPFMFMNUDITECHUCGQXMLKZRL' - b'VQCQXXKKX9MOMDTPFBI9JQVNRQFRIRIEAYTXDBYWPLYTRHAFYF9LQAVVFOTNUPOTSA' - b'9TCNZCDVIFQWITACKXQYHBDJURGJFRLVCSNDWAOQNZIN9KBXDIXKROW9BWYZBLSSSC' - b'LFFUZRVSM' + b'ZIGSOMJRQUXQHMNP9NCEGWCNXXVMJW9BXYRTMVUWRVTFQ9GCMJOOENTBSJDKPQTWML' + b'FGEMPNODQWJ9BIZJSFWOOQNYLHIGUAJJXGISEMZKVPQOLQIMKESDECLJLDJFTRSBCQ' + b'UEAJTCMUDYYWWENQZAPI9B9B9RBOCEAUAQHQNCZTWDOCYCRXZNHYTTTTNUNROHJGCF' + b'VRPV9RWZFFQOIWCIYSTWIQOZFYMESMFYPQTZNLFTUPGROJSQRCSOPRDYQFM9UBJDJZ' + b'CIV9UQDPZWRAEELURWQWZVM9BMZL9YMVLFBYXRXOJMVVAWOCQQDVGDGXMKOAQWU9YP' + b'UUXSGYZBSLQEVLLSMLIHISX9UGOPRKIVYMCLJXA9CRUSBKEHVDHPEBELEDIMT9VDVP' + b'KVZLZJCWKQQQTWQOWLYYHNVYFOIMLLXIZFMYBPZ9E9U9ZZIRNGPP9YYLPSDHKDOMQB' + b'PZAKGCVBQCOCERITODGEQUISDBUQSAASVCJRFOPIAQRAWMSMWLWFQ9GGRNFCCXZELB' + b'UAISJVCMDEWMWQPAWTAXKHSKBGWCZKHIBQCIUUBLGPVHAEVLMFBGQZXFY9AGCYWCBI' + b'JKDULRYBMAHXXBBTDMXZMWK9VMJKGXL9WSQCDSVZSDAKYETPBUZHVJOCZSDKQVBNF9' + b'PHETFF99VSCZGHUZGJPGLDLGTWHWRAZTBTWONDXVNDAUMGZAUKQTIVNTMQHTCHPDBL' + b'JWHOXITFMDDLF9UZ9QVMJPHQAOKJVJNICLSHHFOEOLEPJJBRREWMGGZKOSLLIXOXRM' + b'XSXWBHPIFQGZDXWIANKUMERQQKITASHTK9UTWYIVBCADTHTCLPAQOKSWOCTVBPDISR' + b'TXFOX9JJGSSIMUJNBLCATMROIYTULKMSDSAOXHIFOEQXORNQBJIQXTKWKHONKWWGLL' + b'IECZX9QGVZKURJ9GQJNJWMRMZQAJALNICBUQOWVPOLHYHLCDHNMGNBQPJDGGXFDQVZ' + b'GUOILR9MPAPCGJ9ZZ9YWBJKQGDYJSGK9TFBIJWSMIWWCUFJ9QCGUNEQMDBQDODDWJN' + b'FMCZOIJREHRNFSIQIBLLDGPHWGZB9MPKBGHFTRRFZRZJYOZRAYVB9AUOGXLGHHDGBE' + b'XOVAYLBAVUEAQCBIWNVJJQSCIE9TOL9RQSYLYAZQCZGGOIIDOSHFZDJDZITGVHMZYI' + b'Q9WGIQDGCKUUGCQAZYCSMNRPXKPDIUUHBFNGPRORGLXSGEDQHHYIJKBKCFZFTRQCYY' + b'BKRETMZWKQWFMKZGETJSLZFBTA9JETYEYN9VJUBJKM9SEJROWFPRB9NAPMDMIZOCUB' + b'BUJWFMBBSSUSDKLXVMDR9HXZGYJIEX9JJXTUBMROYERCUNFQUDUDGHHFH9CRTDPTAY' + b'PPFHIJTBGPQHMAJWHMPDCPBBJLEULBBMWKCDPHQZE99JWZGJWYIZKWIZIIXLWTYVTB' + b'LLPHSLMXRLBFTMRPPXADNVFRLFYCJZNLRFZXTR9MVDANWPSNZBVFXPKVBQJXATDCBQ' + b'VSCXJZCQSMIEPDJARJF9NKDVXZAY9EPTDGTCGMEWMCPGOKOZDYZAYTTOEDGNGSZUIH' + b'CQWQXMGTXFGKUQ9EQFMOUWXBTEIJZUYFTTFFFJBKYURQKTXCNIZOIONWOBKRFMRVSB' + b'QP9ACSWHOAPYIWNCDSLZGXQAKSBBUZPDNQEBWFRISIZIXLHIHCBTQT9SONHRHRGERF' + b'RJGBHWSVUUGQAEABDGWXBRIQIQHSINHXNI9BDJUCQKHTPDRNMVD9GFBIUIMCJSEFYV' + b'DEUUQFF9RRMZPAPRJUVCGIGMJEHPYMUMASUROXAYQVTVEB9ATIYKRSECWLZLWWGIAX' + b'VGCOXFIIYTME99PT9CCKQWHJYMSIMCAUPN9PXGCHBCTWBJD9JGSZPOTWYRWQOYEES9' + b'GZJHTDQVAWFKJYHW9NOMLXDXYWUCER9KJVDAKEVWCUBHSQWIWVHVZLQACWHNSPELZA' + b'UBGQHVZPVKRIAZSNYWTEQSXAJDNKNXTFH9QBOXLRZJRMJSSCMWHQWJCTZ9YNDJORJF' + b'APQQSJALFLUGIBFBRPYKKM9DP9ZKPGVIBGIVVPXFQQURTFRBAVNWWF9DXIDEDCXMDE' + b'NAEDEEZGLLWGYKXYEGLYC9FKYNKARWYIGVWBLGROXSEBAOIVARNLWTUEJUYYNHIQXS' + b'DDAKAUN9T' + ), + ], + ) + + def test_generate_multiple_keys(self): + """ + Generating multiple keys in one go. + """ + ag = KeyGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999TPXGCGPRTMI9QQNCW9PKWTAAOPYHU', + ) + + self.assertListEqual( + # ``get_keys`` parameters have the same behavior as slices. + # I.E., ``ag.get_keys(1, 3) == ag[1:3]``. + ag.get_keys(1, 3), + + [ + SigningKey( + b'VBGOUVYVPMMVMNYIASWCSDOECOVM9BLBAWECSHKURDUVTFBRMUCIADFORQXPPCDVGM' + b'QAGEPRS9IVKLZTJOVOBFXWC9PDXBL9WCRYKCNNFWOAJCKKV9KZICCQKUOFUAUYTRJH' + b'DTCRDLWCXKSPK9ZMVVFMGXBPQRCVAFMRAGVHWFKQEDPXPR9UTYOSKPMGMZZWLW9SBZ' + b'PIQKJAPCAVMOWRURRUKYP99ILMGYGNUTCKXEHXTYHPJXUJJBATFLBJTZZSKUZZVTRU' + b'BNJJJCBXTVNUVFQQPOYIIRVJGJQGPIDDSPNRTAQLHBKJNJXSGSUOHXSWRTDOSLB9OG' + b'WPRBDCZMRCFEUHQMYZGLRSPCSMCYHZQLRPYTFFLVJDYTEETUVIAJLKVPWIEGKJ99LO' + b'LHSDQNDTFIXTRY9RCZTOTCYJEDIK9YOQLEEYIAMTSOENXLAALCHWJUSYFJZEZPVBUH' + b'NSIQRUDTVQUKJZWYJDNMSMFDVDTFSAODYOGFYK9OTEIHQGSSUVINXURWBAAYVNRYVR' + b'YIHQSSPKQKFKVGJRTHMRMSBLIWKHSCFIMAJIBTKMVADBOEWTANSJZCUGQRCLVIB9ZY' + b'PYPIEJPQAXOPEEGWYHJJTPZRHOSTSWIPQMC9T99IMALJONRAOLSJVLGHAQSXNCCIBF' + b'XBJZA9FVP9TBQDHNJPSAWSJ9HMCKDX9RGQEUHOHLXPVJHCFBEQTLDVXYOMLRNUGFWK' + b'UGXNYDKXKAKNHFYVO9DXNALJLUSROLNMPSOOAQKPDHGXHCOAVMYRQDPQQQRBQESBOW' + b'L99CCPSYYYSMTLDLVEWUFWLAAZMINKRRAYIZRENIKFSYFROVZFCYQIH9BYSTUEWNHT' + b'SA9LQDET9XLMYAKAR9MJILMHKMZAZPXSGVMKMZWCNO9RGGPJBGYRDEIZWSALUVVYEZ' + b'VKJKCIOWBHLGUICAPMSCOKHGYVGUTKFCEPZNFQPUIQTKCLQIFKDQYESLKYERQPQDLS' + b'YXIUAZVHWGFCAPVQOITULEJEVQRQXFYKZPUEVKWKQENGSGFLMFSPPBBPZL9NPFYILD' + b'MWCRVHYYBEIJPLGSLDJ9ATXIWHPM9VSBZBOLAVMLRUHLAPDVHVAMAOWPQMP9BUYUIG' + b'PWJCCXSTCNABWECAVAJGBBSJXIDOOZKWMXTENEXHBLJAZHNFTTORCTHELRFDYRCCWI' + b'YJXUL9BWE9YPVTHVFCSUGRPAOTTKIFJAAS9MDRENZYAGEHARCOVARJHUWJCKLHOKQA' + b'ZPRXOGKOBVCGCQFUJAMQZMKQOZPXZJZKJCHADNKZV9ZKQ9DUHZOCNTU9ZGKWHJODYH' + b'JVSOOCFPKSAE9EFOLSNWYXYGVONYLFBHIKGRBMSS9URVCITGYCHXKVWYRQYTLWZLOU' + b'QXMTDADHDZGWYKWQVZLSUPMTPBHWAUWQLLKXHY99QNSMQXAMHHWLVL9WHTALBLVMAP' + b'WVFFCPXMUZTQCBPXNRQMKDEIZBIOCOJVUYGXFKMZGTCIYMX99RGMGKMWTRJ9N9ONLG' + b'XJDPMYECS9AIJSEDALMOMSHBLDRI9AYRWDEZYNRBOEB9DIDXXEBOPBDZPPEQTKYRMJ' + b'AEAZWUYQXROVBY9JZZGBRDUQFEKHH9TZMTLNNFEZBFFPEXQIHDZE9TBHDYN9SKGMA9' + b'SYZXUSBOOXREZAQMFFKPFGTWUGUDV9SKSNH9ONNWDJYKZCBYCZIDZCFOMBNGNCSQDI' + b'RCCEMYSHNWKVWJFWFPI9UTYEZRZHECQKGBGKUJEEGQQGSVNOVMIKGBRPXACAS9VNZW' + b'CIYAXUHYOFJCXTAFZVWFXVVELGHCUNISAMCVJDNW9XXNLSFOXHWPBNYCUDZHHDITG9' + b'WETROSQWYMRIIAAMLGNJSR9HMSMICDEWBHLXDJNMGVLTWOCEUPTFCNCIZZ9OGIANLO' + b'WKFGGKLXODJDYNIDSECWHBIYSYHVUDROUGW9V9KBQCGMZVSUIGYMKAZSMYTLBKDNYH' + b'ZLPFTCJZCDAPGNWJVQBLYVEWAEQKGGJMLEQPUDXPXPHZPYGTHZCGPUGTXNGUNLZJZV' + b'QOCSNRTMVYANLYHVBRZXPYAGSQYDGZFASTYTXKATWLJZHGUF9MH9VWAGGWEVRCKUCC' + b'PYSXYKHFRVHGLZVTXYJZ9WPOPBHLPYMPQCZNOKCNDNKDNQOQTYCRAPCJDQGRCBEHCM' + b'REONEIFHT' + ), + + SigningKey( + b'H9VZYRIEGPFTUOMXYHPDLGNNWEXKHOMNHI9YIEOCBXRQJTZMYW9LHXTZDQEYMWUAGY' + b'SJASVKHVXRJBMZINYZX9WBUERAH9EV9XDESZESVMYMQRNCNERUUGCYLBKHZDDLAPGU' + b'MIEMROYBKAIETO9JVWLFPOHJZTFOTU9FNMOECNZEOFG9DKVSQNQBXWLVDTBCPMRAUN' + b'AOCMVLMNYRHFCONSTS9BGEKBHTVMEBQCWKQVHYADOPGS9DILCOQSZPLJTSFVCRWDVZ' + b'FPSPMUDDGQGZLFYWLXKXWUKDNGVMDSJ9WQMDGNMCQUXHDORCRQUVSTUHDPDQOVMRJP' + b'JGALZNEEVZQWEYELKZATPCZK9TLJOSUCBUEPPOZAGKYKMSKQDGHTDFZMAGJEBMXC9F' + b'HMZQODXKGGXFYZ9JQ9TPO9ED9EASXDU9NNLIUZMKCJNZVGMJCXCATGLABBR9TBTPNA' + b'LUQS9FICSSWFQWZCSMPECJZOQKIUBOCHFBGJIMIAXCBMLQ9DRNJLE9LRBA9ZLAKVWM' + b'BVLGARBWLX99GZVWOPGE9JXVATWKLWDIGDZYWKMRWSVCAUKCFZANFDEBEDOXRCMNSK' + b'JELOAPHRLVEZOLICVBEXFYUVIMPAY9EBAXZCVNUZPE9IGGONTDGKDLXRQNLTSJDQIC' + b'EWAINBIIFTWIDYNZGSP9ZMMDBIMAWA9GVZOAENXYYRRFTEI9CUTDJ9RMJCVK9QDH9C' + b'KFMROPLYAJHOLO9KDJSXFZZBEXC9XIDJCCDELTQIUG9FBMNKNOEMGMFGLEFJIOM9R9' + b'ZTKIKAJGJHRHONUZAJGJONYTGXUEYBAXRJHJ9PMYQBMNAZZKMHWHHLTYUVDGNEGIDO' + b'AYAAUMHLYOTEXFHTCGSVQRYKGOXECJSEZFZMLFFFXO9WWQHIEXXJZWRGVQUQQRSCKO' + b'ORUCSQFUCQYO9MQW9XADTARGZYFUXAAJXSSGPHLWVDLOIYUNVGAENJWQLSMNKCBVPZ' + b'XCWBWO9NTUAOUMWGFPAEHCPXNTSYIUMRKMYMTZDVQMN9QC9GF9RSAWDIYJUXVELERK' + b'UCKHJUUCGP9DQRVMKVJMPNONGTOVXQYBTEFHTBQALQAMDWLJWYQAPNWIONSAVYTGQZ' + b'SSTQ9KHRZKU9GVWXCMSTTBKRILDDDADFIPYSKHEYNPVXOCNATUQGWUWYPDSCEWIXCO' + b'UGFCWRVRBSQRLPXLCVWJDAVXOCAXLYBMQUQEVKPWSNPBXPKZPLXUBKKTINVMZFUMXY' + b'EQIURPJXBSNAZERRDWMQARORPS9SVKSOIQIEXGNEQD9ZJJGFNYXFPA99BAMK9TLDLT' + b'QTJHSFKEXRFCPIJPNXZVXPQDDBOVGQDLD9E9JPDNNTISHFTEICSCSGWRPNCICTZRDU' + b'XKL9PZDJXYDUITTHTKTQRXBQHSYKIDVBFQJLE9SA9SEJHWAJRGXRTZSKJTNDBFQLPB' + b'LRHPPQUQPD9INBFAWOJUYCQG9ALWQKVAJCGAKGBWZLOCCEPRCJMSJIYLMGUPTUDDTY' + b'9QWLWCPTMPRYTBJHKWQNALSLRAZVWUFJULOHRVPGGAPST9LDLIVVWXG9MOXLJQEAMY' + b'UJNZVOHHBATSQMLKDCOXHJPJAJEYFRDYSKHVIDLQYDLRJLUBPJLZRQOEOCMBPYQIM9' + b'PRTOHDATAUWGDFAJBPJQSFPCGPIZOMNTXWGPKUTETANJUAGDZOBBBMOPJAASNVUC9J' + b'DOXSGNHCUCFICUBYKPKUJJCXCFXCZCWWCGGJAHDNIGCFEBMQCBDIUIURDX9FMBVCY9' + b'IYFDRWVYIABFNSR9MBQGISPSBCRVCLWFMY99SYNYNUCRGAWKPGJXV9LOYWSEHREKRQ' + b'WFKGHBREVCQYESRHGQMDHOPPNHZWR9TMVXUXEKORNWQANFWMPFQ9FJHSBFJGPCGUFX' + b'ROODDYB9SEI9BENQMPDKLMUUXB9LTTTGOYUIJLGIIDIEJBYAPFYUGTEEVKKBVMQNOY' + b'OTBKGRLTMLZMOAAEGCGRNMABTOJFKYQYDAYNSJSNXGKDEVKXAVYHBBSJIEFPOD9JMD' + b'CEROCHTBQPNUYRAHRTVOUPAMVY9MLSWCMSJYM9FWJFEHKSFUVOACFBVXKQVRXEMANB' + b'XDGGYBSACXOLLZ9EEZGFXLSINKTWLUGQEABYPOFXVMCQSYRJBHGQJRCNTNWQJXRAFH' + b'SHWFKWSBP' + ), + ], + ) + + def test_iterations(self): + """ + Using more iterations to generate longer, more secure keys. + """ + ag = KeyGenerator( + # Using the same seed as the previous test, just to make sure the + # key generator doesn't cheat. + seed = b'TESTSEED9DONTUSEINPRODUCTION99999TPXGCGPRTMI9QQNCW9PKWTAAOPYHU', + ) + + self.assertListEqual( + ag.get_keys(start=1, iterations=2), + + [ + # 2 iterations = key is twice as long! + SigningKey( + b'VBGOUVYVPMMVMNYIASWCSDOECOVM9BLBAWECSHKURDUVTFBRMUCIADFORQXPPCDVGM' + b'QAGEPRS9IVKLZTJOVOBFXWC9PDXBL9WCRYKCNNFWOAJCKKV9KZICCQKUOFUAUYTRJH' + b'DTCRDLWCXKSPK9ZMVVFMGXBPQRCVAFMRAGVHWFKQEDPXPR9UTYOSKPMGMZZWLW9SBZ' + b'PIQKJAPCAVMOWRURRUKYP99ILMGYGNUTCKXEHXTYHPJXUJJBATFLBJTZZSKUZZVTRU' + b'BNJJJCBXTVNUVFQQPOYIIRVJGJQGPIDDSPNRTAQLHBKJNJXSGSUOHXSWRTDOSLB9OG' + b'WPRBDCZMRCFEUHQMYZGLRSPCSMCYHZQLRPYTFFLVJDYTEETUVIAJLKVPWIEGKJ99LO' + b'LHSDQNDTFIXTRY9RCZTOTCYJEDIK9YOQLEEYIAMTSOENXLAALCHWJUSYFJZEZPVBUH' + b'NSIQRUDTVQUKJZWYJDNMSMFDVDTFSAODYOGFYK9OTEIHQGSSUVINXURWBAAYVNRYVR' + b'YIHQSSPKQKFKVGJRTHMRMSBLIWKHSCFIMAJIBTKMVADBOEWTANSJZCUGQRCLVIB9ZY' + b'PYPIEJPQAXOPEEGWYHJJTPZRHOSTSWIPQMC9T99IMALJONRAOLSJVLGHAQSXNCCIBF' + b'XBJZA9FVP9TBQDHNJPSAWSJ9HMCKDX9RGQEUHOHLXPVJHCFBEQTLDVXYOMLRNUGFWK' + b'UGXNYDKXKAKNHFYVO9DXNALJLUSROLNMPSOOAQKPDHGXHCOAVMYRQDPQQQRBQESBOW' + b'L99CCPSYYYSMTLDLVEWUFWLAAZMINKRRAYIZRENIKFSYFROVZFCYQIH9BYSTUEWNHT' + b'SA9LQDET9XLMYAKAR9MJILMHKMZAZPXSGVMKMZWCNO9RGGPJBGYRDEIZWSALUVVYEZ' + b'VKJKCIOWBHLGUICAPMSCOKHGYVGUTKFCEPZNFQPUIQTKCLQIFKDQYESLKYERQPQDLS' + b'YXIUAZVHWGFCAPVQOITULEJEVQRQXFYKZPUEVKWKQENGSGFLMFSPPBBPZL9NPFYILD' + b'MWCRVHYYBEIJPLGSLDJ9ATXIWHPM9VSBZBOLAVMLRUHLAPDVHVAMAOWPQMP9BUYUIG' + b'PWJCCXSTCNABWECAVAJGBBSJXIDOOZKWMXTENEXHBLJAZHNFTTORCTHELRFDYRCCWI' + b'YJXUL9BWE9YPVTHVFCSUGRPAOTTKIFJAAS9MDRENZYAGEHARCOVARJHUWJCKLHOKQA' + b'ZPRXOGKOBVCGCQFUJAMQZMKQOZPXZJZKJCHADNKZV9ZKQ9DUHZOCNTU9ZGKWHJODYH' + b'JVSOOCFPKSAE9EFOLSNWYXYGVONYLFBHIKGRBMSS9URVCITGYCHXKVWYRQYTLWZLOU' + b'QXMTDADHDZGWYKWQVZLSUPMTPBHWAUWQLLKXHY99QNSMQXAMHHWLVL9WHTALBLVMAP' + b'WVFFCPXMUZTQCBPXNRQMKDEIZBIOCOJVUYGXFKMZGTCIYMX99RGMGKMWTRJ9N9ONLG' + b'XJDPMYECS9AIJSEDALMOMSHBLDRI9AYRWDEZYNRBOEB9DIDXXEBOPBDZPPEQTKYRMJ' + b'AEAZWUYQXROVBY9JZZGBRDUQFEKHH9TZMTLNNFEZBFFPEXQIHDZE9TBHDYN9SKGMA9' + b'SYZXUSBOOXREZAQMFFKPFGTWUGUDV9SKSNH9ONNWDJYKZCBYCZIDZCFOMBNGNCSQDI' + b'RCCEMYSHNWKVWJFWFPI9UTYEZRZHECQKGBGKUJEEGQQGSVNOVMIKGBRPXACAS9VNZW' + b'CIYAXUHYOFJCXTAFZVWFXVVELGHCUNISAMCVJDNW9XXNLSFOXHWPBNYCUDZHHDITG9' + b'WETROSQWYMRIIAAMLGNJSR9HMSMICDEWBHLXDJNMGVLTWOCEUPTFCNCIZZ9OGIANLO' + b'WKFGGKLXODJDYNIDSECWHBIYSYHVUDROUGW9V9KBQCGMZVSUIGYMKAZSMYTLBKDNYH' + b'ZLPFTCJZCDAPGNWJVQBLYVEWAEQKGGJMLEQPUDXPXPHZPYGTHZCGPUGTXNGUNLZJZV' + b'QOCSNRTMVYANLYHVBRZXPYAGSQYDGZFASTYTXKATWLJZHGUF9MH9VWAGGWEVRCKUCC' + b'PYSXYKHFRVHGLZVTXYJZ9WPOPBHLPYMPQCZNOKCNDNKDNQOQTYCRAPCJDQGRCBEHCM' + b'REONEIFHTYTHN9XKG9OCEPEFEVTPCKRTVYOTBCKBAEEUDDNWINAHQTWWREQAHW9SIB' + b'WLZJSTEHDVGJYLYHBODMSDWGOQIBM9HCQNMHAGGYF9O9QWBXSPBOFCOGYBMKXXKQEA' + b'9YDRIKBKROKSBPYIDQNIHAHNOMPTOEJVTLVIAVGZFRPQLDDMDKCIYATYUYGPVAMHOT' + b'JEDXYGTOZHQALLKTBBJKHRBGVBBSSQSAGFABDMJCOAPFXLNPWPV9DTYLIWMEWIH9AW' + b'WQMVCGFOFRTDRTDNIW9DJUUISRFEWPLOCSVGPEMXUJXQXBHVFYPFPAHWRKIWZXQCRC' + b'NZCSNDY9EBDEGARIXIVTSONP9GZLATF9TLWLESEXRTWIUGMAJFTCJLNBSHXEPNNWJA' + b'HNWSFBTHD9BFYIN9C9WEJBLNJNSQTGJ9H9ZUSAIARGZSLFGXH9SMALFIMNIIWHTJOB' + b'A9NDZICTFRPXQGMLGCZ9JCGLRFBZHUVBIWGUDWGLBROTUQWNPSD9RVC9ALPBJWEYIV' + b'ZRFWUUBNUPHDLFOAT99YMZNBYTX9XWWUSBZGPUP9LPKNPJOMQJEVTXSXRJACTLPIGS' + b'XFFUUICWM9TQDIBVTDEIROVSTAJXSJLKJZGCCC9JCPTKKBVTEYMDLJMLAUPZGQCWWA' + b'ZMMUGZPSWLZTJTGSSWHTVDWHHV99FKK9PTVXQSTRFKMMH9QX9IWLALNSXITGTHQOCF' + b'PLVEDVDBCUY9XCJQZIKV9NS9LIYNQTVYFUSYMPZTS9PUQHVBHNTVPZCKIDHWNOGHO9' + b'XMEWBE9HMBNCCMYGBOVDJO99YV9SARRENSNOG9JHYHIFNBIFYZPWIRWZGSOTPFYOFX' + b'GVRFPVUBNERKXGKDYENHECPC99JOCOFNDULLNDMDREJWLBWFEUIDJCUQVDERFDYDYC' + b'FID9VDWWDBECVFRDSZGPMTRNGQHAMRXADYFJJBGKEQEOVXSDYICOGUYDRBHOAXRSHJ' + b'XPIJIDWAVSSMGDAIDUCDKIIOZEDDKSSZSFOPRHRNPYAXAQOMVNDSKJIP9KTQZBFVOZ' + b'ZZMDLDLQCCOQNKQEA9FCCGDMZGEDJIWGZZS9GIUOHEAPVDQTSTWTMVMJA9SHZRHZCS' + b'9KOF9ZSSJWKA9LLK9TBWAZYARJJQVWRKZQXSMRGUOXFWLAIWERAVIWAHPXAKPCRYTX' + b'XMYXDGSVG9SBPY9IWVAUS9VWXVLGTDDEWMFLKQTIRDKNTNJRNALMKJMMAESGODAUWK' + b'WUNJZNG9OLZCZRCQDFGVSR9UXUOVDJBOXMKFB9FWFSTQHVILADJHEAGHWBICNZZGXK' + b'B9SDZPECJXXXXMEHPTISE99MOCHFUDJLKSZOMWIAFYQQY9YCSMZC9RUFPNBIHUMXLI' + b'FBEDUDFDOKQGKLMWTILEEXOQLAYEPKHZQWRTXEKXJYCGIYWZHWVH9PYZRYXLVMUEBT' + b'MPSUAAHVSBQPXBGSAUUWDFNRJJIJLBZBFBQZGQKHKFQUY9FKPHTOIUZAKUKUQAOXTA' + b'VXFZYJQZL9YMAYEEOMPYAILPEPTJMRGBWNAARSSINLJGD9ZFQELMDUBNHPZWYFZQGW' + b'QQXCNGOQGMTXRFSXYOXZFCBTSTHBQ9FWYEXHKZEWQNMLOZSHJVKQY9FC9FZQKUTKYF' + b'FLMMNXXPKOMOOQL9F9HSANCTLOQT9YSTGQCFBEANXUGFKGKQQJFYRPEBBVOFZCGHFJ' + b'HKFGISYFIXDZACBIDGOUCMLVZQOWXFARMNEWJTNBSXYSZNYYXYRTZAQFVSHAHR9LPP' + b'9VFBLSQWFRCGVUKTVXYZSGFRZDQGUYEWOBNJGVDGVVQDOQIHSWNTHOURYPDVZDHTYL' + b'VMLSLCENHG9AR9OQFHRFODEHOGVGTYLGUYG9MZK9VPOTPVOINJRJSNVVDK9WEBCHJL' + b'FR9PAWCEJGPQOYCRQCN9DWHWDBXBRXUIXVMQJCFRNLRGZOQEDQCSRFUNLMKNEFQDVV' + b'CGFF9VWUEXGYYUFRJPJNKQHXXGKRTUNTCCFSMDMDYU9PLQEJHSLFX9XQ9KEFNLIFDU' + b'TENVG9VGPXLZPJCLIYXYMZIJFXWCBXQJDFSBTLHGMHLJHUCSNWDVLKIWNPTCEYCNOD' + b'UFKOJCHLELHMNRPEXOKTDE9VQUIRCDZBUBKKCDTQOKTKJGRSJJEJS9XUSASFJSQWMR' + b'PMWYTXQHXYUOMVQCVH' ), ], ) + + def test_generator(self): + """ + Creating a generator. + """ + ag = KeyGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999IPKZWMLYYOLWBJGINLSO9EEYQMCUJ', + ) + + generator = ag.create_generator() + + self.assertEqual( + next(generator), + + SigningKey( + b'LZDQHHPRICEESIHX9VVYCHLYSJDMXPLFOMMVOMUZKHSXLQYSQNDFHWLNKTCAJYPWUM' + b'RMILEVQCHTGILDRABOOXPNRQHWBXVBRIZELVFEHTMYITSVBBBUQCZMDEUFGCTKHISF' + b'MI9XQOBAIOVMCKNIGJQANHDMCWHYFJLDQLPMHLULLZZXYNZUNDLMRITVORDUEGNWKH' + b'VOFM9YRMGEOM99DOJICXVVFUHFHTZGFROK9LWRPZZMB9GIVUSEKB9KEZOHRSRMVPOS' + b'IGFM9INOXQSKTBI9O9PIDRUUWG9SDWJPGVK9SZMDZ9XJDHCIRMYBVZSXLMYZOWKZEX' + b'TRCY99JKTTESVPWPTXCAOEKIFC9IRCXTRTIOCDUCJZR9VQMHUXSZTSKFHTABGZBLSY' + b'CMKUHEASAVH9NBPAIJO9QITSPIEGJNCINILWFZU9BCXOEAZ9SGXKDUNOJZQPCRVWEA' + b'QBNYTYAPSEFGQYAXYNIEFVOQZIYPDZHHGKANGQXBT9GCCZKRFUJMQLCDOBXWLQWYYQ' + b'KOYXPNYHJWVGYPKLW9XIESVOX9MSOBTMJHTYEYGERFLUCPCJJLGULVEVGQRB9EATRW' + b'ZBFLGVMAORSIWAIWLMSEYK9OIBOHLOXLSSIOSRTMLHLY9HGKYEOSVNDLDNKYFPVUYF' + b'FCYEATDCRRZSYAJRSJTZ9HYEATYKORKQIMHND9SALEMSBSJPLLAIHK9QLWTSKPQUC9' + b'RLSBBHMEA99HCISABTHXDVBEZYQYWJLAFHAOGFBJKPJNVCEBGAYTYCONOWOCFZTQOF' + b'NUFWRTPWMEWPV9RGPOCGUDJGDGDJKKWTONTYXTM9GHQBPGOBYKTRGMBKRFUBSSGGYZ' + b'GIEKBNLUJKVFFSIVSVCL9FGLWZOGFUBZWWWQROZQEPUZDC9GUHJBBUDHATQDBIFKOP' + b'VNMQAFMTYWUXXJSL9XFRZCJDRNKWWUBYVCQLTATEL9VXIQUO9CQOKL9XFKEYQBGFSC' + b'GPVEVAMR9NMVHKIHAGVVXXAMNSFKI9ZEJXIFCMNPTJKDARSIFKIGUNVNADIGFHQEZM' + b'IBVFTEQQJPKZYFGYHVJOYJUCXSSBEXNQXIOEZVPOTSKRGRVMAFIRMH9LDTSQVOHO9N' + b'SUAMRAGYIVCUZMWUGJWTLISSHBXXRNTNTPGRGFTXEPBWGOFOVSZXTZLNZEKQQNRELZ' + b'XXJQMIZRLIKQDSQIUYBXTRNTUJJKIRRVCFWMJHOXQXRSRJMESICNXYW9QYEM9JNBBK' + b'CSFEKPQIABSMLMXROFOHUNBZBOHNQVOIMERBZMNKXTMUZWRYRFJISOGYEDTBYCDKOJ' + b'TAJGAWI9SSRQUPUDCDMYVFUKXSUHXROTHUSLDZGBACEANJAJMGLLKCKXSDZQPSWDHN' + b'HDIAEIYIODUBOSE9ALOJONOIDS9ZIKFAGWXJBJDSBJEJK9IYUFY9CPVAEVOVPVF9PF' + b'YHZAGYUGWMDDOWCUSISJEZM9BFFJTOPONR9XZKV9OLVXLPEQALITBGNMBJSGYORIH9' + b'MKTFYCYZD9XTKQ9RBCPDTGFJRGWDIWMSHDSSKGWJVUYWO9BCETIUHJUFYFDYADILCU' + b'CRSQYGBTWNAYEYIWBXLINOAUWGLSKBRIWGLPPX9FYWQHSSBJCNBXIYBQWBEIQAQCAO' + b'GSNXKYKDRNCIENDGHAGAQVETYLLQKZSYBWNCFWK99UEIDHXZFJLRKHJZXIO9ZFITBI' + b'BVFPXJHCEFTECVTXWDJTYFTNIUITGPJLBA9BTAJBYGCCDYXTXIWZXDFFHKJINAJCGX' + b'ZGLKEWRJBFOFWOD9OJEYSOOFGFWQHREKQCWLQWTSOFN9EMIBO9RWZLDYMXEUTPXZVQ' + b'KWPMJQTQDUPTFCGWYGJITQRYFZXWYWQ9VYILAU9RPPF9CGFIYNLJNNJLKVQGK9WJVN' + b'NQTGRIKPDFVEWUSKDZKYPUYXXYSKGEXI9DIDIJPGTRWYAKUQHLEVMJXBACKZNPRIQE' + b'PIHONHAKNNODBZZUGISKCFFAUBPLFLDNPDTIGYRSKLZJAIXWSPBWQBGKWRPOIJCCZR' + b'JLPXRDNSSHULCXCXIXATHFG9MWLZ9FKTORJHTZCX9BZWBBMPJOCGOVQWL9BEIOVSXJ' + b'TAPYQCOSOUTFNQU9RVTCOZNSALMPKRXRPKQDRVLPOPBWMCBQBVERXIIEPYGKUWODGP' + b'TZBGMVDVT' + ), + ) + + self.assertEqual( + next(generator), + + SigningKey( + b'BTQKIPUTHFDMCKGVLBCFGEFLJHCHOANY9GBIRSODCOWTJGUWGFMDXWEB9HDNP9M9ZU' + b'HVZQULQHEMXV9GMXDNYDHEJQZ9BSPIPHIVWNZDJWWDWONDKNVRFPZWNXZAVVTZCCMF' + b'DBOYJZCGKIFJDKLMTCGGFUJAZ9FPOIBDSHDZCUYNVXCCDAFZLF9IMKZWTZFUMZSVCT' + b'SFKUFVTZRLORSY9QNBYGEFLVITWYYJAIXGLNCMOEUJRVUHAKFFPRFGFLBXFAXJWKGE' + b'XZOGHLFWAPV9ISJMBPXCNXZDEQTLKMWVYFJBDKEZJVS9IX9RAKNEQCUQWEZZXKLXYF' + b'F9KBZZUOSVQ9FADRSBH9MQEOODDUMYJSZFQMUYNEYWWGSVBGCQLRDAJHFPCQEMKBG9' + b'DAKQZZCDGHOWXV9ZAZYQDGQAVTAUWNMKNBBJ9NGFBRWBWUEUJTHYF9AWSAYBRIOBRQ' + b'VSWAAYGGUGOEARZHSNBCFM9FMDICLOVEUDINON9YDPYNTKFBPGYSDEBYKYQCNDCCLT' + b'MKGYNKJFEHEUXENVNXVJUMI9IFHKMDPOXNPWQAUVJLHAWJMDFSZXIPJQPWHSJG9WDR' + b'R9IFTZXDHXSRKYISISAR9PBXL9R9UWRDLDL9MHKAOENNL9XXHJZWFGYRX9AJ9NIKLX' + b'9NHJRKEZCQCBZZJUREZHWLQY99OJ9MRYAWCVLL9PJDEMIMMPVSUHUD9ZIDDRETZBOV' + b'DOLHJDCRHCZBHDMUCXFBLJQQSHSBGWNGJOTRRXMRADROBTLBIDUBLBJESPZXKTKMUN' + b'9BDUTXPBZMZEFLEVOPIDMAGLLJEEOORHDTMAZBCLDKNOCGZLK9BOEWVAVEGMBBQK9E' + b'DKJQOQWHVVYRPEJFMBULFXROSVEZJPUTKOENCMGVPQWDZCVVJEFIAYSVQWLHFBEJRY' + b'TRRYL9FDTYRXJXGMCFPXBFXL9BJJTTBQXMRCZOSEPCCUX9XQKDGRQYHA9PRAGHYOLU' + b'TXSWWXWGEIHNW9JDZCQWVDQQSMQNAR99OYUFI9BHXZZLPFJXOFAULIJWZKCDTBYBYV' + b'PJYUHINJSODSHRLYZLV9BRLNUTBFRNMEQWIBJHLGYDNPAOCPBQVIPSDDDOACISQB9O' + b'QRRLKEHCHAGGDFUHEWUW9AHKHJIMMQHXYFNPEEBKLPFAMLGITBCHBAAWUAQXZXGRPM' + b'BKCRZNZQSBZTUYXXFEVDOKXXYYYLIUABRDAWYANEJZNOVUQJVNGYSLCYJKDIPUWK9F' + b'JVWHQAFLYMDYQBBFJRSQULZ9AAPVQAMHPNLQR9CULQYGTIMUWBTWKSWDETYDGORKVL' + b'AMTDFSVLKIZDQONCK9HXMYDREBYYWUUZFUJV9WSIM9USEEOZPLNSBBBOFTDRDFQTDJ' + b'QDTLOGRRRK9KBSXBCSJF9FXRELGUOND9OXVWQSVKWEUPPSSDFZIOWQUZRMDRJWJUZB' + b'9VUEGUQBX9JJKHDW9EABVQIZNTLK9TSZDLPINFYEOD9CCPGDPUPYMUUEPVXQRGHBBX' + b'ZYDDRGYKTPVXLNLDFNSMQTYGRHCZNYJQPZCOTGGYCAGGPBRUIVDHPBSGACFUMOZJGM' + b'JOVKWUOHNAZVSXBTZUKCHCRYQCFNUDDMN9GQOWDPPOVNAQOFAZVIEHZOYAJJBSIL9L' + b'BJIHVRELXSNHHWNPKPKAMRVZXH9GACFNXLLEAEBEGIGRGHTI9YONIRCMAXIVJUGRLW' + b'DWFDKBMPFPOAPAJ9ROCVVHHZOIREVFYSDXC9UKQIEHVSZJO9CTDPRADZBDWNUIWGRO' + b'FFRFYAMRNWLGSGZNYUJNEBNWJQNYFMKOKDXCJOB99QEHWZLCBXMIGOWROUFHJATSKY' + b'OYKMPVA9TXMM9Y9KOHUJZ9XRKAJ9TPTJYOWBNKXQFKLMWNVFZRXGBDSQRAGGFVSABR' + b'MDEQMHKWWEJMBLVBKVHHUFOVBSLMXAYTREGXECZTHOSDOQLFXOCCZTKHVLWBEYUIEA' + b'JBRSTMFHGQK9UCESUMUBFZGR9MQ9LIZFYNVXOTLJJERNSMUWMUWDCWGOGR9YLPO9UK' + b'QJWLOKEHIBGPGSD9CY9FQKUBVABDL9ODRPOZDWIEQBEKRNZDYWREIOOBU9XXLLDDFQ' + b'KFEIESAVXZIGTHLYZRTHLRSLKEEMTWFFLZFUVEJFEKZUEIZWBYADEKBUIQLJGTORCO' + b'SGXJHVGAB' + ), + ) + + def test_generator_with_offset(self): + """ + Creating a generator that starts at an offset greater than 0. + """ + ag = KeyGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999FFRFYAMRNWLGSGZNYUJNEBNWJQNYF', + ) + + generator = ag.create_generator(start=3, step=2) + + self.assertEqual( + next(generator), + + SigningKey( + b'CBKFYOTDYHFWSIOUYAUAHQWOOQDNNQBTSSJPHREUWFBXZFYFHPHZJN9ILAMZYOXLBQ' + b'MCN9T9F9VSCUSFVQDZROPWBKZ9ENENGFVDLMBNDOAAKZXVBXVAYJLSGVD9ZZSMDCRT' + b'JUUJPTNRVYYTOALRFOIGAXHUIXWCMXEUGTH9UJKYWTWBEOVWPMNVHJSXXKBAUPYCLS' + b'XATLJYLFNZ9NLRTPCCMAEFGLIONLXPTDCCAWCWNZNCUG9HKCRXPQRCUWBUDSIBZNCY' + b'VXSSHCKBBDSHJWZSUHQPVWJGYEEJCPYUDX99U9NBEJXKFIJDELKSLJOJ9ORMHA9FRU' + b'YQVGDKRCQZ9CKRNISKMRNWKJ9RGDNKFFESKBPQMIRDCYDQKRPYJZGMIENVBADUSDIX' + b'AECNYIBPXLJDKYCMAEZHF9JMIJZMFZCABOJZFAAGWKFEZVOCTUIEGWBWYDYUCGAQI9' + b'LRQOJVJUPATKDBKPWTMOYADYCCSARRPDMUXDNFZTXB9KLWCAINGFKENGUJPHVIPLLL' + b'CVTLESCANFQDBFOPKZTBRXDQYNTWDGMFHUIDJFEPBGMKWFRXOTBDVPSPFXWEMBXBEO' + b'LLGVWJYJ9HLIT9OQSAWWPFSQKQHJJXUKLVYSRQCLXIVUXWBUXAHV9SU9VOOUOSMVXC' + b'BQSMUVDN9WWOQXIGKTJPSEKFBLGLRQH9OEO9HFYDFDLZ9PIWCEYIINHHWXYZHESAWK' + b'XICDRDHDZMPDETWQNMPLGARJTMYTPMZT9BNZFNC9HMDCCNCKZPXOHZFLLCVPVARNTI' + b'QYAIGLC9SAHOKVELOXUVCVIFU9ZPVLNLSDTSKQJZKMYRXTD9STQEFDLYDQILBTISLV' + b'99EAWHJSNDBPPLKRPMPLYKPXEILA9XTVTKDAUYDVTYYJHWVMGRVEKGNMYDDWNLJPUU' + b'PBUNCZXTFIEWZDNGGSDLZZIOLM99TBHEFBJ9GTFDXNSMWFIYKXELWQYAQBLDNYSTTL' + b'YGLETFJJRTKRFVKRFNDTJVVQDEJTVWQWTKDIQOZTUHIAWJNSNCWNYHOCEZBITEMI9V' + b'NDCMPQ9KXQEUOTIHBJVTJHLSWRPOHQWKJDI9GCFOSABBKCDRHGV9UOHKEORUJVPFSU' + b'RKKXXXIWNEEEABGYNELGAKVUUPYMXTSSFT9Q9KCJTEAASIEJRHHJJIKJEUVM9A9JYZ' + b'YTZW9QZR9IASSDZGGBMOTZLOMCGEXPFUTDKXFLNJJRNMIURDSNRQEPHCVFCQONMCXX' + b'QIYEHOWB9WOCBZVSVKARB9MP9DQ9WAAZDLLZNAPLPAZEWDECSNLBKECVVWFXGZFQB9' + b'OUSIFYLSXQMVFCCLDJVXGHH9LGLNW9UZWVAYYAMGGOVGTNXWBGSQAIEAQNL9EJACRF' + b'QWHUVKDHKRLVAFWFX9HMHLKGWIYKOTVMRNIQTARCNMRCIXPYJJGOHVC9LBWCTFSCRH' + b'AEKUC9BJHXIVFFPGEEGBWZNCSRSOMSUOLNSUXUQLOPWZDTRXIHVZZLBCCKYNXSRIGH' + b'YEACLIEIANAYUT9JCLOGUPMRQRXPTOPKYBENALUUWFU9DXMGECFULSQKQFRRKLSAR9' + b'IUXGNYJZUIMBARYLYTSM9KQKGJQ9LOUVSURPPAILTNOFMKWTPNCXOWVYWNDDBXEESZ' + b'LEDTETJVMXOOHUZPCQRFNKWVDGOCTOTHBOMWWMOMAOEUIGVBRSMBBJPZASDARKOBFJ' + b'UKQSLUBQPHRXJNO9JYBNMHHTWCHYEICB9GGJIQQFTFPLOGVJGDXEZBN9XYZS9BKPXQ' + b'QHXEYE9HISINR9DHDFGKLSO9TCDRUAGCRFZIQVCPJWPQMZGUXXXBOYHXZLRWEIZJDR' + b'WYLQSPBTJQEBUBHVXKOCQUKOSIUTTAGWBBHTQTQRXOMVIALUWAEQMQXNYPAHJABDIW' + b'VGHPBGDFNUTOWQOFZQDYVDKDO9NZHFVLFMCXZGVEXYGIKJLGYOGXVXRIMBBCQOAF9H' + b'LISJHCTKSXTCQXCPLGSCZKQTE9CLWAIVPKGVMECLAAHFEHAXAZI9AZRCFVKX9SGPCL' + b'ALDSDAL9KXQLYDYEPTXQXSTNZLRLMXCSVSGJGNAHGKJLKMZDPXMHSOONBWIU9EGTIV' + b'9Q9DHAXLMSOEG9BXCNERYWTSHCEQBOM9USPXLKXTGQUDKYSITRTMIF9VPJGXGOEXSJ' + b'GBWSNGI9O' + ), + ) + + self.assertEqual( + next(generator), + + SigningKey( + b'OTOFWEAUQFRSWBTJPPAACBIPCTMYAESBAGVMPVMH9IQAEEKEXVUCSOWORDIQBRZZLD' + b'BXNAKQAQ9XMVRKMBQTBDFGJZEQBVRLEOUGHZNCJTSZQDDICMLCWZWF9FGAIFDNWUNV' + b'XMVVXKNCCMRD9NNAKEMILISEKDBYTLPEUXTIGFRWCXKNEOEWBZPHLKPVSNYCEZVNVL' + b'YZTZSCETSIPLBV9JMQDGZJVQHPVRDGFFAE9UYL99GIEXKPJXHXJFKHAPXBNN9QSNUC' + b'IHMHFIVNLBXM9VODQJWK9OQTUI9KIAMPHSOUGXDLJ9HDMCKWRORNKJV9YZYEGSXQZO' + b'VMICUMDOYJNRSYZQALQMLLUIKTEKOHYVZILBDERRROLRXYNIRWZYDOGYDKSCUJWDZB' + b'HHQUGZBCSRUCLOLDBHUNKASWQOT99Y9UMAZALVQPNQGYNKAEPIJJIXFVZJZATWXLYP' + b'JBQSFDCSOD9MUMYTFAIDDGMCFCY9GUGCDRXIGRDOMSTBOEKCKFBWGFTGJCMOTNSPJY' + b'UARTXPHERZIGQKRAHIMJCXMWPR9AA9UHXNKKHQSUFSFSTGMQTHDCYBNVLXEPHIJLUZ' + b'IECWNNI9BZQWQUMACUDXITELAKFQHUXOBCXKRE9J9EHGYCKGKB9RGGXVBALUDGPUOG' + b'XCSMVBDPOCGTXQZZSSYKBMYIJCKVAG99BBCZ9VAFQSZBICEEKAWNLUDUNDSPFVBWAH' + b'BMACURTREUIOYBJIFACYVPYXDWJXROXDKPGAYHNIFJVPDACYQXFVPCCIAGWK9WMKOS' + b'IQN9KBEOKIKVPAD9GJOR9KWI99NOSXSJUOLFPDNFNX9EBQ9QOFFPCSUA99UIDEHLG9' + b'PPUO9ZSZFTITOJTQYYDNAGJNMUUHKIS9QVYRRULFRKYYQYQNQEWOXHDUG9GKMAWIUJ' + b'JVCXGQOBVJYMTODRGSLTFFLWMXZFLWSYXTJBFVREXEAZHWIXEABFBLMREPGDHAQRNY' + b'AQ9ETQZOCVPP9QWKBGDOZVMMHVOZLSXOYVFNVSDYUDIQSMOJJOSHSU9HUMOGQGOJXP' + b'HPAQWUCDM9SQEZSFUTNGROJYTYNEAQMKVSAHAFIWCC9AAZIQSZLEBTIAEFCENIRXCK' + b'FEXPFUMQEJKZGCWQVDUPRAPLXCWNKUEWYVSYAYQSYSGLBZFX9NTRTSFCYLENLBPDDP' + b'JMRVW9RHNTLDMLFZXPYGBRUSEWJN9WTEOSFOFZLMHSWXINXLGNGFDJBWFKRDVYUUX9' + b'ZNLIRJZTKQVBWRJJQ9AAGSMCPBOINBGZSATBSUEIIBNUAMQQUTXCWXWMYQFZSSVNAM' + b'BACN9ISAHFKS9VPZCZX9CKAZGC9WYTBBFZQLPOFK9QHIMXFCXJULKFUYIOTTRPFNUK' + b'GEGUQMKDZREYWBLLPAVLAWXGFYXTXOVMEADBFESCSYHFJGHBVZEJHP9SDNOPMWHXKA' + b'DTOWPBNYNOPZGTTPZSWYXLTATCZHXZXMYMQBWPTGEQEGZM9NJTIFMDJBSCISTPKTWA' + b'APCFNTIDK9LMFGM9EQE9LYHF9POZEHUVSMIUOAGVKOJZDXUIQUGXGMOGJEWEBOXVFL' + b'JP9HMDMYOOMXGQYTPPWXDOCTDAXFALT9ESVOIIADLGAXOOHXVVEHCMVWOVLHOKMDTB' + b'ILNAZPFRCGTWJM9M9DL9YDKUGOXKJFHSOSLKZPZVGGDAHQZTODPPGAHMGZJISGVQMB' + b'ITZUALOLSCDQYOIWVKQOURQBAVEADUCIYIVQPKPUJLGZQJFU9VOK9DIVFMYIRNOBQF' + b'NNFIMND9PDKWDM9MKLEVXOQHC9CGOFOVAKHIZXDP9TIGMJCLHFGPWXKUDYWOGQMFFF' + b'SMRAMJQXWIXGOQTTA9UPIZIDRCWPMNVXHOKHWNQTTXHLQOZXFKTYVCYDZXAEMGVIHQ' + b'ODIIVSVGDNTSTXHS9DLFAEIGCLBCGLBMJ9HCSLIGKAGHGKMPCMXBWYRVBKSMNPEUIG' + b'HKZ9Y9ZNBSHWJFRGMYHMFLGHWJDZDMYZKTXLLGBVRYCYKBNHCJQBHAUSVCSVUJJAWV' + b'YYJ9KJJZHHYRROSFIARDIMYMDMSDUPZAHRRXWEGPH9KIPTYYJFVEYNMEZHOAEPMGBN' + b'SLTAJAVLVUOJNNXNPFQZMWDDFXORWRBPXGQSNAPNUMBSLFRUPK9AJVPWYPPBPIGQMP' + b'RONQOHEYR' + ), + ) + + def test_generator_with_iterations(self): + """ + Creating a generator that uses multiple iterations in order to + create longer keys. + """ + ag = KeyGenerator( + # Using the same seed as the previous test, just to make sure the + # key generator doesn't cheat. + seed = b'TESTSEED9DONTUSEINPRODUCTION99999FFRFYAMRNWLGSGZNYUJNEBNWJQNYF', + ) + + generator = ag.create_generator(start=3, iterations=2) + + self.assertEqual( + next(generator), + + SigningKey( + b'CBKFYOTDYHFWSIOUYAUAHQWOOQDNNQBTSSJPHREUWFBXZFYFHPHZJN9ILAMZYOXLBQ' + b'MCN9T9F9VSCUSFVQDZROPWBKZ9ENENGFVDLMBNDOAAKZXVBXVAYJLSGVD9ZZSMDCRT' + b'JUUJPTNRVYYTOALRFOIGAXHUIXWCMXEUGTH9UJKYWTWBEOVWPMNVHJSXXKBAUPYCLS' + b'XATLJYLFNZ9NLRTPCCMAEFGLIONLXPTDCCAWCWNZNCUG9HKCRXPQRCUWBUDSIBZNCY' + b'VXSSHCKBBDSHJWZSUHQPVWJGYEEJCPYUDX99U9NBEJXKFIJDELKSLJOJ9ORMHA9FRU' + b'YQVGDKRCQZ9CKRNISKMRNWKJ9RGDNKFFESKBPQMIRDCYDQKRPYJZGMIENVBADUSDIX' + b'AECNYIBPXLJDKYCMAEZHF9JMIJZMFZCABOJZFAAGWKFEZVOCTUIEGWBWYDYUCGAQI9' + b'LRQOJVJUPATKDBKPWTMOYADYCCSARRPDMUXDNFZTXB9KLWCAINGFKENGUJPHVIPLLL' + b'CVTLESCANFQDBFOPKZTBRXDQYNTWDGMFHUIDJFEPBGMKWFRXOTBDVPSPFXWEMBXBEO' + b'LLGVWJYJ9HLIT9OQSAWWPFSQKQHJJXUKLVYSRQCLXIVUXWBUXAHV9SU9VOOUOSMVXC' + b'BQSMUVDN9WWOQXIGKTJPSEKFBLGLRQH9OEO9HFYDFDLZ9PIWCEYIINHHWXYZHESAWK' + b'XICDRDHDZMPDETWQNMPLGARJTMYTPMZT9BNZFNC9HMDCCNCKZPXOHZFLLCVPVARNTI' + b'QYAIGLC9SAHOKVELOXUVCVIFU9ZPVLNLSDTSKQJZKMYRXTD9STQEFDLYDQILBTISLV' + b'99EAWHJSNDBPPLKRPMPLYKPXEILA9XTVTKDAUYDVTYYJHWVMGRVEKGNMYDDWNLJPUU' + b'PBUNCZXTFIEWZDNGGSDLZZIOLM99TBHEFBJ9GTFDXNSMWFIYKXELWQYAQBLDNYSTTL' + b'YGLETFJJRTKRFVKRFNDTJVVQDEJTVWQWTKDIQOZTUHIAWJNSNCWNYHOCEZBITEMI9V' + b'NDCMPQ9KXQEUOTIHBJVTJHLSWRPOHQWKJDI9GCFOSABBKCDRHGV9UOHKEORUJVPFSU' + b'RKKXXXIWNEEEABGYNELGAKVUUPYMXTSSFT9Q9KCJTEAASIEJRHHJJIKJEUVM9A9JYZ' + b'YTZW9QZR9IASSDZGGBMOTZLOMCGEXPFUTDKXFLNJJRNMIURDSNRQEPHCVFCQONMCXX' + b'QIYEHOWB9WOCBZVSVKARB9MP9DQ9WAAZDLLZNAPLPAZEWDECSNLBKECVVWFXGZFQB9' + b'OUSIFYLSXQMVFCCLDJVXGHH9LGLNW9UZWVAYYAMGGOVGTNXWBGSQAIEAQNL9EJACRF' + b'QWHUVKDHKRLVAFWFX9HMHLKGWIYKOTVMRNIQTARCNMRCIXPYJJGOHVC9LBWCTFSCRH' + b'AEKUC9BJHXIVFFPGEEGBWZNCSRSOMSUOLNSUXUQLOPWZDTRXIHVZZLBCCKYNXSRIGH' + b'YEACLIEIANAYUT9JCLOGUPMRQRXPTOPKYBENALUUWFU9DXMGECFULSQKQFRRKLSAR9' + b'IUXGNYJZUIMBARYLYTSM9KQKGJQ9LOUVSURPPAILTNOFMKWTPNCXOWVYWNDDBXEESZ' + b'LEDTETJVMXOOHUZPCQRFNKWVDGOCTOTHBOMWWMOMAOEUIGVBRSMBBJPZASDARKOBFJ' + b'UKQSLUBQPHRXJNO9JYBNMHHTWCHYEICB9GGJIQQFTFPLOGVJGDXEZBN9XYZS9BKPXQ' + b'QHXEYE9HISINR9DHDFGKLSO9TCDRUAGCRFZIQVCPJWPQMZGUXXXBOYHXZLRWEIZJDR' + b'WYLQSPBTJQEBUBHVXKOCQUKOSIUTTAGWBBHTQTQRXOMVIALUWAEQMQXNYPAHJABDIW' + b'VGHPBGDFNUTOWQOFZQDYVDKDO9NZHFVLFMCXZGVEXYGIKJLGYOGXVXRIMBBCQOAF9H' + b'LISJHCTKSXTCQXCPLGSCZKQTE9CLWAIVPKGVMECLAAHFEHAXAZI9AZRCFVKX9SGPCL' + b'ALDSDAL9KXQLYDYEPTXQXSTNZLRLMXCSVSGJGNAHGKJLKMZDPXMHSOONBWIU9EGTIV' + b'9Q9DHAXLMSOEG9BXCNERYWTSHCEQBOM9USPXLKXTGQUDKYSITRTMIF9VPJGXGOEXSJ' + b'GBWSNGI9OQGDVCIVNBM9FALTM9SNTBFSDWZCROFVSEUEDMTQZZJATYAOFPTHWQLSHJ' + b'WCQMIXRMICXGKOWT9VYKYTXXSIEVLMIHBREXFDIHPQOQHZYBURPHBDQSAWCNTDSADJ' + b'DYGVVUTVDWLXFWN9AENFDAJNQHHXZHCTLQSRMFS9MSJQQWAANCISFBDPXLERSETCQX' + b'TKXNVCIIOZWT9OLRVPGTKMLSMXYNJDPEVQFYAADMLXC9VXLTJTSVDBGFLVRLMYQAOR' + b'MJG9EYCQQENFCRH9JYTEWMMZTUASOOKXGYGDWKG9MCUNBGNEOYVXEOMAHMNNPDDJI9' + b'UKZOUVCMSEDSY9SOVEHHQ9ZCCWOXTXXGUYNLKMMBLRQCM9BBDZPUTPM9GBO9UTEEJA' + b'XSMQXAKFTCGPHQJQX9GMTZKSK9SIQOMBHSZRTPMPLKLOKKJQCCWVVHCHNCAHSQLNFF' + b'99A9WXKINJV9JHCAEFSQY9UIBVQPBUKWDAAEEZTPSFQNUXOYXHD9LYXWMBBABVNJYH' + b'DZMOJDYUBL9ZHILQDZJLKCYBBUHFOHETRMJURXODDPVKTWUUFEPANILESYJXOKBYAN' + b'OTUQSNOZUZQZFGGHKWHY9EAMUE9N9ZDMVGZKWQVXXTDOSOTBMJEMLQTMXUQPEV9HNO' + b'OHNI9MMTOWDJMCFUUNQEAPIOHFLLNTWGKPJGWWDJRJNFTUFLLHKXWCWXNZITFRLJRF' + b'PEUDOGNNTTSHTXCBXVFILDRRJVYTOPAGGSIPQDJGIMBZQ9XYCFYRMHOMPHRM9LTRMF' + b'MWESTEIJFCPGOFKPRTIHPUGRMOOR9FBCFAXPYSDZ9DIBSRLBZPMPOSQZKZXEDAXBKV' + b'I9IHSLIFTQEXIYMMIPZHYEKGNE9KJBXATVYCXLOX9FVXWWBVGPLCWYXJYMXSQPFYJT' + b'MSQZPSVMHFJTHHWRJLNGHLWPXRIEOQLLLFTOAIKAPLPNVDSHLWTHNKRPTWEKHQIKZL' + b'XCWQAKKLIPHYBJFPVNWWWGLDKTARWSIFRQBVJMYETVLUSRAPNLPCHACXXCGHLXCSVR' + b'BH9RTPQZFW9FNQAS9VGHAQBCGKUQKFG9OTYYTJAWXANALWFPAQWYEWQMILAPUICZUL' + b'ADQQUAQEKTVUIEECFCCGPYKIUCYJEIQFFHAKZWJZMTOFWQUZHINXNIHWYKGDLJ9AUE' + b'GKVQAXFTESQENO9TCSTQFATFBDCYCWHKPXNPJFR9ZAWCKZRKHPOMRQQVUHCLALSXFW' + b'QNKOVUPFRAOZNRQISOODHBBJPJOASUIFLLFLLRUDGPXY9XXHDIDSHZADBPBEWDGTXA' + b'PBQOXLRIX9VSINBJPKKIHKNIWDVQVEURNTLNMEGB9YETRAZKI9R9XNKKPNKEJIUP9G' + b'KPWEELKPQRJFOBHNZOQWNQXUTPPIEGWOYINXEFCVCXTU9BNSMQPWYHTZPSJOQZNAWR' + b'RS9TEQGZJIOOUWRTTNTCFWINUWQYXUCWXOYBCVLQXOVBCVLPEYKCIOXMNDPCFPPSCS' + b'UTQRDLXZDZIPQCLCOJQ9ASWVLVUVZWWIKQHQBYHEZYATKOZZKGYG9HIXNPAZINWBAP' + b'JNU99OLYNDFCNYQIAKYZDRFQEO9T9K9EWOIIXNUCLRVHWKOGXSXXKCRDLBLWGBCQFQ' + b'WIYHNCITMTCRFBHAGFLDFVQVLPRSBUXSKLY9DAZTKIXKVRVTSALFEOOFCTBTSBSCIL' + b'DVTFFJQRVFJBCJJAZGGPZB9DDF9XLIDDXQESOZKEZWILNVAYHSGCNPJRMATZTOBKJO' + b'FOUM9UUJXZUDDAAHBZSPW9LE9DEUGVJSCJG9EMFOOLPCNVGNFCR9IRGBBQVTADFBZA' + b'9GKQBJWLNOEEEUSOZLTCLITLKHAIWOQWZUWKIRNODXTGVDXJXOIUYAUNKLXRJLXSCE' + b'QKNPAJVVQCWFKWXSTWQMOTDVLSZVDJDBJUKQTQOIHTUTYMZABYEZJMPYZSKJAIXSYG' + b'DX9NXGJIYADQGVZCXMMIUGUY9CAU9YSNHLVFAIQU9MUPPHIKVTSDLEO9ADVTQLGSRD' + b'KXZGKH9YBNMGDIBEM9QSIOJEWGPNDLPJEXBMOUCED9VDNIWQXSPSAACPQNOGPWMVKQ' + b'FQGYYYXWZHTLYXQBIXHHIXOIRDPXWMUQDWIIRYNLZORRNAJOLLXU9WHJPTKLLWRFKW' + b'I9K9NDMQVNOWBHTZL9' + ), + ) From 733454ede86fba9b3072793fcf057980af8c2d33 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 11:56:29 -0500 Subject: [PATCH 143/239] PyCurl performance boost. - Runs 30-40% faster now (generating a signing key digest takes ~7 seconds, down from ~11). --- iota/crypto/pycurl.py | 62 +++++++++++++++++++++++++++++-------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index 2e6e57d..1e2a85a 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -5,10 +5,19 @@ from math import ceil from typing import List, MutableSequence, Optional, Sequence +from six import PY2 + __all__ = [ 'Curl', + 'HASH_LENGTH', ] +HASH_LENGTH = 243 +STATE_LENGTH = 3 * HASH_LENGTH + +NUMBER_OF_ROUNDS = 27 +TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] + class Curl(object): """ @@ -16,13 +25,6 @@ class Curl(object): **IMPORTANT: Not thread-safe!** """ - HASH_LENGTH = 243 - STATE_LENGTH = 3 * HASH_LENGTH - - NUMBER_OF_ROUNDS = 27 - - TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] - def __init__(self): # type: (Optional[Sequence[int]]) -> None self.reset() @@ -33,7 +35,7 @@ def reset(self): """ Resets internal state. """ - self._state = [0] * self.STATE_LENGTH # type: List[int] + self._state = [0] * STATE_LENGTH # type: List[int] def absorb(self, trits): # type: (Sequence[int], Optional[int]) -> None @@ -62,9 +64,9 @@ def _copy_and_transform(self, source, target, length): Copies trits from ``source`` to ``target`` one hash at a time, transforming in between hashes. """ - for i in range(int(ceil(length / self.HASH_LENGTH))): - start = i * self.HASH_LENGTH - stop = min(len(target), len(source), start + self.HASH_LENGTH) + for i in range(int(ceil(length / HASH_LENGTH))): + start = i * HASH_LENGTH + stop = min(len(target), len(source), start + HASH_LENGTH) target[start:stop] = source[start:stop] self._transform() @@ -74,19 +76,39 @@ def _transform(self): """ Transforms internal state. """ - index = 0 - - for _ in range(self.NUMBER_OF_ROUNDS): - temp_state = list(self._state) + # Copy some values locally so we can reduce the number of dot + # lookups we have to perform per list iteration. + # :see: https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Avoiding_dots... + state_length = STATE_LENGTH + truth_table = TRUTH_TABLE + + # Optimization for Python 2 + if PY2: + # noinspection PyUnresolvedReferences + range_ = xrange + else: + range_ = range + + # :see: http://stackoverflow.com/a/2612990/ + prev_state = self._state[:] + new_state = prev_state[:] - for pos in range(self.STATE_LENGTH): + index = 0 + for _ in range_(NUMBER_OF_ROUNDS): + # noinspection PyUnusedLocal + for pos in range_(state_length): prev_index = index index += (364 if index < 365 else -365) - self._state[pos] = ( - self.TRUTH_TABLE[ - temp_state[prev_index] - + (3 * temp_state[index]) + new_state[pos] = ( + truth_table[ + prev_state[prev_index] + + (3 * prev_state[index]) + 4 ] ) + + prev_state = new_state + new_state = new_state[:] + + self._state = prev_state From 6777e93402b05fb02d3dc6eaec14792d158014d1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 12:23:33 -0500 Subject: [PATCH 144/239] Improved documentation. --- iota/crypto/pycurl.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index 1e2a85a..209bc9a 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -76,9 +76,8 @@ def _transform(self): """ Transforms internal state. """ - # Copy some values locally so we can reduce the number of dot - # lookups we have to perform per list iteration. - # :see: https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Avoiding_dots... + # Copy some values locally so we can avoid global lookups. + # :see: https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Local_Variables state_length = STATE_LENGTH truth_table = TRUTH_TABLE @@ -89,6 +88,9 @@ def _transform(self): else: range_ = range + # Operate on a copy of ``self._state`` to avoid the number of dot + # lookups that we perform in the inner loop. + # :see: https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Avoiding_dots... # :see: http://stackoverflow.com/a/2612990/ prev_state = self._state[:] new_state = prev_state[:] From 20b94edb185772a306c45d03cf1d71ab05c54529 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 12:27:38 -0500 Subject: [PATCH 145/239] Minor cleanup. --- iota/crypto/pycurl.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index 209bc9a..e2b0057 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -97,7 +97,6 @@ def _transform(self): index = 0 for _ in range_(NUMBER_OF_ROUNDS): - # noinspection PyUnusedLocal for pos in range_(state_length): prev_index = index index += (364 if index < 365 else -365) @@ -113,4 +112,4 @@ def _transform(self): prev_state = new_state new_state = new_state[:] - self._state = prev_state + self._state = new_state From bfc2b3accffa70e96c56bc2b481afecfedd6a17b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 14:43:09 -0500 Subject: [PATCH 146/239] Implemented digest generation. --- iota/crypto/pycurl.py | 40 +++++++++++++-------- iota/crypto/signing.py | 70 ++++++++++++++++++++++++++++++++++--- test/crypto/signing_test.py | 54 ++++++++++++++++++++++++++++ 3 files changed, 144 insertions(+), 20 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index e2b0057..d000d40 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -44,9 +44,23 @@ def absorb(self, trits): :param trits: Sequence of trits to absorb. - Note: Only the first 729 trits will be absorbed. """ - self._copy_and_transform(trits, self._state, len(trits)) + length = len(trits) + offset = 0 + + while offset < length: + start = offset + stop = min(start + HASH_LENGTH, length) + + # Copy the next hash worth of trits to internal state. + self._state[0:stop-start] = trits[start:stop] + + # Transform + self._transform() + + # Move on to the next hash. + offset += HASH_LENGTH + def squeeze(self, trits): # type: (MutableSequence[int]) -> None @@ -57,19 +71,15 @@ def squeeze(self, trits): Sequence that the squeezed trits will be copied to. Note: this object will be modified! """ - self._copy_and_transform(self._state, trits, len(trits)) - - def _copy_and_transform(self, source, target, length): - """ - Copies trits from ``source`` to ``target`` one hash at a time, - transforming in between hashes. - """ - for i in range(int(ceil(length / HASH_LENGTH))): - start = i * HASH_LENGTH - stop = min(len(target), len(source), start + HASH_LENGTH) - - target[start:stop] = source[start:stop] - self._transform() + # Squeeze is kind of like the opposite of absorb; it copies trits + # from internal state to the ``trits`` parameter. + # However, internal state is always exactly 1 hash in length, so + # the implementation can be simplified somewhat. + + # Note that we copy at most len(trits) trits! + length = min(HASH_LENGTH, len(trits)) + trits[0:length] = self._state[0:length] + self._transform() def _transform(self): # type: () -> None diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 35a7d8e..91b19ef 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -2,10 +2,10 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, List, MutableSequence, Optional, Union +from typing import Generator, List, MutableSequence, Optional from iota import TryteString, TrytesCompatible -from iota.crypto import Curl +from iota.crypto import Curl, HASH_LENGTH __all__ = [ 'KeyGenerator', @@ -36,7 +36,7 @@ class SigningKey(TryteString): A TryteString that acts as a signing key, e.g., for generating message signatures, new addresses, etc. """ - LEN_MULTIPLE = 2187 + BLOCK_LEN = 2187 """ Similar to RSA keys, SigningKeys must have a length that is divisible by a certain number of trytes. @@ -46,14 +46,74 @@ def __init__(self, trytes): # type: (TrytesCompatible) -> None super(SigningKey, self).__init__(trytes) - if len(self._trytes) % self.LEN_MULTIPLE: + if len(self._trytes) % self.BLOCK_LEN: raise ValueError( 'Length of {cls} values must be a multiple of {len} trytes.'.format( cls = type(self).__name__, - len = self.LEN_MULTIPLE + len = self.BLOCK_LEN ), ) + @property + def block_count(self): + # type: () -> int + """ + Returns the length of this key, expressed in blocks. + """ + return len(self) // self.BLOCK_LEN + + def get_digest_trits(self): + # type: () -> List[int] + """ + Generates the digest used to do the actual signing. + + Signing keys can have variable length and tend to be quite long, + which makes them not-well-suited for use in crypto algorithms. + + The digest is essentially the result of running the signing key + through a PBKDF, yielding a constant-length hash that can be used + for crypto. + """ + # Multiply by 3 to convert trytes into trits. + block_size = self.BLOCK_LEN * 3 + raw_trits = self.as_trits() + + # Initialize list with the correct length to improve performance. + digest = [0] * HASH_LENGTH # type: List[int] + + for i in range(self.block_count): + block_start = i * block_size + block_end = block_start + block_size + + block_trits = raw_trits[block_start:block_end] + + # Initialize ``key_fragment`` with the correct length to + # improve performance. + key_fragment = [0] * block_size # type: List[int] + + buffer = [] # type: List[int] + + for j in range(27): + hash_start = j * HASH_LENGTH + hash_end = hash_start + HASH_LENGTH + + buffer = block_trits[hash_start:hash_end] + + for k in range(26): + sponge = Curl() + sponge.absorb(buffer) + sponge.squeeze(buffer) + + key_fragment[hash_start:hash_end] = buffer + + sponge = Curl() + sponge.absorb(key_fragment) + sponge.squeeze(buffer) + + digest[block_start:block_end] = buffer + + return digest + class KeyGenerator(object): """ diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 760a277..21e71a4 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -4,6 +4,7 @@ from unittest import TestCase +from iota import TryteString from iota.crypto.signing import KeyGenerator, SigningKey @@ -615,3 +616,56 @@ def test_generator_with_iterations(self): b'I9K9NDMQVNOWBHTZL9' ), ) + + +# noinspection SpellCheckingInspection +class SigningKeyTestCase(TestCase): + def test_get_digest_trits(self): + """ + Generating digest trits from a valid SigningKey. + """ + key = SigningKey( + b'BWFTZWBZVFOSQYHQFXOPYTZ9SWB9RYYHBOUA9NOYSWGALF9MSVNEDW9A9FLGBRWKED' + b'MPEIPRKBMRXRLLFJCAGVIMXPISRGXIJQ9BOBHKJEUKDEUUWYXJGCGAWHYBQHBPMRTZ' + b'FPBGNLMKPZYXZPXFSPFUWZNRWYXUEWMP9URKVVJOSWEPJKSMPLWZPIZGOTVAA9QQOC' + b'YISMGHSBU9YCXZCMSTPJVASDKEVZCSPNSPYOUUWWFTNWZTTZBKGZ9PDNAKNSGNODSB' + b'IRKUGFYCZXIFHQCDTXQNLMKRVKIFJS9XARBNMJQOTDL9CAOKEXQTMWCKWRNHLLMLYP' + b'QGTDFNTDBHNAFRBEUWTKPKPECAADKRPEAFDHABMYYXQPQYDSGFRSRFNHFHHHTAH9YF' + b'OXKRZOTKAHZPRISHZRR9YBVSOZUSKKU9HTCXPTPZFAHFMOQJBKZIACZB9ZRXFPPPMY' + b'RBCWPBAPRFXLQZOTGXJGMZUUEZIAVWXUEN9UIFLEESVCCGNKDISMEPYWTXDQHOSUWZ' + b'OEHOCZQJKDCJJNRZVODVNNUOV9FZQEXFGAMDKMV9PVUYMWTFNISYGYKQG9OKNOQUEK' + b'YDEJ9EGHUXQFCPHTTVBCRTZJLOAWHGDEQHLPLHWTWVOBCCQTCWCNLYDGUV9FKFZENU' + b'NOCOYNU9CYQDSAQDSMZGRQYB9YOCFSOHQXANMSPYFVCTPTZKUGIGUZMPJIJRZVN9VX' + b'ADLIVJYJGQWXBOBBAROGNIOIJVRUHWMFLVCGTMZISADLTXVEZLQDYAVQ9OCYQDPCYL' + b'DD9SUPNTUMUESC9VRSCAYBPXAPTYXZODUUMBNCSLOWJYJA9JBITZLZNHPZFGSPRURJ' + b'HYFLSFTMEPAEG9DWRFOTFWWPGGDGZWFVEPOHDGNMOXUSR9AVQLNDUMGPYWVN9LKEIZ' + b'Q9MNIUPJXPTRYMSXRA9GTSZFMNZZT9Z9HJOKVBCHRRIZZN9UYTVRDNHOXYSFRO9FRK' + b'HWNNZ9DXTLV9D9PNJLGWJXAFUOJZTRVJOLYGSPNVCXYWMTOEEUBLNRGAJPK9HIWGZM' + b'HMBTHLABTACRYLQIDPOYEFNSYQ9YAQPOYYDCJAAAVDUWCHSS9OKQYH9CQNUPYRTCWK' + b'CXYDKTAIJKWOQIHSGBZMFJXGOQDODBDZNBOPFYCBLSU9RJYVGXINUDODGHNAGFDAEG' + b'LPDVSCJPCIZHOFNHCZUTRLQXEZUFDZVROFXVHUNWMYRSZHBZAFCWIY9ULTBTEDSKEC' + b'CLDGAU9MZXYRAXVY9NIQUYHATJCZXDSAELMCXQMALHNFMEAWHIQAZMQOO9QPEPDOYC' + b'OJXTUWEJHMPXGZBXFPNXUPOSDINZJNIREYDFZMESFQUPBKSDGTAJHEZSCOVSLYUAUK' + b'DIWNNLQJTPYYTPRGGN9IXIORWXHJBPYINQZIUXKLKXCTQZYJIRH9MHYBQIFQZCFAKZ' + b'DUUZTYIMTNNVNKVMFIW9UYRTVRQHMYR9Y9VYFTPBJGSB9VINGTMBKVZJEZUE9RMBDZ' + b'CQGDHNPW9YIJHLGFOG9YAPXZECSFVXAMPILBIHC9DBGMIE99YEPGTAALOHBUKXSGFZ' + b'YHHWFOIHMEDXFHIYSUHADOCKFNGHKTNPZHINAWG9YGRJBQGECRCVPXXOG9CNVJNFLC' + b'LMGC9I9HAGTAGGVRKCXDJWDNHYZBFNQSKH99MFAMLGRSBMIBBMHBDJTVSQ99ZHYPSS' + b'XLUNFCNOJXITUETNBHIGXFLLEHUKEXGJLO9BBALXMNGJKTETFIZHSSKLOQXPXOSZRW' + b'QP9A9RIHWEHATMSVMZEQPGAUQBCIAQXZSUUFSU9HYK9RAVASYCVNALKJJXAJF9RLTD' + b'ZEIIYCFLQVMHBPBFHHQNXVEKPHOOFTQEIVB9IXZMTOFBHTGLPWDYVPO9HHBPVWZYEG' + b'IDMK9UPWEJDLIPJSIGFKKCZFRJVDN9ENWADNOTFWZGUDJRRUMFPVXHNAJBBCI9WEDK' + b'RKCQUHRQTCFYFHXPOFBC9BCENMI9HRSIUAKLWEAOUXRBWMHWLGEOCP9NWIJAXODJDS' + b'P9SKEXEDVUGHZAFPNMR9PXD9THOWNWWTDTWTYMDINGC9EBBVUYZRUQDVSIOXAEVGFP' + b'XS9CLTHUESMTDWUJNCZSOIOEJG9WKNAZMDJGMRBXGVMLUAN9IGDVFAESJMXNTMNFND' + b'CAXEBRAU9' + ) + + self.assertEqual( + TryteString.from_trits(key.get_digest_trits()), + + TryteString( + b'ABQXVJNER9MPMXMBPNMFBMDGTXRWSYHNZKGAGUOI' + b'JKOJGZVGHCUXXGFZEMMGDSGWDCKJXO9ILLFAKGGZE' + ), + ) From 2aad469ded2c6721fb770fdb18b35b4ce2d20cf6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 14:45:37 -0500 Subject: [PATCH 147/239] Cleaned up imports, improved documentation. --- iota/crypto/pycurl.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index d000d40..6fe4be9 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from math import ceil from typing import List, MutableSequence, Optional, Sequence from six import PY2 @@ -55,7 +54,7 @@ def absorb(self, trits): # Copy the next hash worth of trits to internal state. self._state[0:stop-start] = trits[start:stop] - # Transform + # Transform. self._transform() # Move on to the next hash. @@ -72,13 +71,16 @@ def squeeze(self, trits): Note: this object will be modified! """ # Squeeze is kind of like the opposite of absorb; it copies trits - # from internal state to the ``trits`` parameter. + # from internal state to the ``trits`` parameter, one hash at a + # time, and transforming internal state in between hashes. # However, internal state is always exactly 1 hash in length, so # the implementation can be simplified somewhat. # Note that we copy at most len(trits) trits! length = min(HASH_LENGTH, len(trits)) trits[0:length] = self._state[0:length] + + # One hash worth of trits copied; now transform. self._transform() def _transform(self): From 3a9be9dcd806a12bd38b8e6b865282d574d5f8ab Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 14:47:57 -0500 Subject: [PATCH 148/239] Added more documentation. PyCurl is deceptively complex! --- iota/crypto/pycurl.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index 6fe4be9..f6fb038 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -47,6 +47,8 @@ def absorb(self, trits): length = len(trits) offset = 0 + # Copy trits from ``trits`` into internal state, one hash at a + # time, transforming internal state in between hashes. while offset < length: start = offset stop = min(start + HASH_LENGTH, length) @@ -60,7 +62,6 @@ def absorb(self, trits): # Move on to the next hash. offset += HASH_LENGTH - def squeeze(self, trits): # type: (MutableSequence[int]) -> None """ From b076fcb777afaf20f3979b6262a5ecc61f85339d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 14:53:37 -0500 Subject: [PATCH 149/239] A little more documentation. No, really! PyCurl is complicated! --- iota/crypto/pycurl.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index f6fb038..c81f6b2 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -94,11 +94,15 @@ def _transform(self): state_length = STATE_LENGTH truth_table = TRUTH_TABLE - # Optimization for Python 2 + # Optimization: Ensure that we use a generator to create ranges. if PY2: + # In Python 2, ``range`` returns a list, while ``xrange`` returns + # a generator. # noinspection PyUnresolvedReferences range_ = xrange else: + # In Python 3, ``range`` returns a generator, and ``xrange`` is + # baleeted. range_ = range # Operate on a copy of ``self._state`` to avoid the number of dot From df29f26d100b0d16e5d32559ac2e952194d8f3b9 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 15:52:02 -0500 Subject: [PATCH 150/239] Implemented address generator. --- iota/crypto/addresses.py | 143 ++++++++++++++++++++++++++++++++++ test/crypto/addresses_test.py | 56 +++++++++++++ 2 files changed, 199 insertions(+) create mode 100644 iota/crypto/addresses.py create mode 100644 test/crypto/addresses_test.py diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py new file mode 100644 index 0000000..6a4e903 --- /dev/null +++ b/iota/crypto/addresses.py @@ -0,0 +1,143 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import Generator, Iterable, List, MutableSequence, Optional, Union + +from iota import Address, TryteString, TrytesCompatible +from iota.crypto import Curl +from iota.crypto.signing import KeyGenerator, SigningKey + +__all__ = [ + 'AddressGenerator', +] + + +class AddressGenerator(Iterable[Address]): + """ + Generates new addresses using a standard algorithm. + + Note: This class does not check if addresses have already been used; + if you want to exclude used addresses, invoke + :py:meth:`iota.api.IotaApi.get_new_addresses` instead. + + Note also that :py:meth:`iota.api.IotaApi.get_new_addresses` uses + ``AddressGenerator`` internally, so you get the best of both worlds + when you use the API (: + """ + DIGEST_ITERATIONS = 2 + + def __init__(self, seed): + # type: (TrytesCompatible) -> None + super(AddressGenerator, self).__init__() + + self.seed = TryteString(seed) + + def __getitem__(self, slice_): + # type: (Union[int, slice]) -> Union[Address, List[Address]] + """ + Generates and returns one or more addresses at the specified + index(es). + + :param slice_: + Index of address to generate, or a slice. + + Warning: This method may take awhile to run if the requested + index(es) is a large number! + + :return: + Behavior matches slicing behavior of other collections: + + - If an int is provided, a single address will be returned. + - If a slice is provided, a list of addresses will be returned. + """ + return ( + self.get_addresses(slice_.start, slice_.stop, slice_.step) + if isinstance(slice_, slice) + else self.get_addresses(slice_)[0] + ) + + def __iter__(self): + # type: () -> Generator[Address] + """ + Returns a generator for creating new addresses, starting at index + 0 and potentially continuing on forever. + """ + return self.create_generator(0) + + def get_addresses(self, start, stop=None, step=1): + # type: (int, Optional[int], int) -> List[Address] + """ + Generates and returns one or more addresses at the specified + index(es). + + This is a one-time operation; if you want to create lots of + addresses across multiple contexts, consider invoking + :py:meth:`create_generator` and sharing the resulting generator + object instead. + + Warning: This method may take awhile to run if the starting index + and/or the number of requested addresses is a large number! + + :param start: + Starting index. + + :param stop: + Stop before this index. This value must be positive. + If ``None`` (default), only generate a single address. + + :param step: + Number of indexes to advance after each address. + + :return: + Always returns a list, even if only one address is generated. + """ + generator = self.create_generator(start, step) + interval = range(start, start+1 if stop is None else stop, step) + + addresses = [] + for _ in interval: + try: + next_key = next(generator) + except StopIteration: + break + else: + addresses.append(next_key) + + return addresses + + def create_generator(self, start, step=1): + # type: (int, int) -> Generator[Address] + """ + Creates a generator that can be used to progressively generate new + addresses. + + :param start: + Starting index. + + Warning: This method may take awhile to reset if ``start`` + is a large number! + + :param step: + Number of indexes to advance after each address. + + Warning: The generator may take awhile to advance between + iterations if ``step`` is a large number! + """ + key_generator = ( + KeyGenerator(self.seed) + .create_generator(start, step, iterations=self.DIGEST_ITERATIONS) + ) + + while True: + signing_key = next(key_generator) # type: SigningKey + digest = signing_key.get_digest_trits() + + # Multiply by 3 to convert from trits to trytes. + address_trits = [0] * (Address.LEN * 3) # type: MutableSequence[int] + + sponge = Curl() + sponge.absorb(digest) + sponge.squeeze(address_trits) + + yield Address.from_trits(address_trits) diff --git a/test/crypto/addresses_test.py b/test/crypto/addresses_test.py new file mode 100644 index 0000000..bb063ad --- /dev/null +++ b/test/crypto/addresses_test.py @@ -0,0 +1,56 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota import Address +from iota.crypto.addresses import AddressGenerator + + +# noinspection SpellCheckingInspection +class AddressGeneratorTestCase(TestCase): + def test_generate_single_address(self): + """ + Generating a single address. + """ + ag = AddressGenerator( + seed = b'ITJVZTRFNBTRBSDIHWKOWCFBOQYQTENWLRUVHIBCBRTXYGDCCLLMM9DI9OQO', + ) + + self.assertListEqual( + ag.get_addresses(0), + + [ + Address( + b'QIMWUZUX9RBGQZQDMAVBHLMUP9SHRUDYACNRRRBX' + b'QWRLTYFHDLQKYVHLLGBLSSKACNRIQJVX99OUFNUFE' + ), + ], + ) + + self.assertListEqual( + ag.get_addresses(1), + + [ + Address( + b'IGNEVQHYMVIMZB9XZAFQT9EMDGQJZQUKIVWGOESQ' + b'DFDOEG9YQUPPD9MSKGDLP9QIHKGOSQZ9PTPEAZGNK' + ), + ], + ) + + # You can request an address at any arbitrary index, and the result + # will always be consistent (assuming the seed doesn't change). + # Note: this can be a slow process, so we'll keep the numbers small + # so that tests don't take too long. + self.assertListEqual( + ag.get_addresses(13), + + [ + Address( + b'VYJG9FMDTLHJXWXSEMIXJGMNCXWVNKQVBXUCWYLF' + b'FYBBWUXNTECCHZQRA9WWHOKTYVRZTQAVFAKBQRXPJ' + ), + ], + ) From a49593364a33c0ebf4beb3c0880987c8b4dd8dd6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 18:10:17 -0500 Subject: [PATCH 151/239] Tightened up signing/addy code. - Got rid of slice notation; it's confusing and too easy to provide invalid parameters. - Replaced ``stop`` argument with ``count`` in ``get_{addresses,keys}``. - Added more unit tests. - Cleaned up messy code. --- iota/crypto/addresses.py | 89 ++++++++++--------- iota/crypto/signing.py | 30 +++++-- test/crypto/addresses_test.py | 149 ++++++++++++++++++++++++++++++- test/crypto/signing_test.py | 159 ++++++++++++++++++++++++++++++---- 4 files changed, 356 insertions(+), 71 deletions(-) diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 6a4e903..1d1d422 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, Iterable, List, MutableSequence, Optional, Union +from typing import Generator, Iterable, List, MutableSequence from iota import Address, TryteString, TrytesCompatible from iota.crypto import Curl @@ -33,40 +33,16 @@ def __init__(self, seed): self.seed = TryteString(seed) - def __getitem__(self, slice_): - # type: (Union[int, slice]) -> Union[Address, List[Address]] - """ - Generates and returns one or more addresses at the specified - index(es). - - :param slice_: - Index of address to generate, or a slice. - - Warning: This method may take awhile to run if the requested - index(es) is a large number! - - :return: - Behavior matches slicing behavior of other collections: - - - If an int is provided, a single address will be returned. - - If a slice is provided, a list of addresses will be returned. - """ - return ( - self.get_addresses(slice_.start, slice_.stop, slice_.step) - if isinstance(slice_, slice) - else self.get_addresses(slice_)[0] - ) - def __iter__(self): # type: () -> Generator[Address] """ Returns a generator for creating new addresses, starting at index 0 and potentially continuing on forever. """ - return self.create_generator(0) + return self.create_generator() - def get_addresses(self, start, stop=None, step=1): - # type: (int, Optional[int], int) -> List[Address] + def get_addresses(self, start, count=1, step=1): + # type: (int, int, int) -> List[Address] """ Generates and returns one or more addresses at the specified index(es). @@ -81,22 +57,33 @@ def get_addresses(self, start, stop=None, step=1): :param start: Starting index. + Must be >= 0. - :param stop: - Stop before this index. This value must be positive. - If ``None`` (default), only generate a single address. + :param count: + Number of addresses to generate. + Must be > 0. :param step: Number of indexes to advance after each address. + This may be any non-zero (positive or negative) integer. :return: Always returns a list, even if only one address is generated. + + The returned list will contain ``count`` addresses, except when + ``step * count < start`` (only applies when ``step`` is + negative). """ + if count < 1: + raise ValueError('``count`` must be positive.') + + if not step: + raise ValueError('``step`` must not be zero.') + generator = self.create_generator(start, step) - interval = range(start, start+1 if stop is None else stop, step) addresses = [] - for _ in interval: + for _ in range(count): try: next_key = next(generator) except StopIteration: @@ -106,7 +93,7 @@ def get_addresses(self, start, stop=None, step=1): return addresses - def create_generator(self, start, step=1): + def create_generator(self, start=0, step=1): # type: (int, int) -> Generator[Address] """ Creates a generator that can be used to progressively generate new @@ -124,14 +111,15 @@ def create_generator(self, start, step=1): Warning: The generator may take awhile to advance between iterations if ``step`` is a large number! """ - key_generator = ( - KeyGenerator(self.seed) - .create_generator(start, step, iterations=self.DIGEST_ITERATIONS) - ) + if start < 0: + raise ValueError('``start`` cannot be negative.') - while True: - signing_key = next(key_generator) # type: SigningKey - digest = signing_key.get_digest_trits() + digest_generator = self._create_digest_generator(start, step) + + current = start + + while current >= 0: + digest = next(digest_generator) # type: List[int] # Multiply by 3 to convert from trits to trytes. address_trits = [0] * (Address.LEN * 3) # type: MutableSequence[int] @@ -141,3 +129,22 @@ def create_generator(self, start, step=1): sponge.squeeze(address_trits) yield Address.from_trits(address_trits) + + current += step + + def _create_digest_generator(self, start, step): + # type: (int, int) -> Generator[SigningKey] + """ + Initializes a generator to create SigningKey digests. + + Implemented as a separate method so that it can be mocked during + unit tests. + """ + key_generator = ( + KeyGenerator(self.seed) + .create_generator(start, step, iterations=self.DIGEST_ITERATIONS) + ) + + while True: + signing_key = next(key_generator) # type: SigningKey + yield signing_key.get_digest_trits() diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 91b19ef..b112f3b 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -2,7 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, List, MutableSequence, Optional +from typing import Generator, List, MutableSequence from iota import TryteString, TrytesCompatible from iota.crypto import Curl, HASH_LENGTH @@ -125,8 +125,8 @@ def __init__(self, seed): self.seed = Seed(seed).as_trits() - def get_keys(self, start, stop=None, step=1, iterations=1): - # type: (int, Optional[int], int, int) -> List[SigningKey] + def get_keys(self, start, count=1, step=1, iterations=1): + # type: (int, int, int, int) -> List[SigningKey] """ Generates and returns one or more keys at the specified index(es). @@ -140,13 +140,15 @@ def get_keys(self, start, stop=None, step=1, iterations=1): :param start: Starting index. + Must be >= 0. - :param stop: - Stop before this index. - If ``None``, only generate a single key. + :param count: + Number of keys to generate. + Must be > 0. :param step: Number of indexes to advance after each key. + This may be any non-zero (positive or negative) integer. :param iterations: Number of _transform iterations to apply to each key. @@ -157,12 +159,21 @@ def get_keys(self, start, stop=None, step=1, iterations=1): :return: Always returns a list, even if only one key is generated. + + The returned list will contain ``count`` keys, except when + ``step * count < start`` (only applies when ``step`` is + negative). """ + if count < 1: + raise ValueError('``count`` must be positive.') + + if not step: + raise ValueError('``step`` must not be zero.') + generator = self.create_generator(start, step, iterations) - interval = range(start, start+1 if stop is None else stop, step) keys = [] - for _ in interval: + for _ in range(count): try: next_key = next(generator) except StopIteration: @@ -200,6 +211,9 @@ def create_generator(self, start=0, step=1, iterations=1): Increasing this value makes key generation slower, but more resistant to brute-forcing. """ + if start < 0: + raise ValueError('``start`` cannot be negative.') + current = start while current >= 0: diff --git a/test/crypto/addresses_test.py b/test/crypto/addresses_test.py index bb063ad..c4a1575 100644 --- a/test/crypto/addresses_test.py +++ b/test/crypto/addresses_test.py @@ -10,7 +10,7 @@ # noinspection SpellCheckingInspection class AddressGeneratorTestCase(TestCase): - def test_generate_single_address(self): + def test_get_addresses_single(self): """ Generating a single address. """ @@ -19,7 +19,7 @@ def test_generate_single_address(self): ) self.assertListEqual( - ag.get_addresses(0), + ag.get_addresses(start=0), [ Address( @@ -30,7 +30,7 @@ def test_generate_single_address(self): ) self.assertListEqual( - ag.get_addresses(1), + ag.get_addresses(start=1), [ Address( @@ -45,7 +45,7 @@ def test_generate_single_address(self): # Note: this can be a slow process, so we'll keep the numbers small # so that tests don't take too long. self.assertListEqual( - ag.get_addresses(13), + ag.get_addresses(start=13), [ Address( @@ -54,3 +54,144 @@ def test_generate_single_address(self): ), ], ) + + def test_get_addresses_multiple(self): + """ + Generating multiple addresses in one go. + """ + ag = AddressGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999TPXGCGPRTMI9QQNCW9PKWTAAOPYHU', + ) + + self.assertListEqual( + ag.get_addresses(start=1, count=2), + + [ + Address( + b'AZNEMINAIYSTVTTLPJOXSECRCYJUNUSHDPKKHLJA' + b'SHU9DAYROZP99ZHMCERMKHGALVUJQPGGK9TDMBHMB' + ), + + Address( + b'LOBJATMDBWOZWXZESPPCGFPCHKGAVWRYREQLCKAC' + b'DOUDVDFVFJMIAOZZQHYCCQUOXWZOGEIL9HYFHDQ9S' + ), + ], + ) + + def test_get_addresses_error_start_too_small(self): + """ + Providing a negative ``start`` value to ``get_addresses``. + + :py:class:`AddressGenerator` can potentially generate an infinite + number of addresses, so there is no "end" to offset against. + """ + ag = AddressGenerator(seed=b'') + + with self.assertRaises(ValueError): + ag.get_addresses(start=-1) + + def test_get_addresses_error_count_too_small(self): + """ + Providing a ``count`` value less than 1 to ``get_addresses``. + + :py:class:`AddressGenerator` can potentially generate an infinite + number of addresses, so there is no "end" to offset against. + """ + ag = AddressGenerator(seed=b'') + + with self.assertRaises(ValueError): + ag.get_addresses(start=0, count=0) + + def test_get_addresses_error_step_zero(self): + """ + Providing a ``step`` value of 0 to ``get_addresses``. + """ + ag = AddressGenerator(seed=b'') + + with self.assertRaises(ValueError): + ag.get_addresses(start=0, step=0) + + def test_get_addresses_step_negative(self): + """ + Providing a negative ``step`` value to ``get_addresses``. + + This is probably a weird use case, but what the heck. + """ + ag = AddressGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999GIDXIZGHXKLOMIQVQRFSRUGYDWZA9', + ) + + self.assertListEqual( + ag.get_addresses(start=1, count=2, step=-1), + + # This is the same as ``ag.get_addresses(start=0, count=2)``, but + # the order is reversed. + [ + Address( + b'OEEOXMWT99FDFSXJYCKXZ9YKHDVJYWLMYFGWKWTB' + b'LELJPTRZULNTCMIUY9GEGJF9OVIAYJOVKXVPCQGLZ' + ), + + Address( + b'TZIXDQENXNLLVIRAQFZOEZKKZDWJIVJCUH9APDTF' + b'9BNXQUBJAUXZANSBPISZUCBSYQ9UKVAZNEMOKFKOA' + ), + ], + ) + + def test_generator(self): + """ + Creating a generator. + """ + ag = AddressGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999IPKZWMLYYOLWBJGINLSO9EEYQMCUJ', + ) + + generator = ag.create_generator() + + self.assertEqual( + next(generator), + + Address( + b'EFJQQFBNRDDEB9PFSOJTXZGXPVPEZUGWAUWGFEXV' + b'NQEQE9PTXHGIWKDXAHBQGRHSFQQAJORUBGRKVXVNC' + ), + ) + + self.assertEqual( + next(generator), + + Address( + b'UIFOSKQQMEFFCHOCDRZPXGTDXUJLBTVYELZQXOQ9' + b'DOOHIUFMIXJZFWMQXGNHJNZ9XHTNNSQCGLQBMNDPJ' + ), + ) + + def test_generator_with_offset(self): + """ + Creating a generator that starts at an offset greater than 0. + """ + ag = AddressGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999FFRFYAMRNWLGSGZNYUJNEBNWJQNYF', + ) + + generator = ag.create_generator(start=3, step=2) + + self.assertEqual( + next(generator), + + Address( + b'HTSDTNXY9MTVRUAGKTDFIMD9ZOYYELHDCHHWUZYF' + b'GCNOJVJVPLPPTONWYQDIPELEHYX9HHKXWWUSOIJUE' + ), + ) + + self.assertEqual( + next(generator), + + Address( + b'EGNGQFXNJQDUQJGIUMPBGGHFJTH9EXROLGQINOCW' + b'GIDXIZGHXKLOMIQVQRFSRUGYDWZA9EQCEZCJGEBHX' + ), + ) diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 21e71a4..b875b97 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -26,16 +26,16 @@ class KeyGeneratorTestCase(TestCase): References: - http://stackoverflow.com/a/1751478/ """ - def test_generate_single_key(self): + def test_get_keys_single(self): """ Generating a single key. """ - ag = KeyGenerator( + kg = KeyGenerator( seed = b'TESTSEED9DONTUSEINPRODUCTION99999ZTRFNBTRBSDIHWKOWCFBOQYQTENWL', ) self.assertListEqual( - ag.get_keys(0), + kg.get_keys(start=0), # Note that the result is always a list, even when generating a # single key. @@ -80,7 +80,7 @@ def test_generate_single_key(self): ) self.assertListEqual( - ag.get_keys(1), + kg.get_keys(start=1), [ SigningKey( @@ -127,7 +127,7 @@ def test_generate_single_key(self): # Note: this can be a slow process, so we'll keep the numbers small # so that tests don't take too long. self.assertListEqual( - ag.get_keys(13), + kg.get_keys(start=13), [ SigningKey( @@ -169,18 +169,16 @@ def test_generate_single_key(self): ], ) - def test_generate_multiple_keys(self): + def test_get_keys_multiple(self): """ Generating multiple keys in one go. """ - ag = KeyGenerator( + kg = KeyGenerator( seed = b'TESTSEED9DONTUSEINPRODUCTION99999TPXGCGPRTMI9QQNCW9PKWTAAOPYHU', ) self.assertListEqual( - # ``get_keys`` parameters have the same behavior as slices. - # I.E., ``ag.get_keys(1, 3) == ag[1:3]``. - ag.get_keys(1, 3), + kg.get_keys(start=1, count=2), [ SigningKey( @@ -259,18 +257,143 @@ def test_generate_multiple_keys(self): ], ) + def test_get_keys_error_start_too_small(self): + """ + Providing a negative ``start`` value to ``get_keys``. + + :py:class:`KeyGenerator` can potentially generate an infinite + number of keys, so there is no "end" to offset against. + """ + kg = KeyGenerator(seed=b'') + + with self.assertRaises(ValueError): + kg.get_keys(start=-1) + + def test_get_keys_error_count_too_small(self): + """ + Providing a ``count`` value less than 1 to ``get_keys``. + + :py:class:`KeyGenerator` can potentially generate an infinite + number of keys, so there is no "end" to offset against. + """ + kg = KeyGenerator(seed=b'') + + with self.assertRaises(ValueError): + kg.get_keys(start=42, count=0) + + def test_get_keys_error_step_zero(self): + """ + Providing a ``step`` value of 0 to ``get_keys``. + """ + kg = KeyGenerator(seed=b'') + + with self.assertRaises(ValueError): + kg.get_keys(start=42, step=0) + + def test_get_keys_step_negative(self): + """ + Providing a negative ``step`` value to ``get_keys``. + + This is probably a weird use case, but what the heck. + """ + kg = KeyGenerator( + seed = b'TESTSEED9DONTUSEINPRODUCTION99999JKOJGZVGHCUXXGFZEMMGDSGWDCKJX', + ) + + self.assertListEqual( + kg.get_keys(start=1, count=2, step=-1), + + # This is the same as ``kg.get_keys(start=0, count=2)``, except + # the order is reversed. + [ + SigningKey( + b'ODJGKFVKDKETMOUH9OCDBIDQCEMEDVKMEOIKSDTIQFECONPDCZUITROKXYCTNEMVQI' + b'KZWKIJQZVYEAEUFGWAZUJEQMZLBPPYLJYEBOEZNYDFWKLYB9S9GTVZOXGGQ9CVYVVX' + b'ZNIPHGIARTYMUXOJVTQZSASWVK9CSRUJODBJRPCCDDFEPZRNYHIGMFRFXAFGPMACLZ' + b'WTNBVFISOJGBYRBONVFSLUWNZGERXLOCWRSNDFSZHKZMKVQDUXALLWDDOZZAEEXDDK' + b'SXBZC9NDTIKMEZE9ZMDLUVFXKFPEZGPSFVHLQXLVZNCQGITVJKOABSRXALRMECHAZS' + b'EXXDKEKLHXYVG9U9FZXWQHRGZKDUIA9XACNYPPWWLTXTGEAGXCQV9ZRA9OTGQVVWTG' + b'OMHKSOA9YYBHZFNUGX9YF9LBXHK9DFFNIESPRMVESGMIXPNGOJLWMBAXIRIDQNPSLQ' + b'XFELX9QDANRNJYKQLSOFUFQKSXWFGOB9HZQZDND9TKWYONT9LPWMUSYBZNRXJQLWDH' + b'JMDODMYPHNWACOQG9ROBLJJCDDEJHWFXAJAGTILEO9BODJJMFYJPOK9DMTXCFAGSEJ' + b'BMJQTPCETXDRKKUGWHDND9SGZZZDNFNVWYJYRDWPGIQZJMKQAEZPHIEZUVXIHHUWQN' + b'TI9YMTVWZBHKZJVJJEPUNEACXMOKZXDKDSRMLYSETAHODDZGFDW9BQRQ9ULXSSQRQN' + b'RYCRL9YJQZYHBSTXHGVQLNIXBETRHZQNYZPDNPMHXIQZVLHZUVYWXHPNWVMAIAJYHF' + b'DSF9XKYLGPEOQCVEFBIMBNSMUVLEWOMBOTPNGOLWKFUHQWLFTEBKUPZSAXHLK9JIKY' + b'CNPLLDTLGNJUNELVRIKPXPONORRXUHKIA9BWFRKATZZHHISN9GWBEKCZXLLXVAZPVT' + b'OBGGUYPAGLPCD9HADLEIBWGWALONELBMZU9HGDBRXPHL9TFU9EPRGUUZQKPPHJRFBC' + b'C9LNXCKGDRZMWJSGSMWI9VDQGKXEULXWTMROCADMYBFWBEGQXTANPOPAWEQAKYNHLO' + b'LGALTNSIEZJXHIMRQPBJKDOCMZSNQMDKMOQMXRVULJCPEBNK9ELRAZ9XTCKCKUJKLH' + b'FNBVFKYCTKELJSKCDVOOACVEAFPFUIZNXUES9KRVPSVMXVFLYVN9UQCNKWMJAAJA9E' + b'DSYFEUUJTNTAOUZXVAUDNZBC9WCUJ9QNWMSWWIQXKUNNFUUUJXWAPOQDBKVXDUVERI' + b'AXMCSUFLSRIFAYBNU9FDPVGOZOU9HYK9NUUV9WOLHVETGNASJPRHKBTFFURRIXWRPP' + b'RXBOPZZYUFCVHZYBR9DXKSPVAKJKMSSBHOXKTAGTEUUCGDAZWWXQT9QJZQEASYJKTF' + b'SBCXRJQBSACBPNCTAHEZQSNYCEJGVEDK9VTSDR9NYIBMQDUSCMMARWBWTVTYUMTVMU' + b'TCIRIYGSGSLUUABTVBNAGQ99HSFTMJDMEUFNTDUKBCXWVLPEAHDIQREFBWKZXRUTRV' + b'OARCTDNCSPLVP9YAJKFJZG9MVMNIKMBNKT9NEPNRMCAYNPNSYDVOJEFJOMAIEUOPUA' + b'DOFCJBZBULOMFHQCQUDTPNYC9GV9LHSUYFNAEWCIUCNWERVIAKBMPNFKOXGMOMWUV9' + b'9DCI9HFOGWELJUEBHKOBT9DFXRUNJIFIHJWSUIMRBOUGUVR9QUIJOZ9FKABGGKOOFS' + b'QATVJEJVUTXIU9NXLUHFCOVHRV9AGBUMBGGBJQKVFZFCDWOVGPZAIAKMKQYQFXLNNS' + b'PPYNGFVYPZZFCCCF9FPJMDBCCMQHANYURFWEZQIAYCCRIGQETJLUOFKPVC9ESXMGAC' + b'SUQZNWNLQTTKRJ9QMAXPFFGNYHALLPIVNCEPMJFEYPVPXYGKYMJNQESUUWRICVFPTV' + b'VOOGJFJB9T9KZRAPYHIHLXA9HGKSRTEGNHCUWZPUSIQOTFWEMHCHBEYFXXFWJTSQIC' + b'RKRJIBBVEGGNILSTXROYHEXKQBWALNKPMAFSTVCOSOTOKVFRVMFTXGQKOZJRDICBRF' + b'TOMK9FDMKAWUSKJDEVHCSHNLK9IHODD9ZIHXBCKV9QDEIRTKYPLEZLPHCBGCNXUJCL' + b'EQFHFJDLXIL9P9WRSBKVFTZXMZEIHWJWJJVSEJZTUDAOXULUB9OJZQHEVD9DTFKDTF' + b'KVEPFJDTR' + ), + + SigningKey( + b'TIOBFKKFAELHQOCLGJBGJZWVNSZMBPK9D9GTBZWDJBFFIKJWJBKIAAAAPDMYOCPDRN' + b'BMK9QGJZAMIQZKSBNVVDLIVNRYVWQSHSJZZNGJGWNHGDMYCDZLIIUAPIPBKYSWIFNP' + b'ZLVUGCN9MIQTYFYJWVIKXOUOFHAMCYZEVBHYABYPEQHXPHIPM9NWENCXRLUYQFILME' + b'HZYQUAOCIT9HZUDRLNOJPZGWURENUPQFG9CJCAKZRRNKICSUCLYWVEFDLMXIVQCBDP' + b'RQXLSCFVRIZBDCWENUPAIEWKZLTIUWLWFGOAQRVANDCTVTDWKJOGNICUOUGVQAHKRR' + b'EGIDORXMBADYNBXLEUMP9PK9TAMVBJEFGFGCXQZQSJNUBOUUCKDYMUAVOJIWMDA9LF' + b'WPDZHSOMXJFUFCXUIVHRMDCOVSRO9ACUHNEAWCPQCEYVRTMGJWARYYNEYUXQQLQDPS' + b'RAABZ9WYQUQDCOOYBZYBXDEMINQHLWKXLLOUCRTAWLGSCZTASUVIJJBVDUPTNMOBIY' + b'BC9CVDPJNGTBOZZHLDWIGLAHYQVKNWKQQXBY9GPSD9R9KZWNNUDDYXYXJHNDURRTJF' + b'9NXPMVGVSEEHNOKESYRVFBMJGHQLGHGTTEBEPMWW9JNOMKFTVJR9VHHWEXEOAIBNQI' + b'CFNUXLTZCYHHLADHHHAQHSWDTEVUGXHPMUDSIKGZHRMDDLBPZYNNHUXTSUBWUWZMDF' + b'999AKZSRZLAKVDQVWWTN9DPNAMHCJENMKMS9ISTDLB9PBBJVPTHAEX99JZQQXPPCS9' + b'PIOHNVUWINMCLVZVAO9SDEKIQOLZFDOCIQKNPLR9EEEVX9TSNWIP9GOBLWUCUVGTJI' + b'WTZKLWRQTCXZNDFSMJTHSEGNDRMTJP9UQBLZSNWWI9DPI9XPPCDBW9SJIKHODBMJNM' + b'HIEEHLJXOXVUTSWXJ9IPGGCVKLSNVVVNJC9Q9CJYHGGCMGUCBMBIVAODZVANGATWI9' + b'GJFJBTBGFGIDKJUHRXGNVFJCMCADBYKKGAOOCDIXTBKBRQFVCVGPTSEZFTXIMT9PUX' + b'QECAOKJZOPDHXZTFVNNUKUGGZMLVNZYFSHSYWKNXNIIHRBCRGMHVWSWMTS9DCTNLMG' + b'ONAP9EIAUDDZ9HZLITHGENXAWZMXJWUARORECXLWSNWCBOSEUMAQGGIISHXWUZ9A9M' + b'WATL9IZNLH9JJCGAZGJMNEAOWRODWE9DXGPCOVCQGGJOGCH9AA9NFKHYJLCDWUFCNF' + b'WQNZC9PYPGHNPSWT9DZEFUZFPOTNLDLQSRCTSG9YHFWVNDVTGPGGPC9OYOGEUDAGFV' + b'OMVZDQXSWYFAYRSHEZ9PNZMIJPQMYXLIXHYEHZTFSDMCUCKMABZKAMZFUXOGIGHRUG' + b'XABWWL9GRWPATDHEEMOJEMJSSVWKNVOYLQHXUVHIAMMQDEWNWAOPJXBAQVKCBIXDYO' + b'DSPKQHOBVSJNHALPEYCJAZCGWKSXLZZQXYD9ZXAJOOXNB9WEKKVLFFISCBIEIEHJMU' + b'OLMZP9VTHGXDYPQVQO9FYTPMAYDWRRAOQGBDFCEWBXQGLYXGBHCDNDKEVJPWDCYDCR' + b'HZVXTSOSNFXCVZWFJMCSMMUTWMBVDLIUDYTELXUKFSHXH9VKBQAXFPZOKDJBF9LJAF' + b'TR9MYBJLRADSLKU9VVAAPYYEZBRQYKPHGQQHFTYPZUIFIWMWVESRZXPIAIAFQIBKEZ' + b'DIIVTJMHDTPZBQVXSLPTPZBCYVJFYKETYIJQSURIDGRLYZHQKNWKMWKTVTSBFVUZFS' + b'QXUAKQE9AGO9JHBDYCMTGDIRKWJCACCJLWKFOKLAIYBXMPF9XHCT9SWETORWSDLBPY' + b'HSIFZBREQLVLSARPYAFRQUDXYRB999FZFOILTR9GZDTVEFXOQZCAX9FQJRXOSXF9S9' + b'KWJJSRIBDKKPPLWWXPJDLXYUTTRPZBCJRRWLIUEZKOBEAWUVVHKVEEQUKK9MMWWCLI' + b'GEC9LMBXBJ9HSJR9OBWOLUKSXRJAJAIPXXASTICFAYELQEOIRQZKXHPWOSXGEUNQED' + b'VUZNFZ9T9CSECMDQ9ENSXLFNONKZNCNCCQGIJQAKSJSKFUXXSYWVNBFTGKLDMALNHD' + b'WYGBJ99Y9OSENWWKYPWP9IKFSMRFI9QCEFTUZKZTZRHHNXXMEPBGBWHBKYRSCHQ9U9' + b'JJTUIQBXZ' + ) + ], + ) + def test_iterations(self): """ Using more iterations to generate longer, more secure keys. """ - ag = KeyGenerator( + kg = KeyGenerator( # Using the same seed as the previous test, just to make sure the # key generator doesn't cheat. seed = b'TESTSEED9DONTUSEINPRODUCTION99999TPXGCGPRTMI9QQNCW9PKWTAAOPYHU', ) self.assertListEqual( - ag.get_keys(start=1, iterations=2), + kg.get_keys(start=1, iterations=2), [ # 2 iterations = key is twice as long! @@ -350,11 +473,11 @@ def test_generator(self): """ Creating a generator. """ - ag = KeyGenerator( + kg = KeyGenerator( seed = b'TESTSEED9DONTUSEINPRODUCTION99999IPKZWMLYYOLWBJGINLSO9EEYQMCUJ', ) - generator = ag.create_generator() + generator = kg.create_generator() self.assertEqual( next(generator), @@ -442,11 +565,11 @@ def test_generator_with_offset(self): """ Creating a generator that starts at an offset greater than 0. """ - ag = KeyGenerator( + kg = KeyGenerator( seed = b'TESTSEED9DONTUSEINPRODUCTION99999FFRFYAMRNWLGSGZNYUJNEBNWJQNYF', ) - generator = ag.create_generator(start=3, step=2) + generator = kg.create_generator(start=3, step=2) self.assertEqual( next(generator), @@ -535,13 +658,13 @@ def test_generator_with_iterations(self): Creating a generator that uses multiple iterations in order to create longer keys. """ - ag = KeyGenerator( + kg = KeyGenerator( # Using the same seed as the previous test, just to make sure the # key generator doesn't cheat. seed = b'TESTSEED9DONTUSEINPRODUCTION99999FFRFYAMRNWLGSGZNYUJNEBNWJQNYF', ) - generator = ag.create_generator(start=3, iterations=2) + generator = kg.create_generator(start=3, iterations=2) self.assertEqual( next(generator), From c1d58688bd546332c2662e5573c6d249872bbe4f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 18:34:57 -0500 Subject: [PATCH 152/239] Use mocks to massively speed up AddressGenerator tests. --- iota/crypto/addresses.py | 2 +- test/crypto/addresses_test.py | 218 +++++++++++++++++----------------- 2 files changed, 109 insertions(+), 111 deletions(-) diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 1d1d422..73f4a68 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -133,7 +133,7 @@ def create_generator(self, start=0, step=1): current += step def _create_digest_generator(self, start, step): - # type: (int, int) -> Generator[SigningKey] + # type: (int, int) -> Generator[List[int]] """ Initializes a generator to create SigningKey digests. diff --git a/test/crypto/addresses_test.py b/test/crypto/addresses_test.py index c4a1575..33bc24a 100644 --- a/test/crypto/addresses_test.py +++ b/test/crypto/addresses_test.py @@ -2,82 +2,81 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Generator, List from unittest import TestCase -from iota import Address +from mock import patch + +from iota import Address, TryteString from iota.crypto.addresses import AddressGenerator # noinspection SpellCheckingInspection class AddressGeneratorTestCase(TestCase): + def setUp(self): + super(AddressGeneratorTestCase, self).setUp() + + # Addresses that correspond to the digests defined in + # :py:meth:`_mock_digest_gen`. + self.addy0 =\ + Address( + b'VOPYUSDRHYGGOHLAYDWCLLOFWBLK99PYYKENW9IQ' + b'IVIOYMLCCPXGICDBZKCQVJLDWWJLTTUVIXCTOZ9TN' + ) + + self.addy1 =\ + Address( + b'SKKMQAGLZMXWSXRVVRFWMGN9TIXDACQMCXZJRPMS' + b'UFNSXMFOGEBZZPUJBVKVSJNYPSGSXQIUHTRKECVQE' + ) + + self.addy2 =\ + Address( + b'VMMFSGEYJ9SANRULNIMKEZUYVRTWMVR9UKCYDZXW' + b'9TENBWIRMFODOSNMDH9QOVBLQWALOHMSBGEVIXSXY' + ) + + self.addy3 =\ + Address( + b'G9PLHPOMET9NWIBGRGMIF9HFVETTWGKCXWGFYRNG' + b'CFANWBQFGMFKITZBJDSYLGXYUIQVCMXFWSWFRNHRV' + ) + + def test_get_addresses_single(self): """ Generating a single address. """ - ag = AddressGenerator( - seed = b'ITJVZTRFNBTRBSDIHWKOWCFBOQYQTENWLRUVHIBCBRTXYGDCCLLMM9DI9OQO', - ) + # Seed is not important for this test; it is only used by + # :py:class:`KeyGenerator`, which we will mock in this test. + ag = AddressGenerator(seed=b'') - self.assertListEqual( - ag.get_addresses(start=0), - - [ - Address( - b'QIMWUZUX9RBGQZQDMAVBHLMUP9SHRUDYACNRRRBX' - b'QWRLTYFHDLQKYVHLLGBLSSKACNRIQJVX99OUFNUFE' - ), - ], - ) + # noinspection PyUnresolvedReferences + with patch.object(ag, '_create_digest_generator', self._mock_digest_gen): + addresses = ag.get_addresses(start=0) - self.assertListEqual( - ag.get_addresses(start=1), - - [ - Address( - b'IGNEVQHYMVIMZB9XZAFQT9EMDGQJZQUKIVWGOESQ' - b'DFDOEG9YQUPPD9MSKGDLP9QIHKGOSQZ9PTPEAZGNK' - ), - ], - ) + self.assertListEqual(addresses, [self.addy0]) - # You can request an address at any arbitrary index, and the result - # will always be consistent (assuming the seed doesn't change). - # Note: this can be a slow process, so we'll keep the numbers small - # so that tests don't take too long. - self.assertListEqual( - ag.get_addresses(start=13), - - [ - Address( - b'VYJG9FMDTLHJXWXSEMIXJGMNCXWVNKQVBXUCWYLF' - b'FYBBWUXNTECCHZQRA9WWHOKTYVRZTQAVFAKBQRXPJ' - ), - ], - ) + # noinspection PyUnresolvedReferences + with patch.object(ag, '_create_digest_generator', self._mock_digest_gen): + # You can provide any positive integer as the ``start`` value. + addresses = ag.get_addresses(start=2) + + self.assertListEqual(addresses, [self.addy2]) def test_get_addresses_multiple(self): """ Generating multiple addresses in one go. """ - ag = AddressGenerator( - seed = b'TESTSEED9DONTUSEINPRODUCTION99999TPXGCGPRTMI9QQNCW9PKWTAAOPYHU', - ) + # Seed is not important for this test; it is only used by + # :py:class:`KeyGenerator`, which we will mock in this test. + ag = AddressGenerator(seed=b'') - self.assertListEqual( - ag.get_addresses(start=1, count=2), - - [ - Address( - b'AZNEMINAIYSTVTTLPJOXSECRCYJUNUSHDPKKHLJA' - b'SHU9DAYROZP99ZHMCERMKHGALVUJQPGGK9TDMBHMB' - ), - - Address( - b'LOBJATMDBWOZWXZESPPCGFPCHKGAVWRYREQLCKAC' - b'DOUDVDFVFJMIAOZZQHYCCQUOXWZOGEIL9HYFHDQ9S' - ), - ], - ) + # noinspection PyUnresolvedReferences + with patch.object(ag, '_create_digest_generator', self._mock_digest_gen): + addresses = ag.get_addresses(start=1, count=2) + + self.assertListEqual(addresses, [self.addy1, self.addy2]) def test_get_addresses_error_start_too_small(self): """ @@ -118,80 +117,79 @@ def test_get_addresses_step_negative(self): This is probably a weird use case, but what the heck. """ - ag = AddressGenerator( - seed = b'TESTSEED9DONTUSEINPRODUCTION99999GIDXIZGHXKLOMIQVQRFSRUGYDWZA9', - ) + # Seed is not important for this test; it is only used by + # :py:class:`KeyGenerator`, which we will mock in this test. + ag = AddressGenerator(seed=b'') + + # noinspection PyUnresolvedReferences + with patch.object(ag, '_create_digest_generator', self._mock_digest_gen): + addresses = ag.get_addresses(start=1, count=2, step=-1) self.assertListEqual( - ag.get_addresses(start=1, count=2, step=-1), + addresses, # This is the same as ``ag.get_addresses(start=0, count=2)``, but # the order is reversed. - [ - Address( - b'OEEOXMWT99FDFSXJYCKXZ9YKHDVJYWLMYFGWKWTB' - b'LELJPTRZULNTCMIUY9GEGJF9OVIAYJOVKXVPCQGLZ' - ), - - Address( - b'TZIXDQENXNLLVIRAQFZOEZKKZDWJIVJCUH9APDTF' - b'9BNXQUBJAUXZANSBPISZUCBSYQ9UKVAZNEMOKFKOA' - ), - ], + [self.addy1, self.addy0], ) def test_generator(self): """ Creating a generator. """ - ag = AddressGenerator( - seed = b'TESTSEED9DONTUSEINPRODUCTION99999IPKZWMLYYOLWBJGINLSO9EEYQMCUJ', - ) - - generator = ag.create_generator() - - self.assertEqual( - next(generator), - - Address( - b'EFJQQFBNRDDEB9PFSOJTXZGXPVPEZUGWAUWGFEXV' - b'NQEQE9PTXHGIWKDXAHBQGRHSFQQAJORUBGRKVXVNC' - ), - ) + # Seed is not important for this test; it is only used by + # :py:class:`KeyGenerator`, which we will mock in this test. + ag = AddressGenerator(seed=b'') - self.assertEqual( - next(generator), + # noinspection PyUnresolvedReferences + with patch.object(ag, '_create_digest_generator', self._mock_digest_gen): + generator = ag.create_generator() - Address( - b'UIFOSKQQMEFFCHOCDRZPXGTDXUJLBTVYELZQXOQ9' - b'DOOHIUFMIXJZFWMQXGNHJNZ9XHTNNSQCGLQBMNDPJ' - ), - ) + self.assertEqual(next(generator), self.addy0) + self.assertEqual(next(generator), self.addy1) + # ... ad infinitum ... def test_generator_with_offset(self): """ Creating a generator that starts at an offset greater than 0. """ - ag = AddressGenerator( - seed = b'TESTSEED9DONTUSEINPRODUCTION99999FFRFYAMRNWLGSGZNYUJNEBNWJQNYF', - ) + # Seed is not important for this test; it is only used by + # :py:class:`KeyGenerator`, which we will mock in this test. + ag = AddressGenerator(seed=b'') - generator = ag.create_generator(start=3, step=2) + # noinspection PyUnresolvedReferences + with patch.object(ag, '_create_digest_generator', self._mock_digest_gen): + generator = ag.create_generator(start=1, step=2) - self.assertEqual( - next(generator), + self.assertEqual(next(generator), self.addy1) + self.assertEqual(next(generator), self.addy3) - Address( - b'HTSDTNXY9MTVRUAGKTDFIMD9ZOYYELHDCHHWUZYF' - b'GCNOJVJVPLPPTONWYQDIPELEHYX9HHKXWWUSOIJUE' - ), - ) + @staticmethod + def _mock_digest_gen(start, step): + # type: (int, int) -> Generator[List[int]] + """ + Mocks the behavior of :py:class:`KeyGenerator`, to speed up unit + tests. - self.assertEqual( - next(generator), + Note that :py:class:`KeyGenerator` has its own test case, so we're + not impacting the stability of the codebase by doing this. + """ + digests = [ + b'KDWISSPKPF9DZNKMYEVPPYI9CXMFZRAAKCQNMFJI' + b'JIQLT9IPMEVVNYBTIBTN9CBCJMYTUMSRJAEMUUMIA', - Address( - b'EGNGQFXNJQDUQJGIUMPBGGHFJTH9EXROLGQINOCW' - b'GIDXIZGHXKLOMIQVQRFSRUGYDWZA9EQCEZCJGEBHX' - ), - ) + b'XUWRYOZQYEVM9CRZZPZQRTAHBMLM9EYMZZIYRFV9' + b'GZST9XGK9LWUDGIXCCRFLWHUJPYQ9MMYDZEMAJZOI', + + b'SEJHPHEF9NXPPZQUOVGZNUZSP9DQOBRVSAGADUAD' + b'EPRDFQJPXTOJGFPEXUPRQUYTTSD9GVPXTWWZQSGXA', + + b'LNXDEKQCP9OXMBNDUJCZIMVRVGJLKFVMMRPHROSH' + b'XCWW9M9QGYVUMPXCR9ANPEPVGBI9WOERTFDAGKCVZ', + ] + + # This should still behave like the real thing, so that we can + # verify that :py:class`AddressGenerator` is invoking the key + # generator correctly. + for d in digests[start::step]: + yield TryteString(d).as_trits() From bb0c111a85fedd01502afb2c38d91c0fbe4df60f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 19:02:53 -0500 Subject: [PATCH 153/239] Minor cleanup. - Added some more documentation to PyCurl. - Teeny performance boost because why not. - Removed excess whitespace. --- iota/crypto/pycurl.py | 3 +++ iota/crypto/signing.py | 3 ++- test/crypto/addresses_test.py | 1 - 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index c81f6b2..f4d529e 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -54,6 +54,9 @@ def absorb(self, trits): stop = min(start + HASH_LENGTH, length) # Copy the next hash worth of trits to internal state. + # Note that we always copy the trits to the start of the state; + # ``self._state`` is 3 hashes long, which means the last 2 hashes + # are only modified by :py:meth:`_transform`. self._state[0:stop-start] = trits[start:stop] # Transform. diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index b112f3b..404d9b1 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -238,7 +238,8 @@ def _create_sponge(self, index): """ Prepares the Curl sponge for the generator. """ - seed = list(self.seed) # type: MutableSequence[int] + # :see: http://stackoverflow.com/a/2612990/ + seed = self.seed[:] # type: MutableSequence[int] for i in range(index): # Increment each tryte unless/until we overflow. diff --git a/test/crypto/addresses_test.py b/test/crypto/addresses_test.py index 33bc24a..6d1ca86 100644 --- a/test/crypto/addresses_test.py +++ b/test/crypto/addresses_test.py @@ -42,7 +42,6 @@ def setUp(self): b'CFANWBQFGMFKITZBJDSYLGXYUIQVCMXFWSWFRNHRV' ) - def test_get_addresses_single(self): """ Generating a single address. From 9c79fe0d76464dd2c1fc151aebe3ac9b1956b965 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 19:07:06 -0500 Subject: [PATCH 154/239] Fixed incorrect documentation. --- iota/crypto/pycurl.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index f4d529e..31988c5 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -53,10 +53,14 @@ def absorb(self, trits): start = offset stop = min(start + HASH_LENGTH, length) + # # Copy the next hash worth of trits to internal state. - # Note that we always copy the trits to the start of the state; - # ``self._state`` is 3 hashes long, which means the last 2 hashes - # are only modified by :py:meth:`_transform`. + # + # Note that we always copy the trits to the start of the state. + # ``self._state`` is 3 hashes long, but only the first hash is + # "public"; the other 2 are only accessible to + # :py:meth:`_transform`. + # self._state[0:stop-start] = trits[start:stop] # Transform. @@ -74,11 +78,14 @@ def squeeze(self, trits): Sequence that the squeezed trits will be copied to. Note: this object will be modified! """ + # # Squeeze is kind of like the opposite of absorb; it copies trits # from internal state to the ``trits`` parameter, one hash at a # time, and transforming internal state in between hashes. - # However, internal state is always exactly 1 hash in length, so - # the implementation can be simplified somewhat. + # + # However, only the first hash of the state is "public", so we + # can simplify the implementation somewhat. + # # Note that we copy at most len(trits) trits! length = min(HASH_LENGTH, len(trits)) @@ -92,7 +99,8 @@ def _transform(self): """ Transforms internal state. """ - # Copy some values locally so we can avoid global lookups. + # Copy some values locally so we can avoid global lookups in the + # inner loop. # :see: https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Local_Variables state_length = STATE_LENGTH truth_table = TRUTH_TABLE From 21688ae109c98d52bb681e5fc73e08a63ffffe8f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 19:09:23 -0500 Subject: [PATCH 155/239] Fixed wonky grammar in documentation. --- iota/crypto/pycurl.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index 31988c5..b8cffb6 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -116,8 +116,8 @@ def _transform(self): # baleeted. range_ = range - # Operate on a copy of ``self._state`` to avoid the number of dot - # lookups that we perform in the inner loop. + # Operate on a copy of ``self._state`` to eliminate dot lookups in + # the inner loop. # :see: https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Avoiding_dots... # :see: http://stackoverflow.com/a/2612990/ prev_state = self._state[:] From 165b63c8152581c4b7f44fdfa237aa28bbbd444f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 20 Dec 2016 19:20:41 -0500 Subject: [PATCH 156/239] Improved type safety. Prevent probably-wrong operations like initializing an Address from a Tag and other such oddness. --- iota/crypto/addresses.py | 6 +++--- iota/types.py | 18 +++++++++++++++--- test/types_test.py | 26 ++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 6 deletions(-) diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 73f4a68..36f5e9a 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -4,9 +4,9 @@ from typing import Generator, Iterable, List, MutableSequence -from iota import Address, TryteString, TrytesCompatible +from iota import Address, TrytesCompatible from iota.crypto import Curl -from iota.crypto.signing import KeyGenerator, SigningKey +from iota.crypto.signing import KeyGenerator, Seed, SigningKey __all__ = [ 'AddressGenerator', @@ -31,7 +31,7 @@ def __init__(self, seed): # type: (TrytesCompatible) -> None super(AddressGenerator, self).__init__() - self.seed = TryteString(seed) + self.seed = Seed(seed) def __iter__(self): # type: () -> Generator[Address] diff --git a/iota/types.py b/iota/types.py index 065e81a..3f7a4be 100644 --- a/iota/types.py +++ b/iota/types.py @@ -218,9 +218,21 @@ def __init__(self, trytes, pad=None): ) if isinstance(trytes, TryteString): - # Create a copy of the incoming TryteString's trytes, to ensure - # we don't modify it when we apply padding. - trytes = bytearray(trytes._trytes) + incoming_type = type(trytes) + + if incoming_type is TryteString or issubclass(incoming_type, type(self)): + # Create a copy of the incoming TryteString's trytes, to ensure + # we don't modify it when we apply padding. + trytes = bytearray(trytes._trytes) + + else: + raise TypeError( + '{cls} cannot be initialized from a(n) {type}.'.format( + type = type(trytes).__name__, + cls = type(self).__name__, + ), + ) + else: if not isinstance(trytes, bytearray): trytes = bytearray(trytes) diff --git a/test/types_test.py b/test/types_test.py index c525efc..1bb1448 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -147,6 +147,32 @@ def test_init_from_tryte_string(self): self.assertFalse(trytes1 is trytes2) self.assertTrue(trytes1 == trytes2) + def test_init_from_tryte_string_error_wrong_subclass(self): + """ + Initializing a TryteString from a conflicting subclass instance. + + This restriction does not apply when initializing a TryteString + instance; only subclasses. + """ + tag = Tag(b'RBTC9D9DCDQAEASBYBCCKBFA') + + with self.assertRaises(TypeError): + # When initializing a subclassed TryteString, you have to use the + # same type (or a generic TryteString). + Address(tag) + + # If you are 110% confident that you know what you are doing, you + # can force the conversion by casting as a generic TryteString + # first. + addy = Address(TryteString(tag)) + + self.assertEqual( + binary_type(addy), + + b'RBTC9D9DCDQAEASBYBCCKBFA9999999999999999' + b'99999999999999999999999999999999999999999', + ) + def test_init_padding(self): """ Apply padding to ensure a TryteString has a minimum length. From a8796688752b8da8b1861ebbfdc8197b599c5504 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 12:57:44 -0500 Subject: [PATCH 157/239] Removed custom Seed type. Seeds are not supposed to be padded automatically; this will be fixed in a future commit. --- iota/crypto/addresses.py | 7 ++- iota/crypto/signing.py | 108 +----------------------------------- iota/crypto/types.py | 96 ++++++++++++++++++++++++++++++++ test/crypto/signing_test.py | 57 +------------------ test/crypto/types_test.py | 61 ++++++++++++++++++++ 5 files changed, 166 insertions(+), 163 deletions(-) create mode 100644 iota/crypto/types.py create mode 100644 test/crypto/types_test.py diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 36f5e9a..cd839b6 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -4,9 +4,10 @@ from typing import Generator, Iterable, List, MutableSequence -from iota import Address, TrytesCompatible +from iota import Address, TryteString, TrytesCompatible from iota.crypto import Curl -from iota.crypto.signing import KeyGenerator, Seed, SigningKey +from iota.crypto.signing import KeyGenerator +from iota.crypto.types import SigningKey __all__ = [ 'AddressGenerator', @@ -31,7 +32,7 @@ def __init__(self, seed): # type: (TrytesCompatible) -> None super(AddressGenerator, self).__init__() - self.seed = Seed(seed) + self.seed = TryteString(seed) def __iter__(self): # type: () -> Generator[Address] diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 404d9b1..ca07164 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -6,115 +6,13 @@ from iota import TryteString, TrytesCompatible from iota.crypto import Curl, HASH_LENGTH +from iota.crypto.types import SigningKey __all__ = [ 'KeyGenerator', - 'Seed', - 'SigningKey', ] -class Seed(TryteString): - """ - A TryteString that acts as a seed for generating new keys. - """ - LEN = 81 - - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(Seed, self).__init__(trytes, pad=self.LEN) - - if len(self._trytes) > self.LEN: - raise ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )) - - -class SigningKey(TryteString): - """ - A TryteString that acts as a signing key, e.g., for generating - message signatures, new addresses, etc. - """ - BLOCK_LEN = 2187 - """ - Similar to RSA keys, SigningKeys must have a length that is divisible - by a certain number of trytes. - """ - - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(SigningKey, self).__init__(trytes) - - if len(self._trytes) % self.BLOCK_LEN: - raise ValueError( - 'Length of {cls} values must be a multiple of {len} trytes.'.format( - cls = type(self).__name__, - len = self.BLOCK_LEN - ), - ) - - @property - def block_count(self): - # type: () -> int - """ - Returns the length of this key, expressed in blocks. - """ - return len(self) // self.BLOCK_LEN - - def get_digest_trits(self): - # type: () -> List[int] - """ - Generates the digest used to do the actual signing. - - Signing keys can have variable length and tend to be quite long, - which makes them not-well-suited for use in crypto algorithms. - - The digest is essentially the result of running the signing key - through a PBKDF, yielding a constant-length hash that can be used - for crypto. - """ - # Multiply by 3 to convert trytes into trits. - block_size = self.BLOCK_LEN * 3 - raw_trits = self.as_trits() - - # Initialize list with the correct length to improve performance. - digest = [0] * HASH_LENGTH # type: List[int] - - for i in range(self.block_count): - block_start = i * block_size - block_end = block_start + block_size - - block_trits = raw_trits[block_start:block_end] - - # Initialize ``key_fragment`` with the correct length to - # improve performance. - key_fragment = [0] * block_size # type: List[int] - - buffer = [] # type: List[int] - - for j in range(27): - hash_start = j * HASH_LENGTH - hash_end = hash_start + HASH_LENGTH - - buffer = block_trits[hash_start:hash_end] - - for k in range(26): - sponge = Curl() - sponge.absorb(buffer) - sponge.squeeze(buffer) - - key_fragment[hash_start:hash_end] = buffer - - sponge = Curl() - sponge.absorb(key_fragment) - sponge.squeeze(buffer) - - digest[block_start:block_end] = buffer - - return digest - - class KeyGenerator(object): """ Generates signing keys for messages. @@ -123,7 +21,7 @@ def __init__(self, seed): # type: (TrytesCompatible) -> None super(KeyGenerator, self).__init__() - self.seed = Seed(seed).as_trits() + self.seed = TryteString(seed, pad=81).as_trits() def get_keys(self, start, count=1, step=1, iterations=1): # type: (int, int, int, int) -> List[SigningKey] @@ -225,7 +123,7 @@ def create_generator(self, start=0, step=1, iterations=1): for j in range(27): # Multiply by 3 because sponge works with trits, but # ``Seed.LEN`` is a quantity of trytes. - buffer = [0] * (Seed.LEN * 3) # type: MutableSequence[int] + buffer = [0] * HASH_LENGTH # type: MutableSequence[int] sponge.squeeze(buffer) key += buffer diff --git a/iota/crypto/types.py b/iota/crypto/types.py new file mode 100644 index 0000000..d52b178 --- /dev/null +++ b/iota/crypto/types.py @@ -0,0 +1,96 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import List + +from iota import TryteString, TrytesCompatible +from iota.crypto import HASH_LENGTH, Curl + +__all__ = [ + 'SigningKey', +] + + +class SigningKey(TryteString): + """ + A TryteString that acts as a signing key, e.g., for generating + message signatures, new addresses, etc. + """ + BLOCK_LEN = 2187 + """ + Similar to RSA keys, SigningKeys must have a length that is divisible + by a certain number of trytes. + """ + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(SigningKey, self).__init__(trytes) + + if len(self._trytes) % self.BLOCK_LEN: + raise ValueError( + 'Length of {cls} values must be a multiple of {len} trytes.'.format( + cls = type(self).__name__, + len = self.BLOCK_LEN + ), + ) + + @property + def block_count(self): + # type: () -> int + """ + Returns the length of this key, expressed in blocks. + """ + return len(self) // self.BLOCK_LEN + + def get_digest_trits(self): + # type: () -> List[int] + """ + Generates the digest used to do the actual signing. + + Signing keys can have variable length and tend to be quite long, + which makes them not-well-suited for use in crypto algorithms. + + The digest is essentially the result of running the signing key + through a PBKDF, yielding a constant-length hash that can be used + for crypto. + """ + # Multiply by 3 to convert trytes into trits. + block_size = self.BLOCK_LEN * 3 + raw_trits = self.as_trits() + + # Initialize list with the correct length to improve performance. + digest = [0] * HASH_LENGTH # type: List[int] + + for i in range(self.block_count): + block_start = i * block_size + block_end = block_start + block_size + + block_trits = raw_trits[block_start:block_end] + + # Initialize ``key_fragment`` with the correct length to + # improve performance. + key_fragment = [0] * block_size # type: List[int] + + buffer = [] # type: List[int] + + for j in range(27): + hash_start = j * HASH_LENGTH + hash_end = hash_start + HASH_LENGTH + + buffer = block_trits[hash_start:hash_end] + + for k in range(26): + sponge = Curl() + sponge.absorb(buffer) + sponge.squeeze(buffer) + + key_fragment[hash_start:hash_end] = buffer + + sponge = Curl() + sponge.absorb(key_fragment) + sponge.squeeze(buffer) + + digest[block_start:block_end] = buffer + + return digest diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index b875b97..d7ef7e8 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -4,8 +4,8 @@ from unittest import TestCase -from iota import TryteString -from iota.crypto.signing import KeyGenerator, SigningKey +from iota.crypto.signing import KeyGenerator +from iota.crypto.types import SigningKey # noinspection SpellCheckingInspection @@ -739,56 +739,3 @@ def test_generator_with_iterations(self): b'I9K9NDMQVNOWBHTZL9' ), ) - - -# noinspection SpellCheckingInspection -class SigningKeyTestCase(TestCase): - def test_get_digest_trits(self): - """ - Generating digest trits from a valid SigningKey. - """ - key = SigningKey( - b'BWFTZWBZVFOSQYHQFXOPYTZ9SWB9RYYHBOUA9NOYSWGALF9MSVNEDW9A9FLGBRWKED' - b'MPEIPRKBMRXRLLFJCAGVIMXPISRGXIJQ9BOBHKJEUKDEUUWYXJGCGAWHYBQHBPMRTZ' - b'FPBGNLMKPZYXZPXFSPFUWZNRWYXUEWMP9URKVVJOSWEPJKSMPLWZPIZGOTVAA9QQOC' - b'YISMGHSBU9YCXZCMSTPJVASDKEVZCSPNSPYOUUWWFTNWZTTZBKGZ9PDNAKNSGNODSB' - b'IRKUGFYCZXIFHQCDTXQNLMKRVKIFJS9XARBNMJQOTDL9CAOKEXQTMWCKWRNHLLMLYP' - b'QGTDFNTDBHNAFRBEUWTKPKPECAADKRPEAFDHABMYYXQPQYDSGFRSRFNHFHHHTAH9YF' - b'OXKRZOTKAHZPRISHZRR9YBVSOZUSKKU9HTCXPTPZFAHFMOQJBKZIACZB9ZRXFPPPMY' - b'RBCWPBAPRFXLQZOTGXJGMZUUEZIAVWXUEN9UIFLEESVCCGNKDISMEPYWTXDQHOSUWZ' - b'OEHOCZQJKDCJJNRZVODVNNUOV9FZQEXFGAMDKMV9PVUYMWTFNISYGYKQG9OKNOQUEK' - b'YDEJ9EGHUXQFCPHTTVBCRTZJLOAWHGDEQHLPLHWTWVOBCCQTCWCNLYDGUV9FKFZENU' - b'NOCOYNU9CYQDSAQDSMZGRQYB9YOCFSOHQXANMSPYFVCTPTZKUGIGUZMPJIJRZVN9VX' - b'ADLIVJYJGQWXBOBBAROGNIOIJVRUHWMFLVCGTMZISADLTXVEZLQDYAVQ9OCYQDPCYL' - b'DD9SUPNTUMUESC9VRSCAYBPXAPTYXZODUUMBNCSLOWJYJA9JBITZLZNHPZFGSPRURJ' - b'HYFLSFTMEPAEG9DWRFOTFWWPGGDGZWFVEPOHDGNMOXUSR9AVQLNDUMGPYWVN9LKEIZ' - b'Q9MNIUPJXPTRYMSXRA9GTSZFMNZZT9Z9HJOKVBCHRRIZZN9UYTVRDNHOXYSFRO9FRK' - b'HWNNZ9DXTLV9D9PNJLGWJXAFUOJZTRVJOLYGSPNVCXYWMTOEEUBLNRGAJPK9HIWGZM' - b'HMBTHLABTACRYLQIDPOYEFNSYQ9YAQPOYYDCJAAAVDUWCHSS9OKQYH9CQNUPYRTCWK' - b'CXYDKTAIJKWOQIHSGBZMFJXGOQDODBDZNBOPFYCBLSU9RJYVGXINUDODGHNAGFDAEG' - b'LPDVSCJPCIZHOFNHCZUTRLQXEZUFDZVROFXVHUNWMYRSZHBZAFCWIY9ULTBTEDSKEC' - b'CLDGAU9MZXYRAXVY9NIQUYHATJCZXDSAELMCXQMALHNFMEAWHIQAZMQOO9QPEPDOYC' - b'OJXTUWEJHMPXGZBXFPNXUPOSDINZJNIREYDFZMESFQUPBKSDGTAJHEZSCOVSLYUAUK' - b'DIWNNLQJTPYYTPRGGN9IXIORWXHJBPYINQZIUXKLKXCTQZYJIRH9MHYBQIFQZCFAKZ' - b'DUUZTYIMTNNVNKVMFIW9UYRTVRQHMYR9Y9VYFTPBJGSB9VINGTMBKVZJEZUE9RMBDZ' - b'CQGDHNPW9YIJHLGFOG9YAPXZECSFVXAMPILBIHC9DBGMIE99YEPGTAALOHBUKXSGFZ' - b'YHHWFOIHMEDXFHIYSUHADOCKFNGHKTNPZHINAWG9YGRJBQGECRCVPXXOG9CNVJNFLC' - b'LMGC9I9HAGTAGGVRKCXDJWDNHYZBFNQSKH99MFAMLGRSBMIBBMHBDJTVSQ99ZHYPSS' - b'XLUNFCNOJXITUETNBHIGXFLLEHUKEXGJLO9BBALXMNGJKTETFIZHSSKLOQXPXOSZRW' - b'QP9A9RIHWEHATMSVMZEQPGAUQBCIAQXZSUUFSU9HYK9RAVASYCVNALKJJXAJF9RLTD' - b'ZEIIYCFLQVMHBPBFHHQNXVEKPHOOFTQEIVB9IXZMTOFBHTGLPWDYVPO9HHBPVWZYEG' - b'IDMK9UPWEJDLIPJSIGFKKCZFRJVDN9ENWADNOTFWZGUDJRRUMFPVXHNAJBBCI9WEDK' - b'RKCQUHRQTCFYFHXPOFBC9BCENMI9HRSIUAKLWEAOUXRBWMHWLGEOCP9NWIJAXODJDS' - b'P9SKEXEDVUGHZAFPNMR9PXD9THOWNWWTDTWTYMDINGC9EBBVUYZRUQDVSIOXAEVGFP' - b'XS9CLTHUESMTDWUJNCZSOIOEJG9WKNAZMDJGMRBXGVMLUAN9IGDVFAESJMXNTMNFND' - b'CAXEBRAU9' - ) - - self.assertEqual( - TryteString.from_trits(key.get_digest_trits()), - - TryteString( - b'ABQXVJNER9MPMXMBPNMFBMDGTXRWSYHNZKGAGUOI' - b'JKOJGZVGHCUXXGFZEMMGDSGWDCKJXO9ILLFAKGGZE' - ), - ) diff --git a/test/crypto/types_test.py b/test/crypto/types_test.py new file mode 100644 index 0000000..d9b95b6 --- /dev/null +++ b/test/crypto/types_test.py @@ -0,0 +1,61 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota import TryteString +from iota.crypto.types import SigningKey + + +# noinspection SpellCheckingInspection +class SigningKeyTestCase(TestCase): + def test_get_digest_trits(self): + """ + Generating digest trits from a valid SigningKey. + """ + key = SigningKey( + b'BWFTZWBZVFOSQYHQFXOPYTZ9SWB9RYYHBOUA9NOYSWGALF9MSVNEDW9A9FLGBRWKED' + b'MPEIPRKBMRXRLLFJCAGVIMXPISRGXIJQ9BOBHKJEUKDEUUWYXJGCGAWHYBQHBPMRTZ' + b'FPBGNLMKPZYXZPXFSPFUWZNRWYXUEWMP9URKVVJOSWEPJKSMPLWZPIZGOTVAA9QQOC' + b'YISMGHSBU9YCXZCMSTPJVASDKEVZCSPNSPYOUUWWFTNWZTTZBKGZ9PDNAKNSGNODSB' + b'IRKUGFYCZXIFHQCDTXQNLMKRVKIFJS9XARBNMJQOTDL9CAOKEXQTMWCKWRNHLLMLYP' + b'QGTDFNTDBHNAFRBEUWTKPKPECAADKRPEAFDHABMYYXQPQYDSGFRSRFNHFHHHTAH9YF' + b'OXKRZOTKAHZPRISHZRR9YBVSOZUSKKU9HTCXPTPZFAHFMOQJBKZIACZB9ZRXFPPPMY' + b'RBCWPBAPRFXLQZOTGXJGMZUUEZIAVWXUEN9UIFLEESVCCGNKDISMEPYWTXDQHOSUWZ' + b'OEHOCZQJKDCJJNRZVODVNNUOV9FZQEXFGAMDKMV9PVUYMWTFNISYGYKQG9OKNOQUEK' + b'YDEJ9EGHUXQFCPHTTVBCRTZJLOAWHGDEQHLPLHWTWVOBCCQTCWCNLYDGUV9FKFZENU' + b'NOCOYNU9CYQDSAQDSMZGRQYB9YOCFSOHQXANMSPYFVCTPTZKUGIGUZMPJIJRZVN9VX' + b'ADLIVJYJGQWXBOBBAROGNIOIJVRUHWMFLVCGTMZISADLTXVEZLQDYAVQ9OCYQDPCYL' + b'DD9SUPNTUMUESC9VRSCAYBPXAPTYXZODUUMBNCSLOWJYJA9JBITZLZNHPZFGSPRURJ' + b'HYFLSFTMEPAEG9DWRFOTFWWPGGDGZWFVEPOHDGNMOXUSR9AVQLNDUMGPYWVN9LKEIZ' + b'Q9MNIUPJXPTRYMSXRA9GTSZFMNZZT9Z9HJOKVBCHRRIZZN9UYTVRDNHOXYSFRO9FRK' + b'HWNNZ9DXTLV9D9PNJLGWJXAFUOJZTRVJOLYGSPNVCXYWMTOEEUBLNRGAJPK9HIWGZM' + b'HMBTHLABTACRYLQIDPOYEFNSYQ9YAQPOYYDCJAAAVDUWCHSS9OKQYH9CQNUPYRTCWK' + b'CXYDKTAIJKWOQIHSGBZMFJXGOQDODBDZNBOPFYCBLSU9RJYVGXINUDODGHNAGFDAEG' + b'LPDVSCJPCIZHOFNHCZUTRLQXEZUFDZVROFXVHUNWMYRSZHBZAFCWIY9ULTBTEDSKEC' + b'CLDGAU9MZXYRAXVY9NIQUYHATJCZXDSAELMCXQMALHNFMEAWHIQAZMQOO9QPEPDOYC' + b'OJXTUWEJHMPXGZBXFPNXUPOSDINZJNIREYDFZMESFQUPBKSDGTAJHEZSCOVSLYUAUK' + b'DIWNNLQJTPYYTPRGGN9IXIORWXHJBPYINQZIUXKLKXCTQZYJIRH9MHYBQIFQZCFAKZ' + b'DUUZTYIMTNNVNKVMFIW9UYRTVRQHMYR9Y9VYFTPBJGSB9VINGTMBKVZJEZUE9RMBDZ' + b'CQGDHNPW9YIJHLGFOG9YAPXZECSFVXAMPILBIHC9DBGMIE99YEPGTAALOHBUKXSGFZ' + b'YHHWFOIHMEDXFHIYSUHADOCKFNGHKTNPZHINAWG9YGRJBQGECRCVPXXOG9CNVJNFLC' + b'LMGC9I9HAGTAGGVRKCXDJWDNHYZBFNQSKH99MFAMLGRSBMIBBMHBDJTVSQ99ZHYPSS' + b'XLUNFCNOJXITUETNBHIGXFLLEHUKEXGJLO9BBALXMNGJKTETFIZHSSKLOQXPXOSZRW' + b'QP9A9RIHWEHATMSVMZEQPGAUQBCIAQXZSUUFSU9HYK9RAVASYCVNALKJJXAJF9RLTD' + b'ZEIIYCFLQVMHBPBFHHQNXVEKPHOOFTQEIVB9IXZMTOFBHTGLPWDYVPO9HHBPVWZYEG' + b'IDMK9UPWEJDLIPJSIGFKKCZFRJVDN9ENWADNOTFWZGUDJRRUMFPVXHNAJBBCI9WEDK' + b'RKCQUHRQTCFYFHXPOFBC9BCENMI9HRSIUAKLWEAOUXRBWMHWLGEOCP9NWIJAXODJDS' + b'P9SKEXEDVUGHZAFPNMR9PXD9THOWNWWTDTWTYMDINGC9EBBVUYZRUQDVSIOXAEVGFP' + b'XS9CLTHUESMTDWUJNCZSOIOEJG9WKNAZMDJGMRBXGVMLUAN9IGDVFAESJMXNTMNFND' + b'CAXEBRAU9' + ) + + self.assertEqual( + TryteString.from_trits(key.get_digest_trits()), + + TryteString( + b'ABQXVJNER9MPMXMBPNMFBMDGTXRWSYHNZKGAGUOI' + b'JKOJGZVGHCUXXGFZEMMGDSGWDCKJXO9ILLFAKGGZE' + ), + ) From 6ab683ff7dca0ff97877369b56bbc8f6637bf2b2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 12:59:55 -0500 Subject: [PATCH 158/239] Minor code cleanup. --- iota/commands/extended/get_new_addresses.py | 4 +++- test/commands/extended/get_new_addresses_test.py | 4 +--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index 77daa38..f8026e2 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -34,13 +34,15 @@ class GetNewAddressesRequestFilter(RequestFilter): def __init__(self): super(GetNewAddressesRequestFilter, self).__init__( { + # ``count`` and ``index`` are optional. 'count': f.Type(int) | f.Min(1) | f.Optional(1), 'index': f.Type(int) | f.Min(0), + 'seed': f.Required | Trytes, }, allow_missing_keys = { 'count', 'index', - } + }, ) diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index 830e78c..d02f28e 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -2,11 +2,9 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from unittest import TestCase - import filters as f from filters.test import BaseFilterTestCase -from iota import Address, TryteString +from iota import TryteString from iota.commands.extended.get_new_addresses import GetNewAddressesCommand from iota.filters import Trytes from six import binary_type, text_type From aac5183fe785bdbab365175f9a8afeb6ba07ff15 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 13:53:34 -0500 Subject: [PATCH 159/239] Curl.squeeze now always copies exactly one hash. --- iota/crypto/pycurl.py | 8 +++++--- iota/crypto/signing.py | 34 ++++++++++++++++++++++++---------- iota/types.py | 9 +++++---- test/crypto/signing_test.py | 9 +++++++++ 4 files changed, 43 insertions(+), 17 deletions(-) diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index b8cffb6..96bdbe4 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -87,9 +87,11 @@ def squeeze(self, trits): # can simplify the implementation somewhat. # - # Note that we copy at most len(trits) trits! - length = min(HASH_LENGTH, len(trits)) - trits[0:length] = self._state[0:length] + # Ensure that ``trits`` can hold at least one hash worth of trits. + trits.extend([0] * max(0, HASH_LENGTH - len(trits))) + + # Copy exactly one hash. + trits[0:HASH_LENGTH] = self._state[0:HASH_LENGTH] # One hash worth of trits copied; now transform. self._transform() diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index ca07164..0d0fb1b 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -17,11 +17,16 @@ class KeyGenerator(object): """ Generates signing keys for messages. """ + HASHES_PER_BLOCK = 27 + """ + Number of hashes that make up one block in a signing key. + """ + def __init__(self, seed): # type: (TrytesCompatible) -> None super(KeyGenerator, self).__init__() - self.seed = TryteString(seed, pad=81).as_trits() + self.seed = TryteString(seed) def get_keys(self, start, count=1, step=1, iterations=1): # type: (int, int, int, int) -> List[SigningKey] @@ -112,20 +117,30 @@ def create_generator(self, start=0, step=1, iterations=1): if start < 0: raise ValueError('``start`` cannot be negative.') + if iterations < 1: + raise ValueError('``iterations`` must be >= 1.') + current = start while current >= 0: sponge = self._create_sponge(current) - key = [] + # Multiply by 3 to convert trytes into trits. + block_length = SigningKey.BLOCK_LEN * 3 - for i in range(iterations): - for j in range(27): - # Multiply by 3 because sponge works with trits, but - # ``Seed.LEN`` is a quantity of trytes. - buffer = [0] * HASH_LENGTH # type: MutableSequence[int] + key = [0] * (block_length * iterations) + buffer = [0] * HASH_LENGTH # type: MutableSequence[int] + + for block_seq in range(iterations): + # Squeeze trits from the buffer and append them to the key, one + # hash at a time. + for hash_seq in range(self.HASHES_PER_BLOCK): sponge.squeeze(buffer) - key += buffer + + key_start = (block_seq * block_length) + (hash_seq * HASH_LENGTH) + key_stop = key_start + HASH_LENGTH + + key[key_start:key_stop] = buffer yield SigningKey.from_trits(key) @@ -136,8 +151,7 @@ def _create_sponge(self, index): """ Prepares the Curl sponge for the generator. """ - # :see: http://stackoverflow.com/a/2612990/ - seed = self.seed[:] # type: MutableSequence[int] + seed = self.seed.as_trits() # type: MutableSequence[int] for i in range(index): # Increment each tryte unless/until we overflow. diff --git a/iota/types.py b/iota/types.py index 3f7a4be..357d93a 100644 --- a/iota/types.py +++ b/iota/types.py @@ -454,15 +454,16 @@ def _generate_checksum(self): """ Generates the correct checksum for this address. """ - # Multiply by 3 because AddressChecksum.LEN is number of trytes, - # but Curl returns trits. - checksum_trits = [0] * (AddressChecksum.LEN * 3) # type: MutableSequence[int] + checksum_trits = [] # type: MutableSequence[int] sponge = Curl() sponge.absorb(self.address.as_trits()) sponge.squeeze(checksum_trits) - return TryteString.from_trits(checksum_trits) + # Multiply by 3 to convert trytes into trits. + checksum_length = (AddressChecksum.LEN * 3) + + return TryteString.from_trits(checksum_trits[:checksum_length]) class AddressChecksum(TryteString): diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index d7ef7e8..b252528 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -469,6 +469,15 @@ def test_iterations(self): ], ) + def test_error_iterations_zero(self): + """ + Attempting to generate a key with a number of iterations < 1. + """ + kg = KeyGenerator(seed=b'') + + with self.assertRaises(ValueError): + kg.get_keys(start=0, iterations=0) + def test_generator(self): """ Creating a generator. From fa168a682fb6d302aa4be4edc57b6de9e8790118 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 13:54:21 -0500 Subject: [PATCH 160/239] Renamed ambiguous functions. --- iota/types.py | 4 ++-- test/types_test.py | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/iota/types.py b/iota/types.py index 357d93a..8830d80 100644 --- a/iota/types.py +++ b/iota/types.py @@ -432,7 +432,7 @@ def __init__(self, trytes): # Make the address sans checksum accessible. self.address = self[:self.LEN] # type: TryteString - def is_valid(self): + def is_checksum_valid(self): # type: () -> bool """ Returns whether this address has a valid checksum. @@ -442,7 +442,7 @@ def is_valid(self): return False - def with_checksum(self): + def with_valid_checksum(self): # type: () -> Address """ Returns the address with a valid checksum attached. diff --git a/test/types_test.py b/test/types_test.py index 1bb1448..4ccfdc0 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -682,10 +682,10 @@ def test_checksum_valid(self): b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX' ) - self.assertTrue(addy.is_valid()) + self.assertTrue(addy.is_checksum_valid()) self.assertEqual( - binary_type(addy.with_checksum()), + binary_type(addy.with_valid_checksum()), b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFWYWZRE' b'9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVAFOXM9MUBX' @@ -704,10 +704,10 @@ def test_checksum_invalid(self): trytes + b'IGUKNUNAX' # <- Last tryte s/b 'W'. ) - self.assertFalse(addy.is_valid()) + self.assertFalse(addy.is_checksum_valid()) self.assertEqual( - binary_type(addy.with_checksum()), + binary_type(addy.with_valid_checksum()), b'IGKUOZGEFNSVJXETLIBKRSUZAWMYSVDPMHGQPCETEFNZP' b'XSJLZMBLAWDRLUBWPIPKFNEPADIWMXMYYRKQIGUKNUNAW', @@ -724,10 +724,10 @@ def test_checksum_null(self): addy = Address(trytes) - self.assertFalse(addy.is_valid()) + self.assertFalse(addy.is_checksum_valid()) self.assertEqual( - binary_type(addy.with_checksum()), + binary_type(addy.with_valid_checksum()), b'ZKIUDZXQYQAWSHPKSAATJXPAQZPGYCDCQDRSMWWCGQJNI' b'PCOORMDRNREDUDKBMUYENYTFVUNEWDBAKXMVSDPEKQPMM', From b0fe9aec6159f50feb0f247e23839cfdcb96b496 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 14:20:01 -0500 Subject: [PATCH 161/239] Implemented TryteString.__contains__. --- iota/types.py | 16 ++++++++++++ test/types_test.py | 62 ++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 76 insertions(+), 2 deletions(-) diff --git a/iota/types.py b/iota/types.py index 8830d80..5431108 100644 --- a/iota/types.py +++ b/iota/types.py @@ -279,6 +279,22 @@ def __iter__(self): # :see: http://stackoverflow.com/a/14267935/ return (self._trytes[i:i + 1] for i in range(len(self))) + def __contains__(self, other): + # type: (TrytesCompatible) -> bool + if isinstance(other, TryteString): + return other._trytes in self._trytes + elif isinstance(other, (binary_type, bytearray)): + return other in self._trytes + else: + raise TypeError( + 'Invalid type for TryteString contains check ' + '(expected Union[TryteString, {binary_type}, bytearray], ' + 'actual {type}).'.format( + binary_type = binary_type.__name__, + type = type(other).__name__, + ), + ) + def __getitem__(self, item): # type: (Union[int, slice]) -> TryteString new_trytes = bytearray() diff --git a/test/types_test.py b/test/types_test.py index 4ccfdc0..2e47706 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -78,7 +78,65 @@ def test_comparison_error_wrong_type(self): self.assertFalse(trytes is 'RBTC9D9DCDQAEASBYBCCKBFA') self.assertTrue(trytes is not 'RBTC9D9DCDQAEASBYBCCKBFA') - def test_concatenate(self): + def test_container(self): + """ + Checking whether a TryteString contains a sequence. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + + self.assertTrue(trytes in trytes) + self.assertTrue(TryteString(b'RBTC9D') in trytes) + self.assertTrue(TryteString(b'DQAEAS') in trytes) + self.assertTrue(TryteString(b'CCKBFA') in trytes) + + self.assertFalse(TryteString(b'9RBTC9D9DCDQAEASBYBCCKBFA') in trytes) + self.assertFalse(TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA9') in trytes) + self.assertFalse(TryteString(b'RBTC9D9DCDQA9EASBYBCCKBFA') in trytes) + self.assertFalse(TryteString(b'X') in trytes) + + # Any TrytesCompatible value will work here. + self.assertTrue(b'EASBY' in trytes) + self.assertFalse(b'QQQ' in trytes) + self.assertTrue(bytearray(b'CCKBF') in trytes) + self.assertFalse(b'ZZZ' in trytes) + + def test_container_error_wrong_type(self): + """ + Checking whether a TryteString contains a sequence with an + incompatible type. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + + with self.assertRaises(TypeError): + # Comparing against unicode strings is not allowed because it is + # ambiguous how to encode the unicode string into trits (should + # we treat the unicode string as an ASCII representation, or + # should we encode the unicode value into bytes and convert the + # result into trytes?). + 'RBTC9D9DCDQAEASBYBCCKBFA' in trytes + + with self.assertRaises(TypeError): + # TryteString is not a numeric type, so this makes about as much + # sense as ``16 in b'Hello, world!'``. + 16 in trytes + + with self.assertRaises(TypeError): + # This is too ambiguous. Is this a list of trit values that can + # appar anywhere in the tryte sequence, or does it have to match + # a tryte exactly? + [0, 1, 1, 0, -1, 0] in trytes + + with self.assertRaises(TypeError): + # This makes more sense than the previous example, but for + # consistency, we will not allow checking for trytes inside + # of a TryteString. + [[0, 0, 0], [1, 1, 0]] in trytes + + with self.assertRaises(TypeError): + # Did I miss something? When did we get to DisneyLand? + None in trytes + + def test_concatenation(self): """ Concatenating TryteStrings with TrytesCompatibles. """ @@ -100,7 +158,7 @@ def test_concatenate(self): b'RBTC9D9DCDQAEASBYBCCKBFA', ) - def test_concatenate_error_wrong_type(self): + def test_concatenation_error_wrong_type(self): """ Attempting to concatenate a TryteString with something that is not a TrytesCompatible. From 8074d205731c0477a81dd4ef628f02651b8cd150 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 14:44:08 -0500 Subject: [PATCH 162/239] Added unit test to catch Curl.squeeze regression. --- test/crypto/signing_test.py | 59 +++++++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index b252528..72f7159 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -169,6 +169,65 @@ def test_get_keys_single(self): ], ) + def test_get_keys_long_seed(self): + """ + Generating a SigningKey from a seed longer than 1 hash. + + This catches a regression caused by + :py:meth:`iota.crypto.pycurl.Curl.squeeze` processing the wrong + number of trits. + """ + kg = KeyGenerator( + b'TESTSEED9DONTUSEINPRODUCTION99999ZTRFNBTRBSDIHWKOWCFBOQYQTENWLTG9S' + b'IGVTKTOFGZAIMWHNQWWENEFBAYZXBYWK9QBIWKTMO9MFZIEQVJULQILER9GRDCBLEY' + b'OPLCYJALVJESQMIEZOVOPYYAOLJMIUCGAJLIUFKHTIHZSEOYYLTPHKSURQSWPQEESV' + b'99QM9DUSKSMLSCCDYMDAJIAPGJIHWBROISBLAA9GZFGPPRPHSTVNJMPUWGLTZEZEGQ' + b'HIHMCRZILISRFGVOJMXOYRALR9ZOUAMQXGW9XPFID' + ) + + self.assertListEqual( + kg.get_keys(start=0), + + [ + SigningKey( + b'V9ZUOXOFKFSYMYRUKLXFTMLKBTDGXJWZZSEQINISGCXCPDQ9MFLTIEXXOEKPJPU9TW' + b'WQAOWDNBVHODJSMTULXCDKTBPEBQMWNXRUWY9WPOFQZDOLYXWEABWU9DNFIUEETSHR' + b'RNXXAGENAIQQYDBWIVXFZIKLNSEHVNKZGKJPAFTICTRNXCRDBXBEKBQF9XKS9LSVZU' + b'EOEUSSHWVQBLBUO9CZRNWHFDBPMUSWLZUUZXXFNZMAUVOPWSVWELDLEBORRPMPWWNU' + b'HRBIERMHKKE99ECTJGTGIY9SDSJITHAQVJNABAQYJGKODTDRPVODJFEBR9HBBOKGTU' + b'XQHVGWMIULQN9QTDGZCYCVRH9ULEOWKLQCZXAQCAQWRGNHWFXDXOOIMRAKARXQWYBP' + b'HZGK9ZXRPERLYLNFOCZIQOM9KFMRTXK9CVYITDLW9WHFYSVEPKZCNKZURNLLPCHYKX' + b'OKPOARQN9EIGPMUWSXRBCDGG9UMMJDUGIPPSUZACIASWZ9PFTBHIJKG9YBQXWNZAHA' + b'AANAOMQGMYSVBGFKHMJBHGPGZSRMKCMYBVTG9DCJCUXAPWLITOAKWOSIMTORUIYRAI' + b'9EAUSYORSHMEVAAVPXBDHQKIMLPQU9WGBQLFLXVIAHQWWQJFACPBQYYSTGHEQCVJZQ' + b'CZNQLHIROHBNFKBCDLTOWIGDKMPUBMLTSWKNF9KRYKE9GFFOWBAJTB9VRGRJGSFTRZ' + b'DEMYHHB9ZDMNUPRNKITM9RRDPQCCLHGVKUMRJNJIIOGK9DWZRPUTBA9FLELUCFLMLR' + b'PZNCOVRFOV9EXQHBUYNBHXIWOEE9UJIBEIST9UCMRHIBYROHCTVMFGYYSRLKXRSWFY' + b'XLYGDDI9VLXEAAZE9LNUYSFLQULELTZKE9ZMAAZRZE9WUTDKHPFFILGKYWBRJDOJMQ' + b'XQRQSHMHCUFZFWVPDBXWATDLJYAKDGXXAJBGMOQABEGKADPBRQINBRFSUUMCTYTC9V' + b'TMVTXTCYIGNQWHAESSGDUNKBTJEVXUUJ9OELTSGITAQDLUXJ9WVNVXBHPHVHTQDFUY' + b'QXZRJZTBBHWZPKBFA9NRT9BGDALRCFOGJE9SGKXMHNTHJGJH9AYBBCMTEKGEGASVXM' + b'9S9ZMFIZXADIMFFZHKNWLG9PECBUH9DWIUEYWCSFQXQBAMGNAOXDDOMQA9RSVYGFET' + b'SVNJKYUQEOPDNPJCNUFO9OIHNMBWDIJBSULTWHTHHXY9GECMMMIMVDYMSYCGGZEXDR' + b'NOZJBTZHIEECAJACURD9Z9YGDZIXDAYLJIZCCX9NHYMJIUPLYHOYRQHXXCVOBJOIMD' + b'ROZFIJOBYAFVNN9PKNAHTMYODHQLFBOEDGFAEITDHZXTEMLGQBCWDRZI9PNECLNZEN' + b'BRPWRQGHDI9VJNKUQIWPIZZ99XKLTIQABPWNQZCRUL9LJIBPTFQRAOSJVAATQOONLM' + b'9KBDO9QTSFS99GFKMSOMAIXADL9ISLMJJPB9ECZPWSARYBJFJQQBALFYEPIYLFC99B' + b'JH9IRDFOYR9VSUJEALRAFNIPYFHUKVAZHYKKGTVGHVMKVMTSHSRKKIVKQJHDOAIVIW' + b'JYWCOFPONADIYTQBRUAVNEIYDFTH9FINVATCSBEEVEALIRBXFPZNWZZTHRYLMRPPZJ' + b'SEUQBMSHA9FVSYRRSAGVDT9WWNIHKKRVTHPOZOVUHGGBGBJWXDEYHQMWJMCEMTVYWQ' + b'RLCYUQFJOZVDRAXUVZUPHBBSQBPF9GJGVFCINMQNLIZ9UTIMCDBEHSLYXFGULQSLCT' + b'VLSSZIOOBKMHRXJX9YHDUBYGJVIBGKGCGBCLUFVQIPDFSTYLD9AEUVUQZPDWDIUVZG' + b'FGJFPRXBDBBBFFUFT9XLJLTOXIIYIBMPPVSJZFUJSDOAPBHJOSURGYTMHDOIB9AACW' + b'NPJPYIHWCEFW9GTWLAQSOIBVAUFWSSVLAOCKNBLVTDDQUGSVZYPVZIYDUVZZSOZH9P' + b'AWJJZQ9CY9BQGWXQADPXVZTJHGM9UHZQGYAUIICUX9BKZPLYOYZWIYHNBORMBKNCQA' + b'CIGBDKXAJLEISKOJVFMGCIQIAZBEOBCFXLBLACGYRSTE9YNAPJPPLQZQSIEKOFFWVM' + b'CNRFTNWRGTDQYN9PNJSPBHFQZXKGERMZOEETWSDPOTSRNUYSYQVZUYLMICCUUKJVBE' + b'9PUHIMNYD' + ), + ], + ) + def test_get_keys_multiple(self): """ Generating multiple keys in one go. From a6784028aeaf6c5763aec2133d0e363bffc0af4a Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 16:05:01 -0500 Subject: [PATCH 163/239] Implemented `getNewAddresses`. --- iota/commands/core/attach_to_tangle.py | 2 +- iota/commands/core/find_transactions.py | 8 +- iota/commands/core/get_balances.py | 2 +- iota/commands/core/get_inclusion_states.py | 2 +- iota/commands/core/get_tips.py | 2 +- .../core/get_transactions_to_approve.py | 2 +- iota/commands/core/get_trytes.py | 2 +- iota/commands/extended/get_new_addresses.py | 24 +++- test/__init__.py | 24 +++- test/commands/core/find_transactions_test.py | 4 +- .../extended/get_new_addresses_test.py | 127 +++++++++++++++++- 11 files changed, 181 insertions(+), 18 deletions(-) diff --git a/iota/commands/core/attach_to_tangle.py b/iota/commands/core/attach_to_tangle.py index 59f845d..6173382 100644 --- a/iota/commands/core/attach_to_tangle.py +++ b/iota/commands/core/attach_to_tangle.py @@ -4,9 +4,9 @@ import filters as f +from iota import TransactionId from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes -from iota.types import TransactionId __all__ = [ 'AttachToTangleCommand', diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index 2d53a24..293f4c7 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -4,9 +4,9 @@ import filters as f +from iota import Address, Tag, TransactionId from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes -from iota.types import Address, Tag, TransactionId __all__ = [ 'FindTransactionsCommand', @@ -90,5 +90,9 @@ def _apply(self, value): class FindTransactionsResponseFilter(ResponseFilter): def __init__(self): super(FindTransactionsResponseFilter, self).__init__({ - 'hashes': f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes), + 'hashes': + f.FilterRepeater( + f.ByteString(encoding='ascii') + | Trytes(result_type=TransactionId) + ), }) diff --git a/iota/commands/core/get_balances.py b/iota/commands/core/get_balances.py index 3b497f7..a891f65 100644 --- a/iota/commands/core/get_balances.py +++ b/iota/commands/core/get_balances.py @@ -4,9 +4,9 @@ import filters as f +from iota import Address from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes -from iota.types import Address __all__ = [ 'GetBalancesCommand', diff --git a/iota/commands/core/get_inclusion_states.py b/iota/commands/core/get_inclusion_states.py index 5433d76..b312526 100644 --- a/iota/commands/core/get_inclusion_states.py +++ b/iota/commands/core/get_inclusion_states.py @@ -4,9 +4,9 @@ import filters as f +from iota import TransactionId from iota.commands import FilterCommand, RequestFilter from iota.filters import Trytes -from iota.types import TransactionId __all__ = [ 'GetInclusionStatesCommand', diff --git a/iota/commands/core/get_tips.py b/iota/commands/core/get_tips.py index 41d0c00..d99ca13 100644 --- a/iota/commands/core/get_tips.py +++ b/iota/commands/core/get_tips.py @@ -4,9 +4,9 @@ import filters as f +from iota import Address from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes -from iota.types import Address __all__ = [ 'GetTipsCommand', diff --git a/iota/commands/core/get_transactions_to_approve.py b/iota/commands/core/get_transactions_to_approve.py index 9131c64..32b5e55 100644 --- a/iota/commands/core/get_transactions_to_approve.py +++ b/iota/commands/core/get_transactions_to_approve.py @@ -4,9 +4,9 @@ import filters as f +from iota import TransactionId from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes -from iota.types import TransactionId __all__ = [ 'GetTransactionsToApproveCommand', diff --git a/iota/commands/core/get_trytes.py b/iota/commands/core/get_trytes.py index b0eda14..73d2d08 100644 --- a/iota/commands/core/get_trytes.py +++ b/iota/commands/core/get_trytes.py @@ -4,9 +4,9 @@ import filters as f +from iota import TransactionId from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes -from iota.types import TransactionId __all__ = [ 'GetTrytesCommand', diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index f8026e2..5eefacb 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -5,6 +5,8 @@ import filters as f from iota.commands import FilterCommand, RequestFilter +from iota.commands.core.find_transactions import FindTransactionsCommand +from iota.crypto.addresses import AddressGenerator from iota.filters import Trytes __all__ = [ @@ -27,7 +29,25 @@ def get_response_filter(self): pass def _send_request(self, request): - pass + # Optional parameters. + count = request.get('count') + index = request.get('index') + + # Required parameters. + seed = request['seed'] + + generator = AddressGenerator(seed) + + if count is None: + # Connect to Tangle and find the first address without any + # transactions. + for addy in generator.create_generator(start=index): + response = FindTransactionsCommand(self.adapter)(addresses=[addy]) + + if not response.get('hashes'): + return [addy] + + return generator.get_addresses(start=index, count=count) class GetNewAddressesRequestFilter(RequestFilter): @@ -35,7 +55,7 @@ def __init__(self): super(GetNewAddressesRequestFilter, self).__init__( { # ``count`` and ``index`` are optional. - 'count': f.Type(int) | f.Min(1) | f.Optional(1), + 'count': f.Type(int) | f.Min(1), 'index': f.Type(int) | f.Min(0), 'seed': f.Required | Trytes, diff --git a/test/__init__.py b/test/__init__.py index ebe9d16..230ae2c 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from collections import defaultdict from typing import Dict, List, Optional, Text from iota.adapter import BaseAdapter, BadApiResponse @@ -22,7 +23,7 @@ def __init__(self): # type: (Optional[dict]) -> None super(MockAdapter, self).__init__() - self.responses = {} # type: Dict[Text, dict] + self.responses = defaultdict(list) # type: Dict[Text, List[dict]] self.requests = [] # type: List[dict] def seed_response(self, command, response): @@ -30,8 +31,23 @@ def seed_response(self, command, response): """ 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!'} + + adapter.send_request({'command': 'sayHello'}) + # {'message': 'Hello!'} """ - self.responses[command] = response + self.responses[command].append(response) return self def send_request(self, payload, **kwargs): @@ -42,8 +58,8 @@ def send_request(self, payload, **kwargs): command = payload['command'] try: - response = self.responses[command] - except KeyError: + response = self.responses[command].pop(0) + except (KeyError, IndexError): raise BadApiResponse( message = ( 'Unknown request {command!r} (expected one of: {seeds!r}).'.format( diff --git a/test/commands/core/find_transactions_test.py b/test/commands/core/find_transactions_test.py index 7b7b1a1..e71bb22 100644 --- a/test/commands/core/find_transactions_test.py +++ b/test/commands/core/find_transactions_test.py @@ -480,12 +480,12 @@ def test_search_results(self): { 'hashes': [ - Address( + TransactionId( b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFW' b'YWZRE9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA', ), - Address( + TransactionId( b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', ), diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index d02f28e..505f6c4 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -2,11 +2,14 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TryteString +from iota import Address, TryteString from iota.commands.extended.get_new_addresses import GetNewAddressesCommand from iota.filters import Trytes +from mock import patch from six import binary_type, text_type from test import MockAdapter @@ -52,7 +55,7 @@ def test_pass_optional_parameters_excluded(self): { 'seed': TryteString(self.seed), 'index': None, - 'count': 1, + 'count': None, }, ) @@ -251,3 +254,123 @@ def test_fail_index_too_small(self): 'index': [f.Min.CODE_TOO_SMALL], }, ) + + +# noinspection SpellCheckingInspection +class GetNewAddressesCommandTestCase(TestCase): + def setUp(self): + super(GetNewAddressesCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = GetNewAddressesCommand(self.adapter) + + # Create a few TryteStrings we can reuse across tests. + self.addy1 =\ + Address( + b'ADDYONE999AHHKVD9SBEYWQFNVQSNTGYQSQ9AGWD' + b'JDZKBYCVTODUHFEVVMNMPQMIXOVXVCZRUENAWYNTO' + ) + + self.addy2 =\ + Address( + b'ADDYTWO999AGAQKYXHRMSFAQNPWCIYUYTXPWUEUR' + b'VNZTCTFUPQ9ESTKNSSLLIZWDQISJVEWIJDVGIECXF' + ) + + def test_get_addresses_offline(self): + """ + Generate addresses in offline mode (without filtering used + addresses). + """ + # To speed up the test, we will mock the address generator. + # :py:class:`iota.crypto.addresses.AddressGenerator` already has + # its own test case, so this does not impact the stability of the + # codebase. + # noinspection PyUnusedLocal + def create_generator(ag, start, step=1): + for addy in [self.addy1, self.addy2][start::step]: + yield addy + + with patch( + target = 'iota.crypto.addresses.AddressGenerator.create_generator', + new = create_generator, + ): + response = self.command( + count = 2, + index = 0, + seed = b'TESTSEED9DONTUSEINPRODUCTION99999', + ) + + self.assertListEqual(response, [self.addy1, self.addy2]) + + # No API requests were made. + self.assertListEqual(self.adapter.requests, []) + + def test_get_addresses_online(self): + """ + Generate address in online mode (filtering used addresses). + """ + # Pretend that ``self.addy1`` has already been used, but not + # ``self.addy2``. + self.adapter.seed_response('findTransactions', { + 'duration': 18, + + 'hashes': [ + 'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' + 'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', + ], + }) + + self.adapter.seed_response('findTransactions', { + 'duration': 1, + 'hashes': [], + }) + + # To speed up the test, we will mock the address generator. + # :py:class:`iota.crypto.addresses.AddressGenerator` already has + # its own test case, so this does not impact the stability of the + # codebase. + # noinspection PyUnusedLocal + def create_generator(ag, start, step=1): + for addy in [self.addy1, self.addy2][start::step]: + yield addy + + with patch( + target = 'iota.crypto.addresses.AddressGenerator.create_generator', + new = create_generator, + ): + response = self.command( + # If ``count`` is missing or ``None``, the command will operate + # in online mode. + # count = None, + index = 0, + seed = b'TESTSEED9DONTUSEINPRODUCTION99999', + ) + + # The command determined that ``self.addy1`` was already used, so + # it skipped that one. + self.assertListEqual(response, [self.addy2]) + + self.assertListEqual( + self.adapter.requests, + + # The command issued two `findTransactions` API requests: one for + # each address generated, until it found an unused address. + [ + { + 'command': 'findTransactions', + 'addresses': [self.addy1], + 'approvees': [], + 'bundles': [], + 'tags': [], + }, + + { + 'command': 'findTransactions', + 'addresses': [self.addy2], + 'approvees': [], + 'bundles': [], + 'tags': [], + }, + ], + ) From 54d4ee7a4775e098775b693f9a724ac638578052 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 16:40:56 -0500 Subject: [PATCH 164/239] Implemented random seed generation. --- iota/crypto/types.py | 32 +++++++++++++++++++++++++++++++- iota/types.py | 4 ++-- test/types_test.py | 19 ++++++++++++++++--- 3 files changed, 49 insertions(+), 6 deletions(-) diff --git a/iota/crypto/types.py b/iota/crypto/types.py index d52b178..eb2ae3d 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -2,16 +2,46 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import List +from os import urandom +from typing import Callable, List, Optional from iota import TryteString, TrytesCompatible from iota.crypto import HASH_LENGTH, Curl +from six import binary_type __all__ = [ 'SigningKey', ] +class Seed(TryteString): + """ + A TryteString that acts as a seed for crypto functions. + """ + @classmethod + def random(cls, length=81, source=urandom): + # type: (int, Optional[Callable[[int], binary_type]]) -> Seed + """ + Generates a new random seed. + + :param length: + Number of trytes to generate. + This should be at least 81 (one hash). + + :param source: + CSPRNG function or method to use to generate randomness. + + Note: This parameter must be a function/method that accepts an + int and returns random bytes. + + Example:: + + from Crypto import Random + new_seed = Seed.random(81, source=Random.new().read) + """ + return cls.from_bytes(source(length)) + + class SigningKey(TryteString): """ A TryteString that acts as a signing key, e.g., for generating diff --git a/iota/types.py b/iota/types.py index 5431108..95bc169 100644 --- a/iota/types.py +++ b/iota/types.py @@ -119,10 +119,10 @@ class TryteString(object): IMPORTANT: A TryteString does not represent a numeric value! """ @classmethod - def from_ascii(cls, bytes_): + def from_bytes(cls, bytes_): # type: (Union[binary_type, bytearray]) -> TryteString """ - Creates a TryteString from an ASCII representation. + Creates a TryteString from a sequence of bytes. """ return cls(encode(bytes_, 'trytes')) diff --git a/test/types_test.py b/test/types_test.py index 2e47706..262a694 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from os import urandom from unittest import TestCase from iota import ( @@ -532,15 +533,27 @@ def test_as_trits_multiple_trytes(self): ], ) - def test_from_ascii(self): + def test_from_bytes(self): """ - Converting a sequence of ASCII chars into a TryteString. + Converting a sequence of bytes into a TryteString. """ self.assertEqual( - binary_type(TryteString.from_ascii(b'Hello, IOTA!')), + binary_type(TryteString.from_bytes(b'Hello, IOTA!')), b'RBTC9D9DCDQAEASBYBCCKBFA', ) + def test_from_bytes_random(self): + """ + Generating a TryteString from a sequence of random bytes. + """ + bytes_ = urandom(81) + trytes = TryteString.from_bytes(bytes_) + + # We can't predict exactly what the result will be, but we can at + # least verify that the bytes were correctly interpreted, and no + # errors were generated. + self.assertEqual(trytes.as_bytes(), bytes_) + def test_from_trytes(self): """ Converting a sequence of tryte values into a TryteString. From 36eed55b84804a035998dd80df453631801efffd Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 17:24:13 -0500 Subject: [PATCH 165/239] Added example script to generate addresses. --- examples/address_generator.py | 113 ++++++++++++++++++++++++++++++++++ examples/hello_world.py | 4 +- iota/api.py | 8 ++- iota/crypto/types.py | 6 +- 4 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 examples/address_generator.py diff --git a/examples/address_generator.py b/examples/address_generator.py new file mode 100644 index 0000000..0045a70 --- /dev/null +++ b/examples/address_generator.py @@ -0,0 +1,113 @@ +# coding=utf-8 +""" +Generates a shiny new IOTA address that you can use for transfers! +""" + +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from argparse import ArgumentParser +from getpass import getpass as secure_input +from sys import argv +from typing import Optional, Text + +from iota import __version__, Iota +from iota.crypto.types import Seed +from six import binary_type, moves as compat, text_type + + +def main(uri, index, count): + # type: (Text, int, Optional[int], bool) -> None + seed = get_seed() + + # Create the API instance. + # Note: If ``seed`` is null, a random seed will be generated. + api = Iota(uri, seed) + + # If we generated a random seed, then we need to display it to the + # user, or else they won't be able to use their new addresses! + if not seed: + print('A random seed has been generated. Press return to see it.') + output_seed(api.seed) + + print('Generating addresses. This may take a few minutes...') + print('') + + # Here's where all the magic happens! + for addy in api.get_new_addresses(index, count): + print(binary_type(addy).decode('ascii')) + + print('') + + +def get_seed(): + # type: () -> binary_type + """ + Prompts the user securely for their seed. + """ + print( + 'Enter seed and press return (typing will not be shown). ' + 'If empty, a random seed will be generated and displayed on the screen.' + ) + seed = secure_input('') # type: Text + return seed.encode('ascii') + + +def output_seed(seed): + # type: (Seed) -> None + """ + Outputs the user's seed to stdout, along with lots of warnings + about security. + """ + print( + 'WARNING: Anyone who has your seed can spend your IOTAs! ' + 'Clear the screen after recording your seed!' + ) + compat.input('') + print('Your seed is:') + print('') + print(binary_type(seed).decode('ascii')) + print('') + + print( + 'Clear the screen to prevent shoulder surfing, ' + 'and press return to continue.' + ) + print('https://en.wikipedia.org/wiki/Shoulder_surfing_(computer_security)') + compat.input('') + + +if __name__ == '__main__': + parser = ArgumentParser( + description = __doc__, + epilog = 'PyOTA v{version}'.format(version=__version__), + ) + + parser.add_argument( + '--uri', + type = text_type, + default = 'udp://localhost:14265/', + + help = + 'URI of the node to connect to ' + '(defaults to udp://localhost:14265/).', + ) + + parser.add_argument( + '--index', + type = int, + default = 0, + help = 'Index of the key to generate.', + ) + + parser.add_argument( + '--count', + type = int, + default = None, + + help = + 'Number of addresses to generate. ' + 'If not specified, the first unused address will be returned.' + ) + + main(**vars(parser.parse_args(argv[1:]))) diff --git a/examples/hello_world.py b/examples/hello_world.py index 8dd32d6..7d3c103 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -13,7 +13,7 @@ from typing import Text from requests.exceptions import ConnectionError -from six import text_type as text +from six import text_type from iota import BadApiResponse, StrictIota, __version__ @@ -43,7 +43,7 @@ def main(uri): parser.add_argument( '--uri', - type = text, + type = text_type, default = 'udp://localhost:14265/', help = diff --git a/iota/api.py b/iota/api.py index 910e693..be4d0aa 100644 --- a/iota/api.py +++ b/iota/api.py @@ -4,9 +4,11 @@ from typing import Iterable, List, Optional, Text -from iota import Address, Bundle, Tag, TransactionId, Transfer, TryteString +from iota import Address, Bundle, Tag, TransactionId, Transfer, TryteString, \ + TrytesCompatible from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry +from iota.crypto.types import Seed __all__ = [ 'Iota', @@ -317,7 +319,7 @@ class Iota(StrictIota): - https://github.com/iotaledger/wiki/blob/master/api-proposal.md """ def __init__(self, adapter, seed=None): - # type: (AdapterSpec, Optional[TryteString]) -> None + # type: (AdapterSpec, Optional[TrytesCompatible]) -> None """ :param seed: Seed used to generate new addresses. @@ -327,7 +329,7 @@ def __init__(self, adapter, seed=None): """ super(Iota, self).__init__(adapter) - self.seed = seed + self.seed = Seed(seed) if seed else Seed.random() def get_inputs(self, start=None, end=None, threshold=None): # type: (Optional[int], Optional[int], Optional[int]) -> dict diff --git a/iota/crypto/types.py b/iota/crypto/types.py index eb2ae3d..0cac3ea 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -7,6 +7,7 @@ from iota import TryteString, TrytesCompatible from iota.crypto import HASH_LENGTH, Curl +from math import ceil from six import binary_type __all__ = [ @@ -25,7 +26,7 @@ def random(cls, length=81, source=urandom): Generates a new random seed. :param length: - Number of trytes to generate. + Minimum number of trytes to generate. This should be at least 81 (one hash). :param source: @@ -39,7 +40,8 @@ def random(cls, length=81, source=urandom): from Crypto import Random new_seed = Seed.random(81, source=Random.new().read) """ - return cls.from_bytes(source(length)) + # Encoding bytes -> trytes yields 2 trytes per byte. + return cls.from_bytes(source(ceil(length / 2))) class SigningKey(TryteString): From 2f3642ba9965c8a73343d1f3a475821118c292b6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 17:33:17 -0500 Subject: [PATCH 166/239] Fixed ambiguous function name. --- iota/commands/__init__.py | 4 ++-- iota/commands/extended/broadcast_and_store.py | 2 +- iota/commands/extended/get_new_addresses.py | 2 +- iota/commands/extended/send_trytes.py | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 816c53c..efdef67 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -94,7 +94,7 @@ def __call__(self, **kwargs): if replacement is not None: self.request = replacement - self.response = self._send_request(self.request) + self.response = self._execute(self.request) replacement = self._prepare_response(self.response) if replacement is not None: @@ -113,7 +113,7 @@ def reset(self): self.request = None # type: dict self.response = None # type: dict - def _send_request(self, request): + def _execute(self, request): # type: (dict) -> dict """ Sends the request object to the adapter and returns the response. diff --git a/iota/commands/extended/broadcast_and_store.py b/iota/commands/extended/broadcast_and_store.py index 5093a04..658f53d 100644 --- a/iota/commands/extended/broadcast_and_store.py +++ b/iota/commands/extended/broadcast_and_store.py @@ -26,7 +26,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _send_request(self, request): + def _execute(self, request): bt_command = BroadcastTransactionsCommand( adapter = self.adapter, prepare_request = self.prepare_request, diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index 5eefacb..b74f1b2 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -28,7 +28,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _send_request(self, request): + def _execute(self, request): # Optional parameters. count = request.get('count') index = request.get('index') diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index 77e692c..87fddc1 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -29,7 +29,7 @@ def get_request_filter(self): def get_response_filter(self): pass - def _send_request(self, request): + def _execute(self, request): # Call ``getTransactionsToApprove`` to locate trunk and branch # transactions so that we can attach the bundle to the Tangle. gta_command = GetTransactionsToApproveCommand( From 64edcdb3477c2ffb40d4e638ccd607c2695b0f99 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 21 Dec 2016 18:28:36 -0500 Subject: [PATCH 167/239] Impl'd request validation for `getInputs`. --- iota/api.py | 22 +- iota/commands/extended/get_inputs.py | 80 +++++ iota/commands/extended/get_new_addresses.py | 3 +- test/commands/extended/get_inputs_test.py | 339 ++++++++++++++++++ .../extended/get_new_addresses_test.py | 25 +- 5 files changed, 453 insertions(+), 16 deletions(-) create mode 100644 iota/commands/extended/get_inputs.py create mode 100644 test/commands/extended/get_inputs_test.py diff --git a/iota/api.py b/iota/api.py index be4d0aa..8504d6e 100644 --- a/iota/api.py +++ b/iota/api.py @@ -348,21 +348,37 @@ def get_inputs(self, start=None, end=None, threshold=None): :param end: Starting key index. + If not specified, then this method will not stop until it finds + an unused address. + :param threshold: - Minimum required balance of accumulated inputs. + Stop once the accumulated inputs meet or exceed this amount. :return: Dict with the following keys:: { - 'inputs': , + 'inputs': [ + { + 'address':
, + 'balance':
, + 'keyIndex`: , + }, + ... + ] + 'totalBalance': , } References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs """ - raise NotImplementedError('Not implemented yet.') + return self.getInputs( + seed = self.seed, + start = start, + end = end, + threshold = threshold, + ) def prepare_transfers(self, transfers, inputs=None, change_address=None): # type: (Iterable[Transfer], Optional[Iterable[TransactionId]], Optional[Address]) -> List[TryteString] diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py new file mode 100644 index 0000000..af73861 --- /dev/null +++ b/iota/commands/extended/get_inputs.py @@ -0,0 +1,80 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from iota.commands import FilterCommand, RequestFilter +from iota.crypto.types import Seed +from iota.filters import Trytes + +__all__ = [ + 'GetInputsCommand', +] + + +class GetInputsCommand(FilterCommand): + """ + Executes ``getInputs`` extended API command. + + See :py:meth:`iota.api.Iota.get_inputs` for more info. + """ + command = 'getInputs' + + def get_request_filter(self): + return GetInputsRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + +class GetInputsRequestFilter(RequestFilter): + CODE_INTERVAL_INVALID = 'interval_invalid' + + templates = { + CODE_INTERVAL_INVALID: '``start`` must be <= ``end``', + } + + def __init__(self): + super(GetInputsRequestFilter, self).__init__( + { + # These arguments are optional. + 'end': f.Type(int) | f.Min(0), + 'start': f.Type(int) | f.Min(0) | f.Optional(0), + 'threshold': f.Type(int) | f.Min(0), + + # These arguments are required. + 'seed': f.Required | Trytes(result_type=Seed), + }, + + allow_missing_keys = { + 'end', + 'start', + 'threshold', + } + ) + + def _apply(self, value): + # noinspection PyProtectedMember + filtered = super(GetInputsRequestFilter, self)._apply(value) + + if self._has_errors: + return None + + if (filtered['end'] is not None) and (filtered['start'] > filtered['end']): + return self._invalid_value( + value = filtered['start'], + reason = self.CODE_INTERVAL_INVALID, + sub_key = 'start', + + context = { + 'start': filtered['start'], + 'end': filtered['end'], + }, + ) + + return filtered diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index b74f1b2..5a194b3 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -7,6 +7,7 @@ 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 __all__ = [ @@ -58,7 +59,7 @@ def __init__(self): 'count': f.Type(int) | f.Min(1), 'index': f.Type(int) | f.Min(0), - 'seed': f.Required | Trytes, + 'seed': f.Required | Trytes(result_type=Seed), }, allow_missing_keys = { diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py new file mode 100644 index 0000000..d52b3dd --- /dev/null +++ b/test/commands/extended/get_inputs_test.py @@ -0,0 +1,339 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +import filters as f +from filters.test import BaseFilterTestCase +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 +from test import MockAdapter + + +class GetInputsRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetInputsCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(GetInputsRequestFilterTestCase, self).setUp() + + # Define a few tryte sequences that we can re-use between tests. + self.seed = b'HELLOIOTA' + + def test_pass_happy_path(self): + """ + Request is valid. + """ + request = { + 'seed': Seed(self.seed), + 'start': 0, + 'end': 10, + 'threshold': 100, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + # ``seed`` can be any value that is convertible into a + # TryteString. + 'seed': binary_type(self.seed), + + # These values must still be integers, however. + 'start': 42, + 'end': 86, + 'threshold': 99, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'seed': Seed(self.seed), + 'start': 42, + 'end': 86, + 'threshold': 99, + }, + ) + + def test_pass_optional_parameters_excluded(self): + """ + The request contains only required parameters. + """ + filter_ = self._filter({ + 'seed': Seed(self.seed), + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'seed': Seed(self.seed), + 'start': 0, + 'end': None, + 'threshold': None, + } + ) + + def test_fail_empty_request(self): + """ + The request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'seed': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """ + The request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'seed': Seed(self.seed), + + # Told you I did. Reckless is he. Now, matters are worse. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_seed_null(self): + """ + ``seed`` is null. + """ + self.assertFilterErrors( + { + 'seed': None, + }, + + { + 'seed': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_seed_wrong_type(self): + """ + ``seed`` cannot be converted into a TryteString. + """ + self.assertFilterErrors( + { + 'seed': text_type(self.seed, 'ascii'), + }, + + { + 'seed': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_seed_malformed(self): + """ + ``seed`` has the correct type, but it contains invalid characters. + """ + self.assertFilterErrors( + { + 'seed': b'not valid; seeds can only contain uppercase and "9".', + }, + + { + 'seed': [Trytes.CODE_NOT_TRYTES], + }, + ) + + def test_fail_start_string(self): + """ + ``start`` is a string. + """ + self.assertFilterErrors( + { + # Not valid; it must be an int. + 'start': '0', + + 'seed': Seed(self.seed), + }, + + { + 'start': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_start_float(self): + """ + ``start`` is a float. + """ + self.assertFilterErrors( + { + # Even with an empty fpart, floats are not valid. + # It's gotta be an int. + 'start': 8.0, + + 'seed': Seed(self.seed), + }, + + { + 'start': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_start_too_small(self): + """ + ``start`` is less than 0. + """ + self.assertFilterErrors( + { + 'start': -1, + + 'seed': Seed(self.seed), + }, + + { + 'start': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_end_string(self): + """ + ``end`` is a string. + """ + self.assertFilterErrors( + { + # Not valid; it must be an int. + 'end': '0', + + 'seed': Seed(self.seed), + }, + + { + 'end': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_end_float(self): + """ + ``end`` is a float. + """ + self.assertFilterErrors( + { + # Even with an empty fpart, floats are not valid. + # It's gotta be an int. + 'end': 8.0, + + 'seed': Seed(self.seed), + }, + + { + 'end': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_end_too_small(self): + """ + ``end`` is less than 0. + """ + self.assertFilterErrors( + { + 'end': -1, + + 'seed': Seed(self.seed), + }, + + { + 'end': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_end_occurs_before_start(self): + """ + ``end`` is less than ``start``. + """ + self.assertFilterErrors( + { + 'start': 1, + 'end': 0, + + 'seed': Seed(self.seed), + }, + + { + 'start': [GetInputsRequestFilter.CODE_INTERVAL_INVALID], + }, + ) + + def test_fail_threshold_string(self): + """ + ``threshold`` is a string. + """ + self.assertFilterErrors( + { + # Not valid; it must be an int. + 'threshold': '0', + + 'seed': Seed(self.seed), + }, + + { + 'threshold': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_threshold_float(self): + """ + ``threshold`` is a float. + """ + self.assertFilterErrors( + { + # Even with an empty fpart, floats are not valid. + # It's gotta be an int. + 'threshold': 8.0, + + 'seed': Seed(self.seed), + }, + + { + 'threshold': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_threshold_too_small(self): + """ + ``threshold`` is less than 0. + """ + self.assertFilterErrors( + { + 'threshold': -1, + + 'seed': Seed(self.seed), + }, + + { + 'threshold': [f.Min.CODE_TOO_SMALL], + }, + ) + + +class GetInputsCommandTestCase(TestCase): + def setUp(self): + super(GetInputsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = GetInputsCommand(self.adapter) diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index 505f6c4..e611942 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -6,8 +6,9 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, TryteString +from iota import Address from iota.commands.extended.get_new_addresses import GetNewAddressesCommand +from iota.crypto.types import Seed from iota.filters import Trytes from mock import patch from six import binary_type, text_type @@ -30,7 +31,7 @@ def test_pass_happy_path(self): Request is valid. """ request = { - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), 'index': 1, 'count': 1, } @@ -45,7 +46,7 @@ def test_pass_optional_parameters_excluded(self): Request omits ``index`` and ``count``. """ filter_ = self._filter({ - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), }) self.assertFilterPasses(filter_) @@ -53,7 +54,7 @@ def test_pass_optional_parameters_excluded(self): filter_.cleaned_data, { - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), 'index': None, 'count': None, }, @@ -78,7 +79,7 @@ def test_pass_compatible_types(self): filter_.cleaned_data, { - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), 'index': 100, 'count': 8, }, @@ -102,7 +103,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), 'index': None, 'count': 1, @@ -166,7 +167,7 @@ def test_fail_count_string(self): # Not valid; it must be an int. 'count': '42', - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), }, { @@ -183,7 +184,7 @@ def test_fail_count_float(self): # Not valid, even with an empty fpart; it must be an int. 'count': 42.0, - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), }, { @@ -198,7 +199,7 @@ def test_fail_count_too_small(self): self.assertFilterErrors( { 'count': 0, - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), }, { @@ -215,7 +216,7 @@ def test_fail_index_string(self): # Not valid; it must be an int. 'index': '42', - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), }, { @@ -232,7 +233,7 @@ def test_fail_index_float(self): # Not valid, even with an empty fpart; it must be an int. 'index': 42.0, - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), }, { @@ -247,7 +248,7 @@ def test_fail_index_too_small(self): self.assertFilterErrors( { 'index': -1, - 'seed': TryteString(self.seed), + 'seed': Seed(self.seed), }, { From 9440260e1312557de3188b303660e5563277fa47 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 10:26:34 -0500 Subject: [PATCH 168/239] Implemented `getInputs` (note: needs tests). --- iota/api.py | 15 ++- iota/commands/core/get_balances.py | 2 + iota/commands/extended/get_inputs.py | 129 +++++++++++++++++++--- iota/exceptions.py | 25 +++++ test/commands/core/get_balances_test.py | 44 ++------ test/commands/extended/get_inputs_test.py | 45 ++++++++ 6 files changed, 205 insertions(+), 55 deletions(-) create mode 100644 iota/exceptions.py diff --git a/iota/api.py b/iota/api.py index 8504d6e..e36aa05 100644 --- a/iota/api.py +++ b/iota/api.py @@ -346,13 +346,20 @@ def get_inputs(self, start=None, end=None, threshold=None): Starting key index. :param end: - Starting key index. + 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 + result. If not specified, then this method will not stop until it finds an unused address. :param threshold: - Stop once the accumulated inputs meet or exceed this amount. + 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. :return: Dict with the following keys:: @@ -370,6 +377,10 @@ def get_inputs(self, start=None, end=None, threshold=None): 'totalBalance': , } + :raise: + - :py:class:`iota.adapter.BadApiResponse` if ``threshold`` is not + met. + References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs """ diff --git a/iota/commands/core/get_balances.py b/iota/commands/core/get_balances.py index a891f65..fe08ff8 100644 --- a/iota/commands/core/get_balances.py +++ b/iota/commands/core/get_balances.py @@ -53,6 +53,8 @@ def __init__(self): class GetBalancesResponseFilter(ResponseFilter): def __init__(self): super(GetBalancesResponseFilter, self).__init__({ + 'balances': f.Array | f.FilterRepeater(f.Int), + 'milestone': f.ByteString(encoding='ascii') | Trytes(result_type=Address), }) diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index af73861..7c2d173 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -2,9 +2,16 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Optional + import filters as f +from iota import BadApiResponse from iota.commands import FilterCommand, RequestFilter +from iota.commands.core.find_transactions import FindTransactionsCommand +from iota.commands.core.get_balances import GetBalancesCommand +from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed +from iota.exceptions import with_context from iota.filters import Trytes __all__ = [ @@ -27,16 +34,90 @@ def get_response_filter(self): pass def _execute(self, request): - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) + # Optional parameters. + end = request.get('end') # type: Optional[int] + threshold = request.get('threshold') # type: Optional[int] + + # Required parameters. + start = request['start'] # type: int + seed = request['seed'] # type: Seed + + generator = AddressGenerator(seed) + + # Determine the addresses we will be scanning. + if end is None: + # This is similar to the ``getNewAddresses`` command, except it + # is interested in all the addresses that `getNewAddresses` + # skips. + addresses = [] + for addy in generator.create_generator(start): + ftc_response = FindTransactionsCommand(self.adapter)(addresses=[addy]) + + if ftc_response.get('hashes'): + addresses.append(addy) + else: + break + else: + addresses = generator.get_addresses(start, end - start) + + # Load balances for the addresses that we generated. + gb_response = GetBalancesCommand(self.adapter)(addresses=addresses) + + result = { + 'inputs': [], + 'totalBalance': 0, + } + + threshold_met = threshold is None + + for i, balance in enumerate(gb_response['balances']): + if balance: + result['inputs'].append({ + 'address': addresses[i], + 'balance': balance, + 'keyIndex': start + i, + }) + + result['totalBalance'] += balance + + if (threshold is not None) and (result['totalBalance'] >= threshold): + threshold_met = True + break + + if threshold_met: + return result + else: + # This is an exception case, but note that we attach the result + # to the exception context so that it can be used for + # troubleshooting. + raise with_context( + exc = BadApiResponse( + message = + 'Accumulated balance {balance} is less than threshold {threshold} ' + '(``exc.context["inputs"]`` contains more information).'.format( + threshold = threshold, + balance = result['totalBalance'], + ), + + request = request, + ), + + context = { + 'inputs': result['inputs'], + 'total_balance': result['totalBalance'], + }, + ) class GetInputsRequestFilter(RequestFilter): + MAX_INTERVAL = 500 + CODE_INTERVAL_INVALID = 'interval_invalid' + 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}', } def __init__(self): @@ -63,18 +144,34 @@ def _apply(self, value): filtered = super(GetInputsRequestFilter, self)._apply(value) if self._has_errors: - return None - - if (filtered['end'] is not None) and (filtered['start'] > filtered['end']): - return self._invalid_value( - value = filtered['start'], - reason = self.CODE_INTERVAL_INVALID, - sub_key = 'start', - - context = { - 'start': filtered['start'], - 'end': filtered['end'], - }, - ) + return filtered + + if filtered['end'] is not None: + if filtered['start'] > filtered['end']: + filtered['start'] = self._invalid_value( + value = filtered['start'], + reason = self.CODE_INTERVAL_INVALID, + sub_key = 'start', + + context = { + 'start': filtered['start'], + 'end': filtered['end'], + }, + ) + elif (filtered['end'] - filtered['start']) > self.MAX_INTERVAL: + filtered['end'] = self._invalid_value( + value = filtered['end'], + reason = self.CODE_INTERVAL_TOO_BIG, + sub_key = 'end', + + context = { + 'start': filtered['start'], + 'end': filtered['end'], + }, + + template_vars = { + 'max_interval': self.MAX_INTERVAL, + }, + ) return filtered diff --git a/iota/exceptions.py b/iota/exceptions.py new file mode 100644 index 0000000..eaa71e1 --- /dev/null +++ b/iota/exceptions.py @@ -0,0 +1,25 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + + +def with_context(exc, context): + # type: (Exception, dict) -> Exception + """ + Attaches a ``context`` value to an Exception. + + Before:: + + exc = Exception('Frog blast the vent core!') + exc.context = { ... } + raise exc + + After:: + + raise with_context(Exception('Frog blast the vent core!'), { ... }) + """ + if not hasattr(exc, 'context'): + exc.context = {} + + exc.context.update(context) + return exc diff --git a/test/commands/core/get_balances_test.py b/test/commands/core/get_balances_test.py index 1881436..eb147ad 100644 --- a/test/commands/core/get_balances_test.py +++ b/test/commands/core/get_balances_test.py @@ -236,47 +236,17 @@ def test_fail_threshold_too_big(self): ) +# noinspection SpellCheckingInspection class GetBalancesResponseFilterTestCase(BaseFilterTestCase): filter_type = GetBalancesCommand(MockAdapter()).get_response_filter skip_value_check = True - # noinspection SpellCheckingInspection - def test_no_balances(self): - """Incoming response contains no balances.""" - filter_ = self._filter({ - 'balances': [], - 'duration': 42, - 'milestoneIndex': 128, - - 'milestone': - 'INRTUYSZCWBHGFGGXXPWRWBZACYAFGVRRP9VYEQJ' - 'OHYD9URMELKWAFYFMNTSP9MCHLXRGAFMBOZPZ9999', - }) - - self.assertFilterPasses(filter_) - self.assertDictEqual( - filter_.cleaned_data, - - { - 'balances': [], - 'duration': 42, - 'milestoneIndex': 128, - - 'milestone': - Address( - b'INRTUYSZCWBHGFGGXXPWRWBZACYAFGVRRP9VYEQJ' - b'OHYD9URMELKWAFYFMNTSP9MCHLXRGAFMBOZPZ9999', - ) - } - ) - - # noinspection SpellCheckingInspection - def test_all_balances(self): + def test_balances(self): """ - Incoming response contains balances for all requested addresses. + Typical ``getBalances`` response. """ filter_ = self._filter({ - 'balances': [114544444, 8175737], + 'balances': ['114544444', '0', '8175737'], 'duration': 42, 'milestoneIndex': 128, @@ -290,7 +260,7 @@ def test_all_balances(self): filter_.cleaned_data, { - 'balances': [114544444, 8175737], + 'balances': [114544444, 0, 8175737], 'duration': 42, 'milestoneIndex': 128, @@ -298,6 +268,6 @@ def test_all_balances(self): Address( b'INRTUYSZCWBHGFGGXXPWRWBZACYAFGVRRP9VYEQJ' b'OHYD9URMELKWAFYFMNTSP9MCHLXRGAFMBOZPZ9999', - ) - } + ), + }, ) diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index d52b3dd..041c08f 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -279,6 +279,23 @@ def test_fail_end_occurs_before_start(self): }, ) + def test_fail_interval_too_large(self): + """ + ``end`` is way more than ``start``. + """ + self.assertFilterErrors( + { + 'start': 0, + 'end': GetInputsRequestFilter.MAX_INTERVAL + 1, + + 'seed': Seed(self.seed), + }, + + { + 'end': [GetInputsRequestFilter.CODE_INTERVAL_TOO_BIG], + }, + ) + def test_fail_threshold_string(self): """ ``threshold`` is a string. @@ -337,3 +354,31 @@ def setUp(self): self.adapter = MockAdapter() self.command = GetInputsCommand(self.adapter) + + def test_start_and_end_with_threshold(self): + """ + ``start`` and ``end`` values provided, with ``threshold``. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_start_and_end_no_threshold(self): + """ + ``start`` and ``end`` values provided, no ``threshold``. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_no_end_with_threshold(self): + """ + No ``end`` value provided, with ``threshold``. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_no_end_no_threshold(self): + """ + No ``end`` value provided, no ``threshold``. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') From fa21034826c6e041de264519cbb3ebacde55ea21 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 10:48:56 -0500 Subject: [PATCH 169/239] Impl'd request validation for `getTransfers`. --- iota/api.py | 26 +- iota/commands/extended/get_transfers.py | 101 ++++++ test/commands/extended/get_transfers_test.py | 311 +++++++++++++++++++ 3 files changed, 430 insertions(+), 8 deletions(-) create mode 100644 iota/commands/extended/get_transfers.py create mode 100644 test/commands/extended/get_transfers_test.py diff --git a/iota/api.py b/iota/api.py index e36aa05..ce2b523 100644 --- a/iota/api.py +++ b/iota/api.py @@ -467,17 +467,22 @@ def get_bundle(self, transaction): """ raise NotImplementedError('Not implemented yet.') - def get_transfers(self, indexes=None, inclusion_states=False): - # type: (Optional[Iterable[int]], bool) -> List[Bundle] + def get_transfers(self, start=0, end=None, inclusion_states=False): + # type: (int, Optional[int], bool) -> List[Bundle] """ Returns all transfers associated with the seed. - :param indexes: - If specified, use addresses at these indexes to perform the - search. + :param start: + Starting key index. + + :param end: + 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 + result. - If not provided, _all_ transfers associated with the seed will be - returned. + If not specified, then this method will not stop until it finds + an unused address. :param inclusion_states: Whether to also fetch the inclusion states of the transfers. @@ -491,7 +496,12 @@ def get_transfers(self, indexes=None, inclusion_states=False): References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers """ - raise NotImplementedError('Not implemented yet.') + return self.getTransfers( + seed = self.seed, + start = start, + end = end, + inclusion_states = inclusion_states, + ) def replay_bundle(self, transaction): # type: (TransactionId) -> Bundle diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py new file mode 100644 index 0000000..fec9a68 --- /dev/null +++ b/iota/commands/extended/get_transfers.py @@ -0,0 +1,101 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from iota.commands import FilterCommand, RequestFilter +from iota.crypto.types import Seed +from iota.filters import Trytes + +__all__ = [ + 'GetTransfersCommand', +] + + +class GetTransfersCommand(FilterCommand): + """ + Executes ``getTransfers`` extended API command. + + See :py:meth:`iota.api.Iota.get_transfers` for more info. + """ + command = 'getTransfers' + + def get_request_filter(self): + return GetTransfersRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + +class GetTransfersRequestFilter(RequestFilter): + MAX_INTERVAL = 500 + + CODE_INTERVAL_INVALID = 'interval_invalid' + 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}', + } + + def __init__(self): + super(GetTransfersRequestFilter, self).__init__( + { + # These arguments are optional. + 'end': f.Type(int) | f.Min(0), + 'start': f.Type(int) | f.Min(0) | f.Optional(0), + + 'inclusion_states': f.Type(bool) | f.Optional(False), + + # These arguments are required. + 'seed': f.Required | Trytes(result_type=Seed), + }, + + allow_missing_keys = { + 'end', + 'inclusion_states', + 'start', + }, + ) + + def _apply(self, value): + # noinspection PyProtectedMember + filtered = super(GetTransfersRequestFilter, self)._apply(value) + + if self._has_errors: + return filtered + + if filtered['end'] is not None: + if filtered['start'] > filtered['end']: + filtered['start'] = self._invalid_value( + value = filtered['start'], + reason = self.CODE_INTERVAL_INVALID, + sub_key = 'start', + + context = { + 'start': filtered['start'], + 'end': filtered['end'], + }, + ) + elif (filtered['end'] - filtered['start']) > self.MAX_INTERVAL: + filtered['end'] = self._invalid_value( + value = filtered['end'], + reason = self.CODE_INTERVAL_TOO_BIG, + sub_key = 'end', + + context = { + 'start': filtered['start'], + 'end': filtered['end'], + }, + + template_vars = { + 'max_interval': self.MAX_INTERVAL, + }, + ) + + return filtered diff --git a/test/commands/extended/get_transfers_test.py b/test/commands/extended/get_transfers_test.py new file mode 100644 index 0000000..1dd83bc --- /dev/null +++ b/test/commands/extended/get_transfers_test.py @@ -0,0 +1,311 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from filters.test import BaseFilterTestCase +from iota.commands.extended.get_transfers import GetTransfersCommand, \ + GetTransfersRequestFilter +from iota.crypto.types import Seed +from iota.filters import Trytes +from six import binary_type, text_type +from test import MockAdapter + + +class GetTransfersRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetTransfersCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(GetTransfersRequestFilterTestCase, self).setUp() + + # Define a few tryte sequences that we can re-use between tests. + self.seed = b'HELLOIOTA' + + def test_pass_happy_path(self): + """ + Request is valid. + """ + request = { + 'seed': Seed(self.seed), + 'start': 0, + 'end': 10, + 'inclusion_states': True, + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + The request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + # ``seed`` can be any value that is convertible into a + # TryteString. + 'seed': binary_type(self.seed), + + # These values must still be integers/bools, however. + 'start': 42, + 'end': 86, + 'inclusion_states': True, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'seed': Seed(self.seed), + 'start': 42, + 'end': 86, + 'inclusion_states': True, + }, + ) + + def test_pass_optional_parameters_excluded(self): + """ + The request contains only required parameters. + """ + filter_ = self._filter({ + 'seed': Seed(self.seed), + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'seed': Seed(self.seed), + 'start': 0, + 'end': None, + 'inclusion_states': False, + } + ) + + def test_fail_empty_request(self): + """ + The request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'seed': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """ + The request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'seed': Seed(self.seed), + + # Your rules are really beginning to annoy me. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_seed_null(self): + """ + ``seed`` is null. + """ + self.assertFilterErrors( + { + 'seed': None, + }, + + { + 'seed': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_seed_wrong_type(self): + """ + ``seed`` cannot be converted into a TryteString. + """ + self.assertFilterErrors( + { + 'seed': text_type(self.seed, 'ascii'), + }, + + { + 'seed': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_seed_malformed(self): + """ + ``seed`` has the correct type, but it contains invalid characters. + """ + self.assertFilterErrors( + { + 'seed': b'not valid; seeds can only contain uppercase and "9".', + }, + + { + 'seed': [Trytes.CODE_NOT_TRYTES], + }, + ) + + def test_fail_start_string(self): + """ + ``start`` is a string. + """ + self.assertFilterErrors( + { + # Not valid; it must be an int. + 'start': '0', + + 'seed': Seed(self.seed), + }, + + { + 'start': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_start_float(self): + """ + ``start`` is a float. + """ + self.assertFilterErrors( + { + # Even with an empty fpart, floats are not valid. + # It's gotta be an int. + 'start': 8.0, + + 'seed': Seed(self.seed), + }, + + { + 'start': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_start_too_small(self): + """ + ``start`` is less than 0. + """ + self.assertFilterErrors( + { + 'start': -1, + + 'seed': Seed(self.seed), + }, + + { + 'start': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_end_string(self): + """ + ``end`` is a string. + """ + self.assertFilterErrors( + { + # Not valid; it must be an int. + 'end': '0', + + 'seed': Seed(self.seed), + }, + + { + 'end': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_end_float(self): + """ + ``end`` is a float. + """ + self.assertFilterErrors( + { + # Even with an empty fpart, floats are not valid. + # It's gotta be an int. + 'end': 8.0, + + 'seed': Seed(self.seed), + }, + + { + 'end': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_end_too_small(self): + """ + ``end`` is less than 0. + """ + self.assertFilterErrors( + { + 'end': -1, + + 'seed': Seed(self.seed), + }, + + { + 'end': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_end_occurs_before_start(self): + """ + ``end`` is less than ``start``. + """ + self.assertFilterErrors( + { + 'start': 1, + 'end': 0, + + 'seed': Seed(self.seed), + }, + + { + 'start': [GetTransfersRequestFilter.CODE_INTERVAL_INVALID], + }, + ) + + def test_fail_interval_too_large(self): + """ + ``end`` is way more than ``start``. + """ + self.assertFilterErrors( + { + 'start': 0, + 'end': GetTransfersRequestFilter.MAX_INTERVAL + 1, + + 'seed': Seed(self.seed), + }, + + { + 'end': [GetTransfersRequestFilter.CODE_INTERVAL_TOO_BIG], + }, + ) + + def test_fail_inclusion_states_wrong_type(self): + """ + ``inclusion_states`` is not a boolean. + """ + self.assertFilterErrors( + { + 'inclusion_states': '1', + + 'seed': Seed(self.seed), + }, + + { + 'inclusion_states': [f.Type.CODE_WRONG_TYPE], + }, + ) From 59afbfbfff8e5ffab9db59acd9720e453e2524f6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 14:18:13 -0500 Subject: [PATCH 170/239] Implemented `getInputs` (note: needs unit tests). --- iota/api.py | 8 +- iota/commands/extended/get_inputs.py | 4 +- iota/commands/extended/get_transfers.py | 44 +++- iota/crypto/types.py | 6 +- iota/types.py | 245 +++++++++++------- test/types_test.py | 326 ++++++++++++++++++++++-- 6 files changed, 507 insertions(+), 126 deletions(-) diff --git a/iota/api.py b/iota/api.py index ce2b523..9b1aec3 100644 --- a/iota/api.py +++ b/iota/api.py @@ -4,7 +4,7 @@ from typing import Iterable, List, Optional, Text -from iota import Address, Bundle, Tag, TransactionId, Transfer, TryteString, \ +from iota import Address, Bundle, Tag, TransactionId, Transaction, TryteString, \ TrytesCompatible from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry @@ -392,13 +392,13 @@ def get_inputs(self, start=None, end=None, threshold=None): ) def prepare_transfers(self, transfers, inputs=None, change_address=None): - # type: (Iterable[Transfer], Optional[Iterable[TransactionId]], Optional[Address]) -> List[TryteString] + # type: (Iterable[Transaction], Optional[Iterable[TransactionId]], Optional[Address]) -> List[TryteString] """ Prepares transactions to be broadcast to the Tangle, by generating the correct bundle, as well as choosing and signing the inputs (for value transfers). - :param transfers: Transfer objects to prepare. + :param transfers: Transaction objects to prepare. :param inputs: List of inputs used to fund the transfer. @@ -529,7 +529,7 @@ def send_transfer( change_address = None, min_weight_magnitude = 18, ): - # type: (int, Iterable[Transfer], Optional[Iterable[TransactionId]], Optional[Address], int) -> Bundle + # type: (int, Iterable[Transaction], Optional[Iterable[TransactionId]], Optional[Address], int) -> Bundle """ Prepares a set of transfers and creates the bundle, then attaches the bundle to the Tangle, and broadcasts and stores the diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index 7c2d173..f601a09 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -51,9 +51,9 @@ def _execute(self, request): # skips. addresses = [] for addy in generator.create_generator(start): - ftc_response = FindTransactionsCommand(self.adapter)(addresses=[addy]) + ft_response = FindTransactionsCommand(self.adapter)(addresses=[addy]) - if ftc_response.get('hashes'): + if ft_response.get('hashes'): addresses.append(addy) else: break diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index fec9a68..9d14e25 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -2,8 +2,14 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Optional + import filters as f +from iota import Bundle from iota.commands import FilterCommand, RequestFilter +from iota.commands.core.find_transactions import FindTransactionsCommand +from iota.commands.core.get_trytes import GetTrytesCommand +from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes @@ -27,9 +33,41 @@ def get_response_filter(self): pass def _execute(self, request): - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) + # Optional parameters. + end = request.get('end') # type: Optional[int] + threshold = request.get('threshold') # type: Optional[int] + + # Required parameters. + start = request['start'] # type: int + seed = request['seed'] # type: Seed + + generator = AddressGenerator(seed) + ft_command = FindTransactionsCommand(self.adapter) + + # Determine the addresses we will be scanning, and pull their + # transaction hashes. + if end is None: + # This is similar to the ``getNewAddresses`` command, except it + # is interested in all the addresses that `getNewAddresses` + # skips. + hashes = [] + for addy in generator.create_generator(start): + ft_response = ft_command(addresses=[addy]) + + if ft_response.get('hashes'): + hashes += ft_response['hashes'] + else: + break + else: + ft_response =\ + ft_command(addresses=generator.get_addresses(start, end - start)) + + hashes = ft_response.get('hashes') or [] + + # Fetch the trytes for each transaction. + gt_response = GetTrytesCommand(self.adapter)(hashes=hashes) + + return list(map(Bundle.from_tryte_string, gt_response.get('trytes') or [])) class GetTransfersRequestFilter(RequestFilter): diff --git a/iota/crypto/types.py b/iota/crypto/types.py index 0cac3ea..f51443c 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -5,7 +5,7 @@ from os import urandom from typing import Callable, List, Optional -from iota import TryteString, TrytesCompatible +from iota import Hash, TryteString, TrytesCompatible from iota.crypto import HASH_LENGTH, Curl from math import ceil from six import binary_type @@ -20,7 +20,7 @@ class Seed(TryteString): A TryteString that acts as a seed for crypto functions. """ @classmethod - def random(cls, length=81, source=urandom): + def random(cls, length=Hash.LEN, source=urandom): # type: (int, Optional[Callable[[int], binary_type]]) -> Seed """ Generates a new random seed. @@ -38,7 +38,7 @@ def random(cls, length=81, source=urandom): Example:: from Crypto import Random - new_seed = Seed.random(81, source=Random.new().read) + new_seed = Seed.random(source=Random.new().read) """ # Encoding bytes -> trytes yields 2 trytes per byte. return cls.from_bytes(source(ceil(length / 2))) diff --git a/iota/types.py b/iota/types.py index 95bc169..c397d0f 100644 --- a/iota/types.py +++ b/iota/types.py @@ -4,11 +4,11 @@ from codecs import encode, decode from itertools import chain -from typing import Dict, Generator, Iterable, List, MutableSequence, \ +from typing import Generator, Iterable, List, MutableSequence, \ Optional, Text, Union from iota import TrytesCodec -from iota.crypto import Curl +from iota.crypto import Curl, HASH_LENGTH from six import PY2, binary_type @@ -16,68 +16,33 @@ 'Address', 'AddressChecksum', 'Bundle', + 'BundleId', + 'Hash', 'Tag', + 'Transaction', 'TransactionId', - 'Transfer', 'TryteString', 'TrytesCompatible', 'int_from_trits', 'trits_from_int', - 'trytes_from_int', ] # Custom types for type hints and docstrings. -Bundle = Iterable['Transfer'] +Bundle = Iterable['Transaction'] TrytesCompatible = Union[binary_type, bytearray, 'TryteString'] -def trytes_from_int(n): - # type: (int) -> List[List[int]] +def trits_from_int(n, pad=None): + # type: (int, Optional[int]) -> List[int] """ - Returns a tryte representation of an integer value. - """ - trytes = [] - - while True: - # divmod does weird things if ``n`` is negative. - # :see: http://stackoverflow.com/q/10063546/ - quotient, remainder = divmod(abs(n), 27) - - sign = -1 if n < 0 else 1 - remainder *= sign - quotient *= sign - - if remainder not in _trytes_dict: - trits = trits_from_int(remainder) - # Pad the tryte out to 3 trits if necessary. - trits += [0] * (3 - len(trits)) - - _trytes_dict[remainder] = trits - - trytes.append(_trytes_dict[remainder]) - - if quotient == 0: - break - - n = quotient - - return trytes - -_trytes_dict = {} # type: Dict[int, List[int]] -""" -Caches tryte values for :py:func:`trytes_from_int`. - -There are only 27 possible tryte configurations, so it's a relatively -small amount of memory; the tradeoff is usually worth it for the -reduced CPU load. -""" + Returns a trit representation of an integer value. + :param n: + Integer value to convert. -def trits_from_int(n): - # type: (int) -> List[int] - """ - Returns a trit representation of an integer value. + :param pad: + Ensure the result has at least this many trits. References: - https://dev.to/buntine/the-balanced-ternary-machines-of-soviet-russia @@ -85,16 +50,21 @@ def trits_from_int(n): - https://rosettacode.org/wiki/Balanced_ternary#Python """ if n == 0: - return [] + trits = [] + else: + quotient, remainder = divmod(n, 3) + + if remainder == 2: + # Lend 1 to the next place so we can make this trit negative. + quotient += 1 + remainder = -1 - quotient, remainder = divmod(n, 3) + trits = [remainder] + trits_from_int(quotient) - if remainder == 2: - # Lend 1 to the next place so we can make this trit negative. - quotient += 1 - remainder = -1 + if pad: + trits += [0] * max(0, pad - len(trits)) - return [remainder] + trits_from_int(quotient) + return trits def int_from_trits(trits): @@ -132,8 +102,11 @@ def from_trytes(cls, trytes): """ Creates a TryteString from a sequence of trytes. + :param trytes: + Iterable of tryte values. + In this context, a tryte is defined as a list containing 3 trits. + References: - - :py:func:`trytes_from_int` - :py:meth:`as_trytes` """ chars = bytearray() @@ -150,23 +123,14 @@ def from_trytes(cls, trytes): return cls(chars) @classmethod - def from_trits(cls, trits, pad=False): - # type: (Iterable[int], bool) -> TryteString + def from_trits(cls, trits): + # type: (Iterable[int]) -> TryteString """ Creates a TryteString from a sequence of trits. :param trits: Iterable of trit values (-1, 0, 1). - :param pad: - How to handle a sequence with length not divisible by 3: - - - ``False`` (default): raise a :py:class:`ValueError`. - - ``True``: pad to a valid length, using null trits. - - Note that this parameter behaves differently than in - :py:meth:`__init__`. - References: - :py:func:`int_from_trits` - :py:meth:`as_trits` @@ -175,21 +139,14 @@ def from_trits(cls, trits, pad=False): # method. trits = list(trits) - if pad: - trits += [0] * max(0, 3 - (len(trits) % 3)) - if len(trits) % 3: - raise ValueError( - 'Cannot convert sequence with length {length} to trytes; ' - 'length must be divisible by 3.'.format( - length = len(trits), - ), - ) + # Pad the trits so that it is cleanly divisible into trytes. + trits += [0] * (3 - (len(trits) % 3)) - return cls.from_trytes([ + return cls.from_trytes( # :see: http://stackoverflow.com/a/1751478/ trits[i:i+3] for i in range(0, len(trits), 3) - ]) + ) def __init__(self, trytes, pad=None): # type: (TrytesCompatible, Optional[int]) -> None @@ -418,7 +375,25 @@ def _tryte_from_int(n): if n > 13: n -= 27 - return trytes_from_int(n)[0] + return trits_from_int(n, pad=3) + + +class Hash(TryteString): + """ + A TryteString that is exactly one hash long. + """ + # Divide by 3 to convert trits to trytes. + LEN = HASH_LENGTH // 3 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Hash, self).__init__(trytes, pad=self.LEN) + + if len(self._trytes) > self.LEN: + raise ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )) class Address(TryteString): @@ -426,7 +401,7 @@ class Address(TryteString): A TryteString that acts as an address, with support for generating and validating checksums. """ - LEN = 81 + LEN = Hash.LEN def __init__(self, trytes): # type: (TrytesCompatible) -> None @@ -501,6 +476,13 @@ def __init__(self, trytes): ) +class BundleId(Hash): + """ + A TryteString that acts as a bundle ID. + """ + pass + + class Tag(TryteString): """ A TryteString that acts as a transaction tag. @@ -518,30 +500,97 @@ def __init__(self, trytes): )) -class TransactionId(TryteString): +class TransactionId(Hash): """ A TryteString that acts as a transaction or bundle ID. """ - LEN = 81 - - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(TransactionId, self).__init__(trytes, pad=self.LEN) - - if len(self._trytes) > self.LEN: - raise ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )) + pass -class Transfer(object): +class Transaction(object): """ A message [to be] published to the Tangle. """ - def __init__(self, recipient, value, message=None, tag=None): - # type: (Address, int, Optional[TryteString], Optional[Tag]) -> None + @classmethod + def from_tryte_string(cls, trytes): + # type: (TrytesCompatible) -> Transaction + """ + Creates a Transaction object from a sequence of trytes. + """ + tryte_string = TryteString(trytes) + + hash_ = [0] * HASH_LENGTH # type: MutableSequence[int] + + sponge = Curl() + sponge.absorb(tryte_string.as_trits()) + sponge.squeeze(hash_) + + return cls( + hash_ = Hash.from_trits(hash_), + signature_message_fragment = tryte_string[0:2187], + recipient = Address(tryte_string[2187:2268]), + value = int_from_trits(tryte_string[2268:2295].as_trits()), + tag = Tag(tryte_string[2295:2322]), + timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), + current_index = int_from_trits(tryte_string[2331:2340].as_trits()), + last_index = int_from_trits(tryte_string[2340:2349].as_trits()), + bundle_id = BundleId(tryte_string[2349:2430]), + trunk_transaction_id = TransactionId(tryte_string[2430:2511]), + branch_transaction_id = TransactionId(tryte_string[2511:2592]), + nonce = Hash(tryte_string[2592:2673]), + ) + + def __init__( + self, + hash_, + signature_message_fragment, + recipient, + value, + tag, + timestamp, + current_index, + last_index, + bundle_id, + trunk_transaction_id, + branch_transaction_id, + nonce, + ): + # type: (Hash, TryteString, Address, int, Tag, int, int, int, Hash, TransactionId, TransactionId, Hash) -> None + self.hash = hash_ + self.bundle_id = bundle_id + self.recipient = recipient - self.value = value, - self.message = TryteString(message or b'') - self.tag = Tag(tag or b'') + self.value = value + + self.tag = tag + + self.nonce = nonce + self.timestamp = timestamp + + self.current_index = current_index + self.last_index = last_index + + self.branch_transaction_id = branch_transaction_id + self.trunk_transaction_id = trunk_transaction_id + + self.signature_message_fragment =\ + TryteString(signature_message_fragment or b'') + + def as_tryte_string(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction. + """ + return ( + self.signature_message_fragment + + self.recipient + + TryteString.from_trits(trits_from_int(self.value, pad=81)) + + self.tag + + TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) + + TryteString.from_trits(trits_from_int(self.current_index, pad=27)) + + TryteString.from_trits(trits_from_int(self.last_index, pad=27)) + + self.bundle_id + + self.trunk_transaction_id + + self.branch_transaction_id + + self.nonce + ) diff --git a/test/types_test.py b/test/types_test.py index 262a694..5e4c4f8 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -8,7 +8,10 @@ from iota import ( Address, AddressChecksum, + BundleId, + Hash, Tag, + Transaction, TransactionId, TryteString, TrytesCodec, @@ -626,21 +629,6 @@ def test_from_trits(self): b'RBTC9D9DCDQAEASBYBCCKBFA', ) - def test_from_trits_error_wrong_length(self): - """ - Converting a sequence of trit values with length not divisible by 3 - into a TryteString. - """ - trits = [ - 0, 0, -1, - -1, 1, 0, - -1, 1, -1, - 0, 1, # 0, <- Oops, did you lose something? - ] - - with self.assertRaises(ValueError): - TryteString.from_trits(trits) - def test_from_trits_wrong_length_padded(self): """ Automatically padding a sequence of trit values with length not @@ -654,7 +642,7 @@ def test_from_trits_wrong_length_padded(self): ] self.assertEqual( - binary_type(TryteString.from_trits(trits, pad=True)), + binary_type(TryteString.from_trits(trits)), b'RBTC', ) @@ -877,3 +865,309 @@ def test_init_error_too_long(self): b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' ) + + +class TransactionTestCase(TestCase): + # noinspection SpellCheckingInspection + def test_from_tryte_string(self): + """ + Initializing a Transaction object from a TryteString. + """ + # :see: http://iotasupport.com/news/index.php/2016/12/02/fixing-the-latest-solid-subtangle-milestone-issue/ + trytes = ( + 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' + ) + + transaction = Transaction.from_tryte_string(trytes) + + self.assertIsInstance(transaction, Transaction) + + self.assertEqual( + transaction.hash, + + Hash( + b'QODOAEJHCFUYFTTPRONYSMMSFDNFWFX9UCMESVWA' + b'FCVUQYOIJGJMBMGQSFIAFQFMVECYIFXHRGHHEOTMK' + ), + ) + + self.assertEqual( + transaction.signature_message_fragment, + + TryteString( + 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'999999999' + ), + ) + + self.assertEqual( + transaction.recipient, + + Address( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + ) + + self.assertEqual(transaction.value, 0) + self.assertEqual(transaction.tag, Tag(b'999999999999999999999999999')) + self.assertEqual(transaction.timestamp, 1480690413) + self.assertEqual(transaction.current_index, 1) + self.assertEqual(transaction.last_index, 1) + + self.assertEqual( + transaction.bundle_id, + + BundleId( + b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' + b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' + ), + ) + + self.assertEqual( + transaction.trunk_transaction_id, + + TransactionId( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + ) + + self.assertEqual( + transaction.branch_transaction_id, + + TransactionId( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + ) + + self.assertEqual( + transaction.nonce, + + Hash( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + ) + + def test_from_tryte_string_error_too_short(self): + """ + Attempting to create a Transaction from a TryteString that is too + short. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_from_tryte_string_error_too_long(self): + """ + Attempting to create a Transaction from a TryteString that is too + long. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + # noinspection SpellCheckingInspection + def test_as_tryte_string(self): + """ + Converting a Transaction into a TryteString. + """ + transaction = Transaction( + hash_ = + Hash( + b'QODOAEJHCFUYFTTPRONYSMMSFDNFWFX9UCMESVWA' + b'FCVUQYOIJGJMBMGQSFIAFQFMVECYIFXHRGHHEOTMK' + ), + + signature_message_fragment = + TryteString( + 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'999999999' + ), + + recipient = + Address( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + + value = 0, + tag = Tag(b'999999999999999999999999999'), + timestamp = 1480690413, + current_index = 1, + last_index = 1, + + bundle_id = + BundleId( + b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' + b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' + ), + + trunk_transaction_id = + TransactionId( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + + branch_transaction_id = + TransactionId( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + + nonce = + Hash( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + ) + + self.assertEqual( + transaction.as_tryte_string(), + + 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', + ) From a2314992303353087645dbde65f0baa24621123f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 16:06:24 -0500 Subject: [PATCH 171/239] Cleanup and new features. - Renamed `TransactionId` to `TransactionHash`. - Implemented `getLatestInclusion` (note: needs unit tests). - Wired up `getBalances` command. - Added unit tests to verify that commands are wired up correctly. - `getNodeInfo` now returns `TransactionHash` objects instead of generic TryteStrings. - Implemented more of `getTransfers` (note: needs `getBundle`). - `Transaction.hash` is now a `TransactionHash` instead of a generic TryteString. - Added `Transaction.is_tail`. --- iota/api.py | 34 +++++-- iota/commands/core/attach_to_tangle.py | 6 +- iota/commands/core/find_transactions.py | 8 +- iota/commands/core/get_balances.py | 2 + iota/commands/core/get_inclusion_states.py | 6 +- iota/commands/core/get_node_info.py | 7 +- .../core/get_transactions_to_approve.py | 6 +- iota/commands/core/get_trytes.py | 4 +- .../commands/extended/get_latest_inclusion.py | 56 +++++++++++ iota/commands/extended/get_transfers.py | 64 ++++++++++-- iota/types.py | 32 ++++-- test/commands/core/add_neighbors_test.py | 19 ++++ test/commands/core/attach_to_tangle_test.py | 96 ++++++++++-------- .../core/broadcast_transactions_test.py | 20 +++- test/commands/core/find_transactions_test.py | 68 ++++++++----- test/commands/core/get_balances_test.py | 20 +++- .../core/get_inclusion_states_test.py | 60 ++++++++---- test/commands/core/get_neighbors_test.py | 19 ++++ test/commands/core/get_node_info_test.py | 24 ++++- test/commands/core/get_tips_test.py | 20 +++- .../core/get_transactions_to_approve_test.py | 24 ++++- test/commands/core/get_trytes_test.py | 34 +++++-- .../interrupt_attaching_to_tangle_test.py | 19 ++++ test/commands/core/remove_neighbors_test.py | 19 ++++ test/commands/core/store_transactions_test.py | 20 +++- .../extended/broadcast_and_store_test.py | 11 ++- test/commands/extended/get_inputs_test.py | 10 ++ .../extended/get_latest_inclusion_test.py | 97 +++++++++++++++++++ .../extended/get_new_addresses_test.py | 11 ++- test/commands/extended/get_transfers_test.py | 22 +++++ test/commands/extended/send_trytes_test.py | 27 ++++-- test/filters_test.py | 8 +- test/types_test.py | 20 ++-- 33 files changed, 724 insertions(+), 169 deletions(-) create mode 100644 iota/commands/extended/get_latest_inclusion.py create mode 100644 test/commands/extended/get_latest_inclusion_test.py diff --git a/iota/api.py b/iota/api.py index 9b1aec3..3ab7620 100644 --- a/iota/api.py +++ b/iota/api.py @@ -4,7 +4,7 @@ from typing import Iterable, List, Optional, Text -from iota import Address, Bundle, Tag, TransactionId, Transaction, TryteString, \ +from iota import Address, Bundle, Tag, TransactionHash, Transaction, TryteString, \ TrytesCompatible from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry @@ -80,7 +80,7 @@ def attach_to_tangle( trytes, min_weight_magnitude = 18, ): - # type: (TransactionId, TransactionId, Iterable[TryteString], int) -> dict + # type: (TransactionHash, TransactionHash, Iterable[TryteString], int) -> dict """ Attaches the specified transactions (trytes) to the Tangle by doing Proof of Work. You need to supply branchTransaction as well as @@ -122,7 +122,7 @@ def find_transactions( tags = None, approvees = None, ): - # type: (Optional[Iterable[TransactionId]], Optional[Iterable[Address]], Optional[Iterable[Tag]], Optional[Iterable[TransactionId]]) -> dict + # type: (Optional[Iterable[TransactionHash]], Optional[Iterable[Address]], Optional[Iterable[Tag]], Optional[Iterable[TransactionHash]]) -> dict """ Find the transactions which match the specified input and return. @@ -182,7 +182,7 @@ def get_balances(self, addresses, threshold=100): ) def get_inclusion_states(self, transactions, tips): - # type: (Iterable[TransactionId], Iterable[TransactionId]) -> dict + # type: (Iterable[TransactionHash], Iterable[TransactionHash]) -> dict """ Get the inclusion states of a set of transactions. This is for determining if a transaction was accepted and confirmed by the @@ -259,7 +259,7 @@ def get_transactions_to_approve(self, depth): return self.getTransactionsToApprove(depth=depth) def get_trytes(self, hashes): - # type: (Iterable[TransactionId]) -> dict + # type: (Iterable[TransactionHash]) -> dict """ Returns the raw transaction data (trytes) of one or more transactions. @@ -392,7 +392,7 @@ def get_inputs(self, start=None, end=None, threshold=None): ) def prepare_transfers(self, transfers, inputs=None, change_address=None): - # type: (Iterable[Transaction], Optional[Iterable[TransactionId]], Optional[Address]) -> List[TryteString] + # type: (Iterable[Transaction], Optional[Iterable[TransactionHash]], Optional[Address]) -> List[TryteString] """ Prepares transactions to be broadcast to the Tangle, by generating the correct bundle, as well as choosing and signing the inputs (for @@ -421,6 +421,22 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): """ raise NotImplementedError('Not implemented yet.') + def get_latest_inclusion(self, hashes): + # type: (Iterable[TransactionHash]) -> Dict[TransactionHash, bool] + """ + Fetches the inclusion state for the specified transaction hashes, + as of the latest milestone that the node has processed. + + Effectively, this is ``getNodeInfo`` + ``getInclusionStates``. + + :param hashes: + Iterable of transaction hashes. + + :return: + {: } + """ + return self.getLatestInclusion(hashes=hashes) + def get_new_addresses(self, index=None, count=1): # type: (Optional[int], Optional[int]) -> List[Address] """ @@ -449,7 +465,7 @@ def get_new_addresses(self, index=None, count=1): return self.getNewAddresses(seed=self.seed, index=index, count=count) def get_bundle(self, transaction): - # type: (TransactionId) -> List[Bundle] + # type: (TransactionHash) -> List[Bundle] """ Returns the bundle associated with the specified transaction hash. @@ -504,7 +520,7 @@ def get_transfers(self, start=0, end=None, inclusion_states=False): ) def replay_bundle(self, transaction): - # type: (TransactionId) -> Bundle + # type: (TransactionHash) -> Bundle """ Takes a tail transaction hash as input, gets the bundle associated with the transaction and then replays the bundle by attaching it to @@ -529,7 +545,7 @@ def send_transfer( change_address = None, min_weight_magnitude = 18, ): - # type: (int, Iterable[Transaction], Optional[Iterable[TransactionId]], Optional[Address], int) -> Bundle + # type: (int, Iterable[Transaction], Optional[Iterable[TransactionHash]], Optional[Address], int) -> Bundle """ Prepares a set of transfers and creates the bundle, then attaches the bundle to the Tangle, and broadcasts and stores the diff --git a/iota/commands/core/attach_to_tangle.py b/iota/commands/core/attach_to_tangle.py index 6173382..4d4c3e5 100644 --- a/iota/commands/core/attach_to_tangle.py +++ b/iota/commands/core/attach_to_tangle.py @@ -4,7 +4,7 @@ import filters as f -from iota import TransactionId +from iota import TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes @@ -32,8 +32,8 @@ class AttachToTangleRequestFilter(RequestFilter): def __init__(self): super(AttachToTangleRequestFilter, self).__init__( { - 'trunk_transaction': f.Required | Trytes(result_type=TransactionId), - 'branch_transaction': f.Required | Trytes(result_type=TransactionId), + 'trunk_transaction': f.Required | Trytes(result_type=TransactionHash), + 'branch_transaction': f.Required | Trytes(result_type=TransactionHash), 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index 293f4c7..5584500 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -4,7 +4,7 @@ import filters as f -from iota import Address, Tag, TransactionId +from iota import Address, Tag, TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes @@ -46,13 +46,13 @@ def __init__(self): 'approvees': ( f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash)) | f.Optional(default=[]) ), 'bundles': ( f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash)) | f.Optional(default=[]) ), @@ -93,6 +93,6 @@ def __init__(self): 'hashes': f.FilterRepeater( f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionId) + | Trytes(result_type=TransactionHash) ), }) diff --git a/iota/commands/core/get_balances.py b/iota/commands/core/get_balances.py index fe08ff8..89e500d 100644 --- a/iota/commands/core/get_balances.py +++ b/iota/commands/core/get_balances.py @@ -19,6 +19,8 @@ class GetBalancesCommand(FilterCommand): See :py:meth:`iota.api.StrictIota.get_balances`. """ + command = 'getBalances' + def get_request_filter(self): return GetBalancesRequestFilter() diff --git a/iota/commands/core/get_inclusion_states.py b/iota/commands/core/get_inclusion_states.py index b312526..38a7c85 100644 --- a/iota/commands/core/get_inclusion_states.py +++ b/iota/commands/core/get_inclusion_states.py @@ -4,7 +4,7 @@ import filters as f -from iota import TransactionId +from iota import TransactionHash from iota.commands import FilterCommand, RequestFilter from iota.filters import Trytes @@ -34,12 +34,12 @@ def __init__(self): 'transactions': ( f.Required | f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash)) ), 'tips': ( f.Required | f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash)) ), }) diff --git a/iota/commands/core/get_node_info.py b/iota/commands/core/get_node_info.py index f398ecf..b642b93 100644 --- a/iota/commands/core/get_node_info.py +++ b/iota/commands/core/get_node_info.py @@ -3,7 +3,7 @@ unicode_literals import filters as f - +from iota import TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes @@ -37,8 +37,9 @@ def __init__(self): class GetNodeInfoResponseFilter(ResponseFilter): def __init__(self): super(GetNodeInfoResponseFilter, self).__init__({ - 'latestMilestone': f.ByteString(encoding='ascii') | Trytes, + 'latestMilestone': + f.ByteString(encoding='ascii') | Trytes(result_type=TransactionHash), 'latestSolidSubtangleMilestone': - f.ByteString(encoding='ascii') | Trytes, + f.ByteString(encoding='ascii') | Trytes(result_type=TransactionHash), }) diff --git a/iota/commands/core/get_transactions_to_approve.py b/iota/commands/core/get_transactions_to_approve.py index 32b5e55..11b6531 100644 --- a/iota/commands/core/get_transactions_to_approve.py +++ b/iota/commands/core/get_transactions_to_approve.py @@ -4,7 +4,7 @@ import filters as f -from iota import TransactionId +from iota import TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes @@ -40,11 +40,11 @@ def __init__(self): super(GetTransactionsToApproveResponseFilter, self).__init__({ 'branchTransaction': ( f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionId) + | Trytes(result_type=TransactionHash) ), 'trunkTransaction': ( f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionId) + | Trytes(result_type=TransactionHash) ), }) diff --git a/iota/commands/core/get_trytes.py b/iota/commands/core/get_trytes.py index 73d2d08..2569d73 100644 --- a/iota/commands/core/get_trytes.py +++ b/iota/commands/core/get_trytes.py @@ -4,7 +4,7 @@ import filters as f -from iota import TransactionId +from iota import TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes @@ -34,7 +34,7 @@ def __init__(self): 'hashes': ( f.Required | f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionId)) + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash)) ), }) diff --git a/iota/commands/extended/get_latest_inclusion.py b/iota/commands/extended/get_latest_inclusion.py new file mode 100644 index 0000000..4d42a35 --- /dev/null +++ b/iota/commands/extended/get_latest_inclusion.py @@ -0,0 +1,56 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from typing import List + +import filters as f +from iota import TransactionHash +from iota.commands import FilterCommand, RequestFilter +from iota.commands.core.get_inclusion_states import GetInclusionStatesCommand +from iota.commands.core.get_node_info import GetNodeInfoCommand +from iota.filters import Trytes + +__all__ = [ + 'GetLatestInclusionCommand', +] + + +class GetLatestInclusionCommand(FilterCommand): + """ + Executes ``getLatestInclusion`` extended API command. + + See :py:meth:`iota.api.Iota.get_latest_inclusion` for more info. + """ + command = 'getLatestInclusion' + + def get_request_filter(self): + return GetLatestInclusionRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + hashes = request['hashes'] # type: List[TransactionHash] + + gni_response = GetNodeInfoCommand(self.adapter)() + + gis_response = GetInclusionStatesCommand(self.adapter)( + transactions = hashes, + tips = [gni_response['latestSolidSubtangleMilestone']], + ) + + return { + 'states': dict(zip(hashes, gis_response['states'])), + } + + +class GetLatestInclusionRequestFilter(RequestFilter): + def __init__(self): + super(GetLatestInclusionRequestFilter, self).__init__({ + 'hashes': ( + f.Required + | f.Array + | f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash)) + ), + }) diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index 9d14e25..18adc63 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -2,13 +2,15 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Optional +from typing import List, Optional import filters as f -from iota import Bundle +from iota import Transaction from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.commands.core.get_trytes import GetTrytesCommand +from iota.commands.extended.get_latest_inclusion import \ + GetLatestInclusionCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes @@ -34,8 +36,8 @@ def get_response_filter(self): def _execute(self, request): # Optional parameters. - end = request.get('end') # type: Optional[int] - threshold = request.get('threshold') # type: Optional[int] + end = request.get('end') # type: Optional[int] + inclusion_states = request.get('inclusion_states', False) # type: bool # Required parameters. start = request['start'] # type: int @@ -64,10 +66,58 @@ def _execute(self, request): hashes = ft_response.get('hashes') or [] - # Fetch the trytes for each transaction. - gt_response = GetTrytesCommand(self.adapter)(hashes=hashes) + # Sort transactions into tail and non-tail. + tails = set() + non_tails = set() - return list(map(Bundle.from_tryte_string, gt_response.get('trytes') or [])) + transactions = self._find_transactions(hashes=hashes) + + for t in transactions: + if t.is_tail: + tails.add(t.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(t.bundle_id) + + if non_tails: + for t in self._find_transactions(bundles=non_tails): + if t.is_tail: + tails.add(t.hash) + + # Attach inclusion states, if requested. + if inclusion_states: + gli_response = GetLatestInclusionCommand(self.adapter)( + hashes = tails, + ) + + for t in transactions: + t.is_confirmed = gli_response['states'].get(t.hash) + + # :todo: Invoke getBundle. + # :todo: Sort bundles by timestamp and return. + + + def _find_transactions(self, **kwargs): + # type: (dict) -> List[Transaction] + """ + Finds transactions matching the specified criteria, fetches the + corresponding trytes and converts them into Transaction objects. + """ + ft_response = FindTransactionsCommand(self.adapter)(**kwargs) + + hashes = ft_response.get('hashes') or [] + + if hashes: + gt_response = GetTrytesCommand(self.adapter)(hashes=hashes) + + return list(map( + Transaction.from_tryte_string, + gt_response.get('trytes') or [], + )) # type: List[Transaction] + + return [] class GetTransfersRequestFilter(RequestFilter): diff --git a/iota/types.py b/iota/types.py index c397d0f..735edb2 100644 --- a/iota/types.py +++ b/iota/types.py @@ -20,7 +20,7 @@ 'Hash', 'Tag', 'Transaction', - 'TransactionId', + 'TransactionHash', 'TryteString', 'TrytesCompatible', 'int_from_trits', @@ -500,9 +500,9 @@ def __init__(self, trytes): )) -class TransactionId(Hash): +class TransactionHash(Hash): """ - A TryteString that acts as a transaction or bundle ID. + A TryteString that acts as a transaction hash. """ pass @@ -526,7 +526,7 @@ def from_tryte_string(cls, trytes): sponge.squeeze(hash_) return cls( - hash_ = Hash.from_trits(hash_), + hash_ = TransactionHash.from_trits(hash_), signature_message_fragment = tryte_string[0:2187], recipient = Address(tryte_string[2187:2268]), value = int_from_trits(tryte_string[2268:2295].as_trits()), @@ -535,8 +535,8 @@ def from_tryte_string(cls, trytes): current_index = int_from_trits(tryte_string[2331:2340].as_trits()), last_index = int_from_trits(tryte_string[2340:2349].as_trits()), bundle_id = BundleId(tryte_string[2349:2430]), - trunk_transaction_id = TransactionId(tryte_string[2430:2511]), - branch_transaction_id = TransactionId(tryte_string[2511:2592]), + trunk_transaction_id = TransactionHash(tryte_string[2430:2511]), + branch_transaction_id = TransactionHash(tryte_string[2511:2592]), nonce = Hash(tryte_string[2592:2673]), ) @@ -555,7 +555,7 @@ def __init__( branch_transaction_id, nonce, ): - # type: (Hash, TryteString, Address, int, Tag, int, int, int, Hash, TransactionId, TransactionId, Hash) -> None + # type: (Hash, TryteString, Address, int, Tag, int, int, int, Hash, TransactionHash, TransactionHash, Hash) -> None self.hash = hash_ self.bundle_id = bundle_id @@ -576,6 +576,24 @@ def __init__( self.signature_message_fragment =\ TryteString(signature_message_fragment or b'') + self.is_confirmed = None # type: Optional[bool] + """ + Whether this transaction has been confirmed by neighbor nodes. + Must be set manually via the ``getInclusionStates`` API command. + + References: + - :py:meth:`iota.api.StrictIota.get_inclusion_states` + - :py:meth:`iota.api.Iota.get_transfers` + """ + + @property + def is_tail(self): + # type: () -> bool + """ + Returns whether this transaction is a tail. + """ + return self.current_index == 0 + def as_tryte_string(self): # type: () -> TryteString """ diff --git a/test/commands/core/add_neighbors_test.py b/test/commands/core/add_neighbors_test.py index b7dc65e..f59646f 100644 --- a/test/commands/core/add_neighbors_test.py +++ b/test/commands/core/add_neighbors_test.py @@ -2,8 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase +from iota import Iota from iota.commands.core.add_neighbors import AddNeighborsCommand from iota.filters import NodeUri from test import MockAdapter @@ -123,3 +126,19 @@ def test_fail_uris_contents_invalid(self): 'uris.6': [f.Type.CODE_WRONG_TYPE], }, ) + + +class AddNeighborsCommandTestCase(TestCase): + def setUp(self): + super(AddNeighborsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).addNeighbors, + AddNeighborsCommand, + ) diff --git a/test/commands/core/attach_to_tangle_test.py b/test/commands/core/attach_to_tangle_test.py index d2a8921..1da65a2 100644 --- a/test/commands/core/attach_to_tangle_test.py +++ b/test/commands/core/attach_to_tangle_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TransactionId, TryteString +from iota import Iota, TransactionHash, TryteString from iota.commands.core.attach_to_tangle import AttachToTangleCommand from iota.filters import Trytes from six import binary_type, text_type @@ -33,11 +35,11 @@ def setUp(self): def test_pass_happy_path(self): """The incoming request is valid.""" request = { - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), 'min_weight_magnitude': 20, - 'trytes': [ + 'trytes': [ TryteString(self.trytes1), TryteString(self.trytes2), ], @@ -51,10 +53,10 @@ def test_pass_happy_path(self): def test_pass_min_weight_magnitude_missing(self): """`min_weight_magnitude` is optional.""" request = { - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), - 'trytes': [ + 'trytes': [ TryteString(self.trytes1) ], @@ -73,7 +75,7 @@ def test_pass_min_weight_magnitude_missing(self): def test_pass_compatible_types(self): """Incoming values can be converted into the expected types.""" filter_ = self._filter({ - # Any value that can be converted into a TransactionId is valid + # Any value that can be converted into a TransactionHash is valid # here. 'trunk_transaction': binary_type(self.txn_id), 'branch_transaction': bytearray(self.txn_id), @@ -84,7 +86,7 @@ def test_pass_compatible_types(self): binary_type(self.trytes1), # This is probably wrong, but technically it's valid. - TransactionId( + TransactionHash( b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA', ), ], @@ -100,11 +102,11 @@ def test_pass_compatible_types(self): # After running through the filter, all of the values have been # converted to the correct types. { - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), 'min_weight_magnitude': 30, - 'trytes': [ + 'trytes': [ TryteString(self.trytes1), TryteString( @@ -131,8 +133,8 @@ def test_fail_unexpected_parameters(self): """The incoming request contains unexpected parameters.""" self.assertFilterErrors( { - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), 'min_weight_magnitude': 20, 'trytes': [TryteString(self.trytes1)], @@ -151,7 +153,7 @@ def test_fail_trunk_transaction_null(self): { 'trunk_transaction': None, - 'branch_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), 'trytes': [TryteString(self.trytes1)], }, @@ -167,7 +169,7 @@ def test_fail_trunk_transaction_wrong_type(self): # Strings are not valid tryte sequences. 'trunk_transaction': text_type(self.txn_id, 'ascii'), - 'branch_transaction': TransactionId(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), 'trytes': [TryteString(self.trytes1)], }, @@ -182,7 +184,7 @@ def test_fail_branch_transaction_null(self): { 'branch_transaction': None, - 'trunk_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), 'trytes': [TryteString(self.trytes1)], }, @@ -198,7 +200,7 @@ def test_fail_branch_transaction_wrong_type(self): # Strings are not valid tryte sequences. 'branch_transaction': text_type(self.txn_id, 'ascii'), - 'trunk_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), 'trytes': [TryteString(self.trytes1)], }, @@ -214,10 +216,10 @@ def test_fail_min_weight_magnitude_float(self): # I don't care if the fpart is empty; it's still not an int! 'min_weight_magnitude': 20.0, - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), - 'trytes': [ + 'trytes': [ TryteString(self.trytes1) ], }, @@ -234,10 +236,10 @@ def test_fail_min_weight_magnitude_string(self): # For want of an int cast, the transaction was lost. 'min_weight_magnitude': '20', - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), - 'trytes': [ + 'trytes': [ TryteString(self.trytes1) ], }, @@ -253,10 +255,10 @@ def test_fail_min_weight_magnitude_too_small(self): { 'min_weight_magnitude': 17, - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), - 'trytes': [ + 'trytes': [ TryteString(self.trytes1) ], }, @@ -270,10 +272,10 @@ def test_fail_trytes_null(self): """`trytes` is null.""" self.assertFilterErrors( { - 'trytes': None, + 'trytes': None, - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), }, { @@ -287,10 +289,10 @@ def test_fail_trytes_wrong_type(self): { # You have to specify an array, even if you only want to attach # a single tryte sequence. - 'trytes': TryteString(self.trytes1), + 'trytes': TryteString(self.trytes1), - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), }, { @@ -304,10 +306,10 @@ def test_fail_trytes_empty(self): { # Ok, you got the list part down, but you have to put something # inside it. - 'trytes': [], + 'trytes': [], - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), }, { @@ -319,7 +321,7 @@ def test_fail_trytes_contents_invalid(self): """`trytes` is an array, but it contains invalid values.""" self.assertFilterErrors( { - 'trytes': [ + 'trytes': [ b'', text_type(self.trytes1, 'ascii'), True, @@ -333,8 +335,8 @@ def test_fail_trytes_contents_invalid(self): 2130706433, ], - 'trunk_transaction': TransactionId(self.txn_id), - 'branch_transaction': TransactionId(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), }, { @@ -389,3 +391,19 @@ def test_pass_happy_path(self): 'duration': 42, }, ) + + +class AttachToTangleCommandTestCase(TestCase): + def setUp(self): + super(AttachToTangleCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).attachToTangle, + AttachToTangleCommand, + ) diff --git a/test/commands/core/broadcast_transactions_test.py b/test/commands/core/broadcast_transactions_test.py index 58192d7..6e1a0b7 100644 --- a/test/commands/core/broadcast_transactions_test.py +++ b/test/commands/core/broadcast_transactions_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TryteString +from iota import Iota, TryteString from iota.commands.core.broadcast_transactions import \ BroadcastTransactionsCommand from iota.filters import Trytes @@ -196,3 +198,19 @@ def test_pass_happy_path(self): ], }, ) + + +class BroadcastTransactionsCommandTestCase(TestCase): + def setUp(self): + super(BroadcastTransactionsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).broadcastTransactions, + BroadcastTransactionsCommand, + ) diff --git a/test/commands/core/find_transactions_test.py b/test/commands/core/find_transactions_test.py index e71bb22..2402ff3 100644 --- a/test/commands/core/find_transactions_test.py +++ b/test/commands/core/find_transactions_test.py @@ -2,11 +2,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import Address, Tag, TransactionId, TryteString -from iota.commands.core.find_transactions import FindTransactionsRequestFilter, \ - FindTransactionsCommand +from iota import Address, Iota, Tag, TransactionHash, TryteString +from iota.commands.core.find_transactions import FindTransactionsCommand, \ + FindTransactionsRequestFilter from iota.filters import Trytes from six import binary_type, text_type from test import MockAdapter @@ -30,8 +32,8 @@ def test_pass_all_parameters(self): """The request contains valid values for all parameters.""" request = { 'bundles': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], 'addresses': [ @@ -45,8 +47,8 @@ def test_pass_all_parameters(self): ], 'approvees': [ - TransactionId(self.trytes1), - TransactionId(self.trytes3), + TransactionHash(self.trytes1), + TransactionHash(self.trytes3), ], } @@ -88,8 +90,8 @@ def test_pass_compatible_types(self): { 'bundles': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], 'addresses': [ @@ -103,8 +105,8 @@ def test_pass_compatible_types(self): ], 'approvees': [ - TransactionId(self.trytes1), - TransactionId(self.trytes3), + TransactionHash(self.trytes1), + TransactionHash(self.trytes3), ], }, ) @@ -113,8 +115,8 @@ def test_pass_bundles_only(self): """The request only includes bundles.""" request = { 'bundles': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], } @@ -126,8 +128,8 @@ def test_pass_bundles_only(self): { 'bundles': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], 'addresses': [], @@ -194,8 +196,8 @@ def test_pass_approvees_only(self): """The request only includes approvees.""" request = { 'approvees': [ - TransactionId(self.trytes1), - TransactionId(self.trytes3), + TransactionHash(self.trytes1), + TransactionHash(self.trytes3), ], } @@ -207,8 +209,8 @@ def test_pass_approvees_only(self): { 'approvees': [ - TransactionId(self.trytes1), - TransactionId(self.trytes3), + TransactionHash(self.trytes1), + TransactionHash(self.trytes3), ], 'addresses': [], @@ -247,8 +249,8 @@ def test_fail_unexpected_parameters(self): self.assertFilterErrors( { 'addresses': [Address(self.trytes1)], - 'approvees': [TransactionId(self.trytes1)], - 'bundles': [TransactionId(self.trytes1)], + 'approvees': [TransactionHash(self.trytes1)], + 'bundles': [TransactionHash(self.trytes1)], 'tags': [Tag(self.trytes1)], # Hey, you're not allowed in he-argh! @@ -264,7 +266,7 @@ def test_fail_bundles_wrong_type(self): """`bundles` is not an array.""" self.assertFilterErrors( { - 'bundles': TransactionId(self.trytes1), + 'bundles': TransactionHash(self.trytes1), }, { @@ -393,7 +395,7 @@ def test_fail_approvees_wrong_type(self): """`approvees` is not an array.""" self.assertFilterErrors( { - 'approvees': TransactionId(self.trytes1), + 'approvees': TransactionHash(self.trytes1), }, { @@ -480,12 +482,12 @@ def test_search_results(self): { 'hashes': [ - TransactionId( + TransactionHash( b'RVORZ9SIIP9RCYMREUIXXVPQIPHVCNPQ9HZWYKFW' b'YWZRE9JQKG9REPKIASHUUECPSQO9JT9XNMVKWYGVA', ), - TransactionId( + TransactionHash( b'ZJVYUGTDRPDYFGFXMKOTV9ZWSGFK9CFPXTITQLQN' b'LPPG9YNAARMKNKYQO9GSCSBIOTGMLJUFLZWSY9999', ), @@ -494,3 +496,19 @@ def test_search_results(self): 'duration': 42, }, ) + + +class FindTransactionsCommandTestCase(TestCase): + def setUp(self): + super(FindTransactionsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).findTransactions, + FindTransactionsCommand, + ) diff --git a/test/commands/core/get_balances_test.py b/test/commands/core/get_balances_test.py index eb147ad..b30c545 100644 --- a/test/commands/core/get_balances_test.py +++ b/test/commands/core/get_balances_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import Address, TryteString +from iota import Address, Iota, TryteString from iota.commands.core.get_balances import GetBalancesCommand from iota.filters import Trytes from six import binary_type, text_type @@ -271,3 +273,19 @@ def test_balances(self): ), }, ) + + +class GetBalancesCommandTestCase(TestCase): + def setUp(self): + super(GetBalancesCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getBalances, + GetBalancesCommand, + ) diff --git a/test/commands/core/get_inclusion_states_test.py b/test/commands/core/get_inclusion_states_test.py index db513db..bb4d7e2 100644 --- a/test/commands/core/get_inclusion_states_test.py +++ b/test/commands/core/get_inclusion_states_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TransactionId, TryteString +from iota import Iota, TransactionHash, TryteString from iota.commands.core.get_inclusion_states import GetInclusionStatesCommand from iota.filters import Trytes from six import binary_type, text_type @@ -33,16 +35,16 @@ def test_pass_happy_path(self): """Typical `getInclusionStates` request.""" request = { 'transactions': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], 'tips': [ # These values would normally be different from # ``transactions``, but for purposes of this unit test, we just # need to make sure the format is correct. - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], } @@ -74,13 +76,13 @@ def test_pass_compatible_types(self): { 'transactions': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], 'tips': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], }, ) @@ -100,8 +102,8 @@ def test_fail_unexpected_parameters(self): """The incoming request contains unexpected parameters.""" self.assertFilterErrors( { - 'transactions': [TransactionId(self.trytes1)], - 'tips': [TransactionId(self.trytes2)], + 'transactions': [TransactionHash(self.trytes1)], + 'tips': [TransactionHash(self.trytes2)], # I bring scientists, you bring a rock star. 'foo': 'bar', @@ -118,7 +120,7 @@ def test_fail_transactions_null(self): { 'transactions': None, - 'tips': [TransactionId(self.trytes2)], + 'tips': [TransactionHash(self.trytes2)], }, { @@ -132,9 +134,9 @@ def test_fail_transactions_wrong_type(self): { # Has to be an array, even if we're only querying for one # transaction. - 'transactions': TransactionId(self.trytes1), + 'transactions': TransactionHash(self.trytes1), - 'tips': [TransactionId(self.trytes2)], + 'tips': [TransactionHash(self.trytes2)], }, { @@ -148,7 +150,7 @@ def test_fail_transactions_empty(self): { 'transactions': [], - 'tips': [TransactionId(self.trytes2)], + 'tips': [TransactionHash(self.trytes2)], }, { @@ -175,7 +177,7 @@ def test_fail_transactions_contents_invalid(self): b'9' * 82, ], - 'tips': [TransactionId(self.trytes2)], + 'tips': [TransactionHash(self.trytes2)], }, { @@ -195,7 +197,7 @@ def test_fail_tips_null(self): { 'tips': None, - 'transactions': [TransactionId(self.trytes1)], + 'transactions': [TransactionHash(self.trytes1)], }, { @@ -207,9 +209,9 @@ def test_fail_tips_wrong_type(self): """`tips` is not an array.""" self.assertFilterErrors( { - 'tips': TransactionId(self.trytes2), + 'tips': TransactionHash(self.trytes2), - 'transactions': [TransactionId(self.trytes1)], + 'transactions': [TransactionHash(self.trytes1)], }, { @@ -223,7 +225,7 @@ def test_fail_tips_empty(self): { 'tips': [], - 'transactions': [TransactionId(self.trytes1)], + 'transactions': [TransactionHash(self.trytes1)], }, { @@ -250,7 +252,7 @@ def test_fail_tips_contents_invalid(self): b'9' * 82, ], - 'transactions': [TransactionId(self.trytes1)], + 'transactions': [TransactionHash(self.trytes1)], }, { @@ -263,3 +265,19 @@ def test_fail_tips_contents_invalid(self): 'tips.7': [Trytes.CODE_WRONG_FORMAT], }, ) + + +class GetInclusionStatesCommandTestCase(TestCase): + def setUp(self): + super(GetInclusionStatesCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getInclusionStates, + GetInclusionStatesCommand, + ) diff --git a/test/commands/core/get_neighbors_test.py b/test/commands/core/get_neighbors_test.py index 5fb85c9..9a0760a 100644 --- a/test/commands/core/get_neighbors_test.py +++ b/test/commands/core/get_neighbors_test.py @@ -2,8 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase +from iota import Iota from iota.commands.core.get_neighbors import GetNeighborsCommand from test import MockAdapter @@ -31,3 +34,19 @@ def test_fail_unexpected_parameters(self): 'foo': [f.FilterMapper.CODE_EXTRA_KEY], }, ) + + +class GetNeighborsCommandTestCase(TestCase): + def setUp(self): + super(GetNeighborsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getNeighbors, + GetNeighborsCommand, + ) diff --git a/test/commands/core/get_node_info_test.py b/test/commands/core/get_node_info_test.py index 42bf4ba..f09eb79 100644 --- a/test/commands/core/get_node_info_test.py +++ b/test/commands/core/get_node_info_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TryteString +from iota import Iota, TransactionHash from iota.commands.core.get_node_info import GetNodeInfoCommand from test import MockAdapter @@ -91,15 +93,31 @@ def test_pass_happy_path(self): 'transactionsToRequest': 0, 'latestMilestone': - TryteString( + TransactionHash( b'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJR' b'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', ), 'latestSolidSubtangleMilestone': - TryteString( + TransactionHash( b'VBVEUQYE99LFWHDZRFKTGFHYGDFEAMAEBGUBTTJR' b'FKHCFBRTXFAJQ9XIUEZQCJOQTZNOOHKUQIKOY9999', ), }, ) + + +class GetNodeInfoCommandTestCase(TestCase): + def setUp(self): + super(GetNodeInfoCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getNodeInfo, + GetNodeInfoCommand, + ) diff --git a/test/commands/core/get_tips_test.py b/test/commands/core/get_tips_test.py index 9536c2f..b78a231 100644 --- a/test/commands/core/get_tips_test.py +++ b/test/commands/core/get_tips_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import Address +from iota import Address, Iota from iota.commands.core.get_tips import GetTipsCommand from test import MockAdapter @@ -97,3 +99,19 @@ def test_pass_no_hashes(self): self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, response) + + +class GetTipsCommandTestCase(TestCase): + def setUp(self): + super(GetTipsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getTips, + GetTipsCommand, + ) diff --git a/test/commands/core/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py index 4904ee9..954dfa8 100644 --- a/test/commands/core/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TransactionId +from iota import Iota, TransactionHash from iota.commands.core.get_transactions_to_approve import \ GetTransactionsToApproveCommand from test import MockAdapter @@ -116,13 +118,13 @@ def test_pass_happy_path(self): { 'trunkTransaction': - TransactionId( + TransactionHash( b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' ), 'branchTransaction': - TransactionId( + TransactionHash( b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' ), @@ -130,3 +132,19 @@ def test_pass_happy_path(self): 'duration': 936, }, ) + + +class GetTransactionsToApproveTestCase(TestCase): + def setUp(self): + super(GetTransactionsToApproveTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getTransactionsToApprove, + GetTransactionsToApproveCommand, + ) diff --git a/test/commands/core/get_trytes_test.py b/test/commands/core/get_trytes_test.py index c6a99d7..77e7d53 100644 --- a/test/commands/core/get_trytes_test.py +++ b/test/commands/core/get_trytes_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TransactionId, TryteString +from iota import Iota, TransactionHash, TryteString from iota.commands.core.get_trytes import GetTrytesCommand from iota.filters import Trytes from six import binary_type, text_type @@ -35,8 +37,8 @@ def test_pass_happy_path(self): """The request is valid.""" request = { 'hashes': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], } @@ -52,7 +54,7 @@ def test_pass_compatible_types(self): """ filter_ = self._filter({ 'hashes': [ - # Any sequence that can be converted into a TransactionId is + # Any sequence that can be converted into a TransactionHash is # valid. binary_type(self.trytes1), bytearray(self.trytes2), @@ -65,8 +67,8 @@ def test_pass_compatible_types(self): { 'hashes': [ - TransactionId(self.trytes1), - TransactionId(self.trytes2), + TransactionHash(self.trytes1), + TransactionHash(self.trytes2), ], }, ) @@ -85,7 +87,7 @@ def test_fail_unexpected_parameters(self): """The request contains unexpected parameters.""" self.assertFilterErrors( { - 'hashes': [TransactionId(self.trytes1)], + 'hashes': [TransactionHash(self.trytes1)], # This is why we can't have nice things! 'foo': 'bar', @@ -114,7 +116,7 @@ def test_fail_hashes_wrong_type(self): { # `hashes` must be an array, even if we're only querying # against a single transaction. - 'hashes': TransactionId(self.trytes1), + 'hashes': TransactionHash(self.trytes1), }, { @@ -218,3 +220,19 @@ def test_pass_no_transactions(self): self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, response) + + +class GetTrytesCommandTestCase(TestCase): + def setUp(self): + super(GetTrytesCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getTrytes, + GetTrytesCommand, + ) diff --git a/test/commands/core/interrupt_attaching_to_tangle_test.py b/test/commands/core/interrupt_attaching_to_tangle_test.py index c1fa325..4b1e6e3 100644 --- a/test/commands/core/interrupt_attaching_to_tangle_test.py +++ b/test/commands/core/interrupt_attaching_to_tangle_test.py @@ -2,8 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase +from iota import Iota from iota.commands.core.interrupt_attaching_to_tangle import \ InterruptAttachingToTangleCommand from test import MockAdapter @@ -32,3 +35,19 @@ def test_fail_unexpected_parameters(self): 'foo': [f.FilterMapper.CODE_EXTRA_KEY], }, ) + + +class InterruptAttachingToTangleCommandTestCase(TestCase): + def setUp(self): + super(InterruptAttachingToTangleCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).interruptAttachingToTangle, + InterruptAttachingToTangleCommand, + ) diff --git a/test/commands/core/remove_neighbors_test.py b/test/commands/core/remove_neighbors_test.py index 78879a1..3740e04 100644 --- a/test/commands/core/remove_neighbors_test.py +++ b/test/commands/core/remove_neighbors_test.py @@ -2,8 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase +from iota import Iota from iota.commands.core.remove_neighbors import RemoveNeighborsCommand from iota.filters import NodeUri from test import MockAdapter @@ -123,3 +126,19 @@ def test_fail_uris_contents_invalid(self): 'uris.6': [f.Type.CODE_WRONG_TYPE], }, ) + + +class RemoveNeighborsCommandTestCase(TestCase): + def setUp(self): + super(RemoveNeighborsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).removeNeighbors, + RemoveNeighborsCommand, + ) diff --git a/test/commands/core/store_transactions_test.py b/test/commands/core/store_transactions_test.py index 0797712..1e3d169 100644 --- a/test/commands/core/store_transactions_test.py +++ b/test/commands/core/store_transactions_test.py @@ -2,9 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase -from iota import TryteString +from iota import Iota, TryteString from iota.commands.core.store_transactions import StoreTransactionsCommand from iota.filters import Trytes from six import binary_type, text_type @@ -157,3 +159,19 @@ def test_trytes_contents_invalid(self): 'trytes.6': [f.Type.CODE_WRONG_TYPE], }, ) + + +class StoreTransactionsCommandTestCase(TestCase): + def setUp(self): + super(StoreTransactionsCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).storeTransactions, + StoreTransactionsCommand, + ) diff --git a/test/commands/extended/broadcast_and_store_test.py b/test/commands/extended/broadcast_and_store_test.py index b2ade2f..83c1a58 100644 --- a/test/commands/extended/broadcast_and_store_test.py +++ b/test/commands/extended/broadcast_and_store_test.py @@ -4,7 +4,7 @@ from unittest import TestCase -from iota import BadApiResponse, TryteString +from iota import BadApiResponse, Iota, TryteString from iota.commands.extended.broadcast_and_store import BroadcastAndStoreCommand from six import text_type from test import MockAdapter @@ -21,6 +21,15 @@ def setUp(self): self.trytes2 =\ b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).broadcastAndStore, + BroadcastAndStoreCommand, + ) + def test_happy_path(self): """ Successful invocation of `broadcastAndStore`. diff --git a/test/commands/extended/get_inputs_test.py b/test/commands/extended/get_inputs_test.py index 041c08f..e4b3ee1 100644 --- a/test/commands/extended/get_inputs_test.py +++ b/test/commands/extended/get_inputs_test.py @@ -6,6 +6,7 @@ import filters as f from filters.test import BaseFilterTestCase +from iota import Iota from iota.commands.extended.get_inputs import GetInputsCommand, \ GetInputsRequestFilter from iota.crypto.types import Seed @@ -355,6 +356,15 @@ def setUp(self): self.adapter = MockAdapter() self.command = GetInputsCommand(self.adapter) + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getInputs, + GetInputsCommand, + ) + def test_start_and_end_with_threshold(self): """ ``start`` and ``end`` values provided, with ``threshold``. diff --git a/test/commands/extended/get_latest_inclusion_test.py b/test/commands/extended/get_latest_inclusion_test.py new file mode 100644 index 0000000..f246d5a --- /dev/null +++ b/test/commands/extended/get_latest_inclusion_test.py @@ -0,0 +1,97 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from filters.test import BaseFilterTestCase +from iota import Iota +from iota.commands.extended.get_latest_inclusion import \ + GetLatestInclusionCommand +from test import MockAdapter + + +class GetLatestInclusionRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetLatestInclusionCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def test_pass_happy_path(self): + """ + Request is valid. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_empty(self): + """ + Request is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_null(self): + """ + ``hashes`` is null. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_wrong_type(self): + """ + ``hashes`` is not an array. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_empty(self): + """ + ``hashes`` is an array, but it is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_hashes_contents_invalid(self): + """ + ``hashes`` is a non-empty array, but it contains invalid values. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + +class GetLatestInclusionCommandTestCase(TestCase): + def setUp(self): + super(GetLatestInclusionCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = GetLatestInclusionCommand(self.adapter) + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getLatestInclusion, + GetLatestInclusionCommand, + ) + + def test_happy_path(self): + """ + Successfully requesting latest inclusion state. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index e611942..78b7920 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -6,7 +6,7 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address +from iota import Address, Iota from iota.commands.extended.get_new_addresses import GetNewAddressesCommand from iota.crypto.types import Seed from iota.filters import Trytes @@ -278,6 +278,15 @@ def setUp(self): b'VNZTCTFUPQ9ESTKNSSLLIZWDQISJVEWIJDVGIECXF' ) + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getNewAddresses, + GetNewAddressesCommand, + ) + def test_get_addresses_offline(self): """ Generate addresses in offline mode (without filtering used diff --git a/test/commands/extended/get_transfers_test.py b/test/commands/extended/get_transfers_test.py index 1dd83bc..acb1453 100644 --- a/test/commands/extended/get_transfers_test.py +++ b/test/commands/extended/get_transfers_test.py @@ -2,8 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from unittest import TestCase + import filters as f from filters.test import BaseFilterTestCase +from iota import Iota from iota.commands.extended.get_transfers import GetTransfersCommand, \ GetTransfersRequestFilter from iota.crypto.types import Seed @@ -309,3 +312,22 @@ def test_fail_inclusion_states_wrong_type(self): 'inclusion_states': [f.Type.CODE_WRONG_TYPE], }, ) + + +class GetTransfersCommandTestCase(TestCase): + def setUp(self): + super(GetTransfersCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = GetTransfersCommand(self.adapter) + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getTransfers, + GetTransfersCommand, + ) + + # :todo: Unit tests. diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index 6ad8582..af2aa07 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -4,7 +4,7 @@ from unittest import TestCase -from iota import BadApiResponse, TransactionId, TryteString +from iota import BadApiResponse, Iota, TransactionHash, TryteString from iota.commands.extended.send_trytes import SendTrytesCommand from six import text_type from test import MockAdapter @@ -33,6 +33,15 @@ def setUp(self): b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' ) + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).sendTrytes, + SendTrytesCommand, + ) + def test_happy_path(self): """ Successful invocation of `sendTrytes`. @@ -82,8 +91,8 @@ def test_happy_path(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionId(self.transaction1), - 'branch_transaction': TransactionId(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ @@ -182,8 +191,8 @@ def test_attach_to_tangle_fails(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionId(self.transaction1), - 'branch_transaction': TransactionId(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ @@ -239,8 +248,8 @@ def test_broadcast_transactions_fails(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionId(self.transaction1), - 'branch_transaction': TransactionId(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ @@ -310,8 +319,8 @@ def test_store_transactions_fails(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionId(self.transaction1), - 'branch_transaction': TransactionId(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ diff --git a/test/filters_test.py b/test/filters_test.py index 8ae23ab..5b2ddad 100644 --- a/test/filters_test.py +++ b/test/filters_test.py @@ -5,7 +5,7 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import TryteString, TransactionId +from iota import TryteString, TransactionHash from iota.filters import NodeUri, Trytes @@ -106,10 +106,10 @@ def test_pass_alternate_result_type(self): b'99999999999999999999999999999999999999999' ) - filter_ = self._filter(input_trytes, result_type=TransactionId) + filter_ = self._filter(input_trytes, result_type=TransactionHash) self.assertFilterPasses(filter_, result_trytes) - self.assertIsInstance(filter_.cleaned_data, TransactionId) + self.assertIsInstance(filter_.cleaned_data, TransactionHash) def test_fail_not_trytes(self): """ @@ -139,7 +139,7 @@ def test_fail_alternate_result_type(self): ) self.assertFilterErrors( - self._filter(trytes, result_type=TransactionId), + self._filter(trytes, result_type=TransactionHash), [Trytes.CODE_WRONG_FORMAT], ) diff --git a/test/types_test.py b/test/types_test.py index 5e4c4f8..1dbc81f 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -12,7 +12,7 @@ Hash, Tag, Transaction, - TransactionId, + TransactionHash, TryteString, TrytesCodec, TrytesDecodeError, @@ -838,12 +838,12 @@ def test_init_error_too_long(self): # noinspection SpellCheckingInspection -class TransactionIdTestCase(TestCase): +class TransactionHashTestCase(TestCase): def test_init_automatic_pad(self): """ - Transaction IDs are automatically padded to 81 trytes. + Transaction hashes are automatically padded to 81 trytes. """ - txn = TransactionId( + txn = TransactionHash( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' ) @@ -858,10 +858,10 @@ def test_init_automatic_pad(self): def test_init_error_too_long(self): """ - Attempting to create a transaction ID longer than 81 trytes. + Attempting to create a transaction hash longer than 81 trytes. """ with self.assertRaises(ValueError): - TransactionId( + TransactionHash( b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' ) @@ -999,7 +999,7 @@ def test_from_tryte_string(self): self.assertEqual( transaction.trunk_transaction_id, - TransactionId( + TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' ), @@ -1008,7 +1008,7 @@ def test_from_tryte_string(self): self.assertEqual( transaction.branch_transaction_id, - TransactionId( + TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' ), @@ -1108,13 +1108,13 @@ def test_as_tryte_string(self): ), trunk_transaction_id = - TransactionId( + TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' ), branch_transaction_id = - TransactionId( + TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' ), From ed97c5b01dea89a0f2decf7f6979e11d38081579 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 16:09:18 -0500 Subject: [PATCH 172/239] Fixed incorrect whitespace. --- test/commands/core/get_transactions_to_approve_test.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/commands/core/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py index 954dfa8..133418d 100644 --- a/test/commands/core/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -118,13 +118,13 @@ def test_pass_happy_path(self): { 'trunkTransaction': - TransactionHash( + TransactionHash( b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' ), 'branchTransaction': - TransactionHash( + TransactionHash( b'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' b'CTHHSSEQCD9YLDPEXYERCNJVASRGWMAVKFQTC9999' ), From 2f1277770b7bdb4dacf3a59a5e29973e987a4180 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 16:11:53 -0500 Subject: [PATCH 173/239] Fixed incorrect whitespace. --- test/commands/core/get_inclusion_states_test.py | 4 ++-- test/commands/extended/send_trytes_test.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/test/commands/core/get_inclusion_states_test.py b/test/commands/core/get_inclusion_states_test.py index bb4d7e2..a09768b 100644 --- a/test/commands/core/get_inclusion_states_test.py +++ b/test/commands/core/get_inclusion_states_test.py @@ -136,7 +136,7 @@ def test_fail_transactions_wrong_type(self): # transaction. 'transactions': TransactionHash(self.trytes1), - 'tips': [TransactionHash(self.trytes2)], + 'tips': [TransactionHash(self.trytes2)], }, { @@ -209,7 +209,7 @@ def test_fail_tips_wrong_type(self): """`tips` is not an array.""" self.assertFilterErrors( { - 'tips': TransactionHash(self.trytes2), + 'tips': TransactionHash(self.trytes2), 'transactions': [TransactionHash(self.trytes1)], }, diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index af2aa07..6c9522f 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -91,8 +91,8 @@ def test_happy_path(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionHash(self.transaction1), - 'branch_transaction': TransactionHash(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ @@ -191,8 +191,8 @@ def test_attach_to_tangle_fails(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionHash(self.transaction1), - 'branch_transaction': TransactionHash(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ @@ -248,8 +248,8 @@ def test_broadcast_transactions_fails(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionHash(self.transaction1), - 'branch_transaction': TransactionHash(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ @@ -319,8 +319,8 @@ def test_store_transactions_fails(self): { 'command': 'attachToTangle', - 'trunk_transaction': TransactionHash(self.transaction1), - 'branch_transaction': TransactionHash(self.transaction2), + 'trunk_transaction': TransactionHash(self.transaction1), + 'branch_transaction': TransactionHash(self.transaction2), 'min_weight_magnitude': 18, 'trytes': [ From bc3baa4713c80a4ab5f1e78360ce497edfb9f562 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 16:14:16 -0500 Subject: [PATCH 174/239] Fixed error when generating random seed in Python 2. --- iota/crypto/types.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/iota/crypto/types.py b/iota/crypto/types.py index f51443c..892525e 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -41,7 +41,8 @@ def random(cls, length=Hash.LEN, source=urandom): new_seed = Seed.random(source=Random.new().read) """ # Encoding bytes -> trytes yields 2 trytes per byte. - return cls.from_bytes(source(ceil(length / 2))) + # Note: int cast for compatibility with Python 2. + return cls.from_bytes(source(int(ceil(length / 2)))) class SigningKey(TryteString): From 5068d40471f6614e6ceff59b317cdfa8d6d3c8af Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 16:53:42 -0500 Subject: [PATCH 175/239] More context when raising exceptions. --- iota/adapter.py | 36 +++++++-- iota/api.py | 2 +- iota/codecs.py | 59 ++++++++++---- iota/commands/__init__.py | 33 ++++---- iota/crypto/addresses.py | 30 ++++++- iota/crypto/signing.py | 43 ++++++++++- iota/crypto/types.py | 16 +++- iota/types.py | 159 ++++++++++++++++++++++++++------------ 8 files changed, 275 insertions(+), 103 deletions(-) diff --git a/iota/adapter.py b/iota/adapter.py index 3cf8abc..2184493 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -9,6 +9,7 @@ from typing import Dict, Text, Tuple, Union import requests +from iota.exceptions import with_context from six import with_metaclass from iota import DEFAULT_PORT @@ -57,14 +58,29 @@ def resolve_adapter(uri): try: protocol, _ = uri.split('://', 1) except ValueError: - raise InvalidUri('URI must begin with "://" (e.g., "udp://").') + raise with_context( + exc = InvalidUri( + 'URI must begin with "://" (e.g., "udp://").', + ), + + context = { + 'uri': uri, + }, + ) try: adapter_type = adapter_registry[protocol] except KeyError: - raise InvalidUri('Unrecognized protocol {protocol!r}.'.format( - protocol = protocol, - )) + raise with_context( + exc = InvalidUri('Unrecognized protocol {protocol!r}.'.format( + protocol = protocol, + )), + + context = { + 'protocol': protocol, + 'uri': uri, + }, + ) return adapter_type.configure(uri) @@ -136,9 +152,15 @@ def configure(cls, uri): raise InvalidUri('No protocol specified in URI {uri!r}.'.format(uri=uri)) else: if protocol not in cls.supported_protocols: - raise InvalidUri('Unsupported protocol {protocol!r}.'.format( - protocol = protocol, - )) + raise with_context( + exc = InvalidUri('Unsupported protocol {protocol!r}.'.format( + protocol = protocol, + )), + + context = { + 'uri': uri, + }, + ) try: server, path = config.split('/', 1) diff --git a/iota/api.py b/iota/api.py index 3ab7620..ae2dbe2 100644 --- a/iota/api.py +++ b/iota/api.py @@ -602,7 +602,7 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=18): References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes """ - raise self.sendTrytes( + return self.sendTrytes( trytes = trytes, depth = depth, min_weight_magnitude = min_weight_magnitude, diff --git a/iota/codecs.py b/iota/codecs.py index 9f6f892..594498c 100644 --- a/iota/codecs.py +++ b/iota/codecs.py @@ -4,6 +4,7 @@ from codecs import Codec, CodecInfo, register as lookup_function +from iota.exceptions import with_context from six import PY3, binary_type __all__ = [ @@ -67,9 +68,15 @@ def encode(self, input, errors='strict'): input = input.tobytes() if not isinstance(input, (binary_type, bytearray)): - raise TypeError("Can't encode {type}; byte string expected.".format( - type = type(input).__name__, - )) + raise with_context( + exc = TypeError("Can't encode {type}; byte string expected.".format( + type = type(input).__name__, + )), + + context = { + 'input': input, + }, + ) # :bc: In Python 2, iterating over a byte string yields characters # instead of integers. @@ -95,9 +102,15 @@ def decode(self, input, errors='strict'): input = input.tobytes() if not isinstance(input, (binary_type, bytearray)): - raise TypeError("Can't decode {type}; byte string expected.".format( - type = type(input).__name__, - )) + raise with_context( + exc = TypeError("Can't decode {type}; byte string expected.".format( + type = type(input).__name__, + )), + + context = { + 'input': input, + }, + ) # :bc: In Python 2, iterating over a byte string yields characters # instead of integers. @@ -111,11 +124,17 @@ def decode(self, input, errors='strict'): first, second = input[i:i+2] except ValueError: if errors == 'strict': - raise TrytesDecodeError( - "'{name}' codec can't decode value; " - "tryte sequence has odd length.".format( - name = self.name, + raise with_context( + exc = TrytesDecodeError( + "'{name}' codec can't decode value; " + "tryte sequence has odd length.".format( + name = self.name, + ), ), + + context = { + 'input': input, + }, ) elif errors == 'replace': bytes_ += b'?' @@ -131,14 +150,20 @@ def decode(self, input, errors='strict'): # This combination of trytes yields a value > 255 when # decoded. Naturally, we can't represent this using ASCII. if errors == 'strict': - raise TrytesDecodeError( - "'{name}' codec can't decode trytes {pair} at position {i}-{j}: " - "ordinal not in range(255)".format( - name = self.name, - pair = chr(first) + chr(second), - i = i, - j = i+1, + raise with_context( + exc = TrytesDecodeError( + "'{name}' codec can't decode trytes {pair} at position {i}-{j}: " + "ordinal not in range(255)".format( + name = self.name, + pair = chr(first) + chr(second), + i = i, + j = i+1, + ), ), + + context = { + 'input': input, + } ) elif errors == 'replace': bytes_ += b'?' diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index efdef67..558eaed 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -10,6 +10,7 @@ from typing import Dict, Mapping, Optional, Text, Union import filters as f +from iota.exceptions import with_context from six import with_metaclass, string_types from iota.adapter import BaseAdapter @@ -85,7 +86,14 @@ def __call__(self, **kwargs): # type: (dict) -> dict """Sends the command to the node.""" if self.called: - raise RuntimeError('Command has already been called.') + raise with_context( + exc = RuntimeError('Command has already been called.'), + + context = { + 'last_request': self.request, + 'last_response': self.response, + }, + ) self.request = kwargs @@ -178,20 +186,6 @@ def _prepare_response(self, response): pass -class FilterError(ValueError): - """ - Indicates that the request or response passed to a FilterCommand - failed one or more filters. - """ - def __init__(self, message, filter_runner): - # type: (Text, f.FilterRunner) -> None - super(FilterError, self).__init__(message) - - self.context = { - 'filter_errors': filter_runner.get_errors(with_context=True), - } - - class RequestFilter(f.FilterChain): """Template for filter applied to API requests.""" # Be more strict about missing/extra keys for requests, since they @@ -293,16 +287,19 @@ def _apply_filter(value, filter_, failure_message): if runner.is_valid(): return runner.cleaned_data else: - raise FilterError( - message = + raise with_context( + exc = ValueError( '{message} ({error_codes}) ' '(`exc.context["filter_errors"]` ' 'contains more information).'.format( message = failure_message, error_codes = runner.error_codes, ), + ), - filter_runner = runner, + context = { + 'filter_errors': runner.get_errors(with_context=True), + }, ) return value diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index cd839b6..29b9dc9 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -8,6 +8,7 @@ from iota.crypto import Curl from iota.crypto.signing import KeyGenerator from iota.crypto.types import SigningKey +from iota.exceptions import with_context __all__ = [ 'AddressGenerator', @@ -76,10 +77,26 @@ def get_addresses(self, start, count=1, step=1): negative). """ if count < 1: - raise ValueError('``count`` must be positive.') + raise with_context( + exc = ValueError('``count`` must be positive.'), + + context = { + 'start': start, + 'count': count, + 'step': step, + }, + ) if not step: - raise ValueError('``step`` must not be zero.') + raise with_context( + exc = ValueError('``step`` must not be zero.'), + + context = { + 'start': start, + 'count': count, + 'step': step, + }, + ) generator = self.create_generator(start, step) @@ -113,7 +130,14 @@ def create_generator(self, start=0, step=1): iterations if ``step`` is a large number! """ if start < 0: - raise ValueError('``start`` cannot be negative.') + raise with_context( + exc = ValueError('``start`` cannot be negative.'), + + context = { + 'start': start, + 'step': step, + }, + ) digest_generator = self._create_digest_generator(start, step) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 0d0fb1b..b66f75a 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -7,6 +7,7 @@ from iota import TryteString, TrytesCompatible from iota.crypto import Curl, HASH_LENGTH from iota.crypto.types import SigningKey +from iota.exceptions import with_context __all__ = [ 'KeyGenerator', @@ -68,10 +69,28 @@ def get_keys(self, start, count=1, step=1, iterations=1): negative). """ if count < 1: - raise ValueError('``count`` must be positive.') + raise with_context( + exc = ValueError('``count`` must be positive.'), + + context = { + 'start': start, + 'count': count, + 'step': step, + 'iterations': iterations, + }, + ) if not step: - raise ValueError('``step`` must not be zero.') + raise with_context( + exc = ValueError('``step`` must not be zero.'), + + context = { + 'start': start, + 'count': count, + 'step': step, + 'iterations': iterations, + }, + ) generator = self.create_generator(start, step, iterations) @@ -115,10 +134,26 @@ def create_generator(self, start=0, step=1, iterations=1): resistant to brute-forcing. """ if start < 0: - raise ValueError('``start`` cannot be negative.') + raise with_context( + exc = ValueError('``start`` cannot be negative.'), + + context = { + 'start': start, + 'step': step, + 'iterations': iterations, + }, + ) if iterations < 1: - raise ValueError('``iterations`` must be >= 1.') + raise with_context( + exc = ValueError('``iterations`` must be >= 1.'), + + context = { + 'start': start, + 'step': step, + 'iterations': iterations, + }, + ) current = start diff --git a/iota/crypto/types.py b/iota/crypto/types.py index 892525e..957eb01 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -8,6 +8,8 @@ from iota import Hash, TryteString, TrytesCompatible from iota.crypto import HASH_LENGTH, Curl from math import ceil + +from iota.exceptions import with_context from six import binary_type __all__ = [ @@ -61,11 +63,17 @@ def __init__(self, trytes): super(SigningKey, self).__init__(trytes) if len(self._trytes) % self.BLOCK_LEN: - raise ValueError( - 'Length of {cls} values must be a multiple of {len} trytes.'.format( - cls = type(self).__name__, - len = self.BLOCK_LEN + raise with_context( + exc = ValueError( + 'Length of {cls} values must be a multiple of {len} trytes.'.format( + cls = type(self).__name__, + len = self.BLOCK_LEN + ), ), + + context = { + 'trytes': trytes, + }, ) @property diff --git a/iota/types.py b/iota/types.py index 735edb2..230848f 100644 --- a/iota/types.py +++ b/iota/types.py @@ -9,6 +9,7 @@ from iota import TrytesCodec from iota.crypto import Curl, HASH_LENGTH +from iota.exceptions import with_context from six import PY2, binary_type @@ -166,12 +167,18 @@ def __init__(self, trytes, pad=None): super(TryteString, self).__init__() if isinstance(trytes, (int, float)): - raise TypeError( - 'Converting {type} is not supported; ' - '{cls} is not a numeric type.'.format( - type = type(trytes).__name__, - cls = type(self).__name__, + raise with_context( + exc = TypeError( + 'Converting {type} is not supported; ' + '{cls} is not a numeric type.'.format( + type = type(trytes).__name__, + cls = type(self).__name__, + ), ), + + context = { + 'trytes': trytes, + }, ) if isinstance(trytes, TryteString): @@ -183,11 +190,17 @@ def __init__(self, trytes, pad=None): trytes = bytearray(trytes._trytes) else: - raise TypeError( - '{cls} cannot be initialized from a(n) {type}.'.format( - type = type(trytes).__name__, - cls = type(self).__name__, + raise with_context( + exc = TypeError( + '{cls} cannot be initialized from a(n) {type}.'.format( + type = type(trytes).__name__, + cls = type(self).__name__, + ), ), + + context = { + 'trytes': trytes, + }, ) else: @@ -196,12 +209,18 @@ def __init__(self, trytes, pad=None): for i, ordinal in enumerate(trytes): if ordinal not in TrytesCodec.index: - raise ValueError( - 'Invalid character {char!r} at position {i} ' - '(expected A-Z or 9).'.format( - char = chr(ordinal), - i = i, + raise with_context( + exc = ValueError( + 'Invalid character {char!r} at position {i} ' + '(expected A-Z or 9).'.format( + char = chr(ordinal), + i = i, + ), ), + + context = { + 'trytes': trytes, + }, ) if pad: @@ -243,13 +262,19 @@ def __contains__(self, other): elif isinstance(other, (binary_type, bytearray)): return other in self._trytes else: - raise TypeError( - 'Invalid type for TryteString contains check ' - '(expected Union[TryteString, {binary_type}, bytearray], ' - 'actual {type}).'.format( - binary_type = binary_type.__name__, - type = type(other).__name__, + raise with_context( + exc = TypeError( + 'Invalid type for TryteString contains check ' + '(expected Union[TryteString, {binary_type}, bytearray], ' + 'actual {type}).'.format( + binary_type = binary_type.__name__, + type = type(other).__name__, + ), ), + + context = { + 'other': other, + }, ) def __getitem__(self, item): @@ -272,13 +297,19 @@ def __add__(self, other): elif isinstance(other, (binary_type, bytearray)): return TryteString(self._trytes + other) else: - raise TypeError( - 'Invalid type for TryteString concatenation ' - '(expected Union[TryteString, {binary_type}, bytearray], ' - 'actual {type}).'.format( - binary_type = binary_type.__name__, - type = type(other).__name__, + raise with_context( + exc = TypeError( + 'Invalid type for TryteString concatenation ' + '(expected Union[TryteString, {binary_type}, bytearray], ' + 'actual {type}).'.format( + binary_type = binary_type.__name__, + type = type(other).__name__, + ), ), + + context = { + 'other': other, + }, ) def __eq__(self, other): @@ -288,13 +319,19 @@ def __eq__(self, other): elif isinstance(other, (binary_type, bytearray)): return self._trytes == other else: - raise TypeError( - 'Invalid type for TryteString comparison ' - '(expected Union[TryteString, {binary_type}, bytearray], ' - 'actual {type}).'.format( - binary_type = binary_type.__name__, - type = type(other).__name__, + raise with_context( + exc = TypeError( + 'Invalid type for TryteString comparison ' + '(expected Union[TryteString, {binary_type}, bytearray], ' + 'actual {type}).'.format( + binary_type = binary_type.__name__, + type = type(other).__name__, + ), ), + + context = { + 'other': other, + }, ) # :bc: In Python 2 this must be defined explicitly. @@ -390,10 +427,16 @@ def __init__(self, trytes): super(Hash, self).__init__(trytes, pad=self.LEN) if len(self._trytes) > self.LEN: - raise ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )) + raise with_context( + exc = ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )), + + context = { + 'trytes': trytes, + }, + ) class Address(TryteString): @@ -412,12 +455,18 @@ def __init__(self, trytes): self.checksum = AddressChecksum(self[self.LEN:]) # type: Optional[AddressChecksum] elif len(self._trytes) > self.LEN: - raise ValueError( - 'Address values must be {len_no_checksum} trytes (no checksum), ' - 'or {len_with_checksum} trytes (with checksum).'.format( - len_no_checksum = self.LEN, - len_with_checksum = self.LEN + AddressChecksum.LEN, + raise with_context( + exc = ValueError( + 'Address values must be {len_no_checksum} trytes (no checksum), ' + 'or {len_with_checksum} trytes (with checksum).'.format( + len_no_checksum = self.LEN, + len_with_checksum = self.LEN + AddressChecksum.LEN, + ), ), + + context = { + 'trytes': trytes, + }, ) # Make the address sans checksum accessible. @@ -468,11 +517,17 @@ def __init__(self, trytes): super(AddressChecksum, self).__init__(trytes, pad=None) if len(self._trytes) != self.LEN: - raise ValueError( - '{cls} values must be exactly {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN, + raise with_context( + exc = ValueError( + '{cls} values must be exactly {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN, + ), ), + + context = { + 'trytes': trytes, + }, ) @@ -494,10 +549,16 @@ def __init__(self, trytes): super(Tag, self).__init__(trytes, pad=self.LEN) if len(self._trytes) > self.LEN: - raise ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )) + raise with_context( + exc = ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )), + + context = { + 'trytes': trytes, + }, + ) class TransactionHash(Hash): From ff153ff1554bea2be1d36d46a8bd89842ed0d665 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 17:22:13 -0500 Subject: [PATCH 176/239] Stubbed out `prepareTransfers` command. --- iota/api.py | 7 ++- iota/commands/extended/prepare_transfers.py | 57 +++++++++++++++++++ .../extended/prepare_transfers_test.py | 31 ++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 iota/commands/extended/prepare_transfers.py create mode 100644 test/commands/extended/prepare_transfers_test.py diff --git a/iota/api.py b/iota/api.py index ae2dbe2..4379b19 100644 --- a/iota/api.py +++ b/iota/api.py @@ -419,7 +419,12 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers """ - raise NotImplementedError('Not implemented yet.') + return self.prepareTransfers( + seed = self.seed, + transfers = transfers, + inputs = inputs, + change_address = change_address, + ) def get_latest_inclusion(self, hashes): # type: (Iterable[TransactionHash]) -> Dict[TransactionHash, bool] diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfers.py new file mode 100644 index 0000000..45a89c7 --- /dev/null +++ b/iota/commands/extended/prepare_transfers.py @@ -0,0 +1,57 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from iota import Address, Transaction, TransactionHash +from iota.commands import FilterCommand, RequestFilter +from iota.crypto.types import Seed +from iota.filters import Trytes + +__all__ = [ + 'PrepareTransfersCommand', +] + + +class PrepareTransfersCommand(FilterCommand): + """ + Executes ``prepareTransfers`` extended API command. + + See :py:meth:`iota.api.Iota.prepare_transfers` for more info. + """ + command = 'prepareTransfers' + + def get_request_filter(self): + return PrepareTransfersRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + +class PrepareTransfersRequestFilter(RequestFilter): + def __init__(self): + super(PrepareTransfersRequestFilter, self).__init__( + { + # Required parameters. + 'seed': f.Required | Trytes(result_type=Seed), + + 'transfers': + f.Required | f.Array | f.FilterRepeater(f.Type(Transaction)), + + # Optional parameters. + 'change_address': Trytes(result_type=Address), + + 'inputs': + f.Array | f.FilterRepeater(Trytes(result_type=TransactionHash)), + }, + + allow_missing_keys = { + 'change_address', + 'inputs', + }, + ) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py new file mode 100644 index 0000000..83d9937 --- /dev/null +++ b/test/commands/extended/prepare_transfers_test.py @@ -0,0 +1,31 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from filters.test import BaseFilterTestCase +from iota import Iota +from iota.commands.extended.prepare_transfers import PrepareTransfersCommand +from test import MockAdapter + + +class PrepareTransfersRequestFilterTestCase(BaseFilterTestCase): + filter_type = PrepareTransfersCommand(MockAdapter()).get_request_filter + skip_value_check = True + + +class PrepareTransfersCommandTestCase(TestCase): + def setUp(self): + super(PrepareTransfersCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).prepareTransfers, + PrepareTransfersCommand, + ) From 016e41b74b16ef0f52f7010cfd6d1f95aca05dbb Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 17:29:54 -0500 Subject: [PATCH 177/239] Stubbed out unit tests for `prepareTransfer`. --- .../extended/prepare_transfers_test.py | 115 ++++++++++++++++++ 1 file changed, 115 insertions(+) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index 83d9937..ffe372d 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -14,6 +14,119 @@ class PrepareTransfersRequestFilterTestCase(BaseFilterTestCase): filter_type = PrepareTransfersCommand(MockAdapter()).get_request_filter skip_value_check = True + def test_pass_happy_path(self): + """ + Request is valid. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_optional_parameters_omitted(self): + """ + Request omits optional parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_empty(self): + """ + Request is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_seed_null(self): + """ + ``seed`` is null. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_seed_wrong_type(self): + """ + ``seed`` is not a TrytesCompatible value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_seed_not_trytes(self): + """ + ``seed`` contains invalid characters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transfers_wrong_type(self): + """ + ``transfers`` is not an array. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transfers_empty(self): + """ + ``transfers`` is an array, but it is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transfers_contents_invalid(self): + """ + ``transfers`` is a non-empty array, but it contains invalid values. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_change_address_wrong_type(self): + """ + ``change_address`` is not a TrytesCompatible value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_change_address_not_trytes(self): + """ + ``change_address`` contains invalid characters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_wrong_type(self): + """ + ``inputs`` is not an array. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_empty(self): + """ + ``inputs`` is an array, but it is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_contents_invalid(self): + """ + ``inputs`` is a non-empty array, but it contains invalid values. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + class PrepareTransfersCommandTestCase(TestCase): def setUp(self): @@ -29,3 +142,5 @@ def test_wireup(self): Iota(self.adapter).prepareTransfers, PrepareTransfersCommand, ) + + # :todo: Unit tests. From 61132e02ab6b1dfeced22b2c5e72104b9ab59cc8 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 22:15:54 -0500 Subject: [PATCH 178/239] New features and cleanup. - Implemented `prepareTransfers`, minus input signing. - Added boolean cast for `TryteString`. - Fixed bytes cast for `TryteString`. - Renamed `BundleId` to `BundleHash`. - Added `ProposedBundle` and `ProposedTransaction`. - Standardized raising exceptions with context values. - Added lots of documentation. --- iota/adapter.py | 46 ++- iota/api.py | 20 +- iota/commands/__init__.py | 15 +- iota/commands/extended/broadcast_and_store.py | 13 +- iota/commands/extended/get_inputs.py | 33 +- iota/commands/extended/prepare_transfers.py | 83 ++++- iota/commands/extended/send_trytes.py | 14 +- iota/types.py | 350 +++++++++++++++++- test/__init__.py | 16 +- test/types_test.py | 115 +++++- 10 files changed, 593 insertions(+), 112 deletions(-) diff --git a/iota/adapter.py b/iota/adapter.py index 2184493..8c06b34 100644 --- a/iota/adapter.py +++ b/iota/adapter.py @@ -9,11 +9,10 @@ from typing import Dict, Text, Tuple, Union import requests -from iota.exceptions import with_context -from six import with_metaclass - from iota import DEFAULT_PORT +from iota.exceptions import with_context from iota.json import JsonEncoder +from six import with_metaclass __all__ = [ 'AdapterSpec', @@ -30,13 +29,8 @@ class BadApiResponse(ValueError): """ Indicates that a non-success response was received from the node. """ - def __init__(self, message, request): - # Type: (Text, dict) -> None - super(BadApiResponse, self).__init__(message) + pass - self.context = { - 'request': request, - } class InvalidUri(ValueError): """ @@ -220,18 +214,28 @@ def send_request(self, payload, **kwargs): raw_content = response.text if not raw_content: - raise BadApiResponse('Empty response from node.', payload) + raise with_context( + exc = BadApiResponse('Empty response from node.'), + + context = { + 'request': payload, + }, + ) try: decoded = json.loads(raw_content) # type: dict # :bc: py2k doesn't have JSONDecodeError except ValueError: - raise BadApiResponse( - message = 'Non-JSON response from node: {raw_content}'.format( - raw_content = raw_content, + raise with_context( + exc = BadApiResponse( + 'Non-JSON response from node: {raw_content}'.format( + raw_content = raw_content, + ) ), - request = payload, + context = { + 'request': payload, + }, ) try: @@ -241,16 +245,20 @@ def send_request(self, payload, **kwargs): # :see:`https://github.com/iotaledger/iri/issues/12` error = decoded.get('exception') or decoded.get('error') except AttributeError: - raise BadApiResponse( - message = 'Invalid response from node: {raw_content}'.format( - raw_content = raw_content, + raise with_context( + exc = BadApiResponse( + 'Invalid response from node: {raw_content}'.format( + raw_content = raw_content, + ), ), - request = payload, + context = { + 'request': payload, + }, ) if error: - raise BadApiResponse(error, payload) + raise with_context(BadApiResponse(error), context={'request': payload}) return decoded diff --git a/iota/api.py b/iota/api.py index 4379b19..6ca61b8 100644 --- a/iota/api.py +++ b/iota/api.py @@ -2,11 +2,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Iterable, List, Optional, Text +from typing import Dict, Iterable, List, Optional, Text -from iota import Address, Bundle, Tag, TransactionHash, Transaction, TryteString, \ +from iota import AdapterSpec, Address, Bundle, ProposedBundle, \ + ProposedTransaction, Tag, Transaction, TransactionHash, TryteString, \ TrytesCompatible -from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter +from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry from iota.crypto.types import Seed @@ -392,7 +393,7 @@ def get_inputs(self, start=None, end=None, threshold=None): ) def prepare_transfers(self, transfers, inputs=None, change_address=None): - # type: (Iterable[Transaction], Optional[Iterable[TransactionHash]], Optional[Address]) -> List[TryteString] + # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address]) -> ProposedBundle """ Prepares transactions to be broadcast to the Tangle, by generating the correct bundle, as well as choosing and signing the inputs (for @@ -401,9 +402,13 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): :param transfers: Transaction objects to prepare. :param inputs: - List of inputs used to fund the transfer. + List of addresses used to fund the transfer. Not needed for zero-value transfers. + If not provided, addresses will be selected automatically by + scanning the Tangle for unspent inputs. Note: this could take + awhile to complete. + :param change_address: If inputs are provided, any unspent amount will be sent to this address. @@ -413,8 +418,7 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): :return: Array containing the trytes of the new bundle. - This value can be provided to :py:meth:`broadcastTransaction` - and/or :py:meth:`storeTransaction`. + This value can be provided to :py:meth:`broadcast_and_store`. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers @@ -470,7 +474,7 @@ def get_new_addresses(self, index=None, count=1): return self.getNewAddresses(seed=self.seed, index=index, count=count) def get_bundle(self, transaction): - # type: (TransactionHash) -> List[Bundle] + # type: (TransactionHash) -> Bundle """ Returns the bundle associated with the specified transaction hash. diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 558eaed..18386d2 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -64,20 +64,14 @@ class BaseCommand(with_metaclass(CommandMeta)): """An API command ready to send to the node.""" command = None # Text - def __init__(self, adapter, prepare_request=True): + def __init__(self, adapter): # type: (BaseAdapter, bool) -> None """ :param adapter: Adapter that will send request payloads to the node. - - :param prepare_request: - Whether to prepare the request before sending it. - Generally, this should be set to ``True``. """ self.adapter = adapter - self.prepare_request = prepare_request - self.called = False self.request = None # type: dict self.response = None # type: dict @@ -97,10 +91,9 @@ def __call__(self, **kwargs): self.request = kwargs - if self.prepare_request: - replacement = self._prepare_request(self.request) - if replacement is not None: - self.request = replacement + replacement = self._prepare_request(self.request) + if replacement is not None: + self.request = replacement self.response = self._execute(self.request) diff --git a/iota/commands/extended/broadcast_and_store.py b/iota/commands/extended/broadcast_and_store.py index 658f53d..b6c7cea 100644 --- a/iota/commands/extended/broadcast_and_store.py +++ b/iota/commands/extended/broadcast_and_store.py @@ -27,14 +27,5 @@ def get_response_filter(self): pass def _execute(self, request): - bt_command = BroadcastTransactionsCommand( - adapter = self.adapter, - prepare_request = self.prepare_request, - ) - bt_command(**request) - - # `storeTransactions` accepts the exact same request object as - # `broadcastTransactions`, so it's safe to bypass request - # validation here. - return \ - StoreTransactionsCommand(self.adapter, prepare_request=False)(**request) + BroadcastTransactionsCommand(self.adapter)(**request) + return StoreTransactionsCommand(self.adapter)(**request) diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index f601a09..e42164f 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -2,10 +2,10 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Optional +from typing import List, Optional import filters as f -from iota import BadApiResponse +from iota import Address, BadApiResponse from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.commands.core.get_balances import GetBalancesCommand @@ -34,13 +34,10 @@ def get_response_filter(self): pass def _execute(self, request): - # Optional parameters. - end = request.get('end') # type: Optional[int] - threshold = request.get('threshold') # type: Optional[int] - - # Required parameters. - start = request['start'] # type: int - seed = request['seed'] # type: Seed + end = request['end'] # type: Optional[int] + seed = request['seed'] # type: Seed + start = request['start'] # type: int + threshold = request['threshold'] # type: Optional[int] generator = AddressGenerator(seed) @@ -49,7 +46,7 @@ def _execute(self, request): # This is similar to the ``getNewAddresses`` command, except it # is interested in all the addresses that `getNewAddresses` # skips. - addresses = [] + addresses = [] # type: List[Address] for addy in generator.create_generator(start): ft_response = FindTransactionsCommand(self.adapter)(addresses=[addy]) @@ -71,6 +68,8 @@ def _execute(self, request): threshold_met = threshold is None for i, balance in enumerate(gb_response['balances']): + addresses[i].balance = balance + if balance: result['inputs'].append({ 'address': addresses[i], @@ -92,18 +91,16 @@ def _execute(self, request): # troubleshooting. raise with_context( exc = BadApiResponse( - message = - 'Accumulated balance {balance} is less than threshold {threshold} ' - '(``exc.context["inputs"]`` contains more information).'.format( - threshold = threshold, - balance = result['totalBalance'], - ), - - request = request, + 'Accumulated balance {balance} is less than threshold {threshold} ' + '(``exc.context`` contains more information).'.format( + threshold = threshold, + balance = result['totalBalance'], + ), ), context = { 'inputs': result['inputs'], + 'request': request, 'total_balance': result['totalBalance'], }, ) diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfers.py index 45a89c7..ffdfdfa 100644 --- a/iota/commands/extended/prepare_transfers.py +++ b/iota/commands/extended/prepare_transfers.py @@ -2,10 +2,16 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import List, Optional + import filters as f -from iota import Address, Transaction, TransactionHash +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.types import Seed +from iota.exceptions import with_context from iota.filters import Trytes __all__ = [ @@ -28,9 +34,74 @@ def get_response_filter(self): pass def _execute(self, request): - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) + # Required parameters. + seed = request['seed'] # type: Seed + bundle = ProposedBundle(request['transfers']) + + # Optional parameters. + change_address = request.get('change_address') # type: Optional[Address] + proposed_inputs = request.get('inputs') or [] # type: List[Address] + + want_to_spend = bundle.balance + if want_to_spend > 0: + # We are spending inputs, so we need to gather and sign them. + if proposed_inputs: + # Inputs provided. Check to make sure we have sufficient + # balance. + available_to_spend = 0 + confirmed_inputs = [] + + gb_response = GetBalancesCommand(self.adapter)( + addresses = [i.address for i in proposed_inputs], + ) + + for i, balance in enumerate(gb_response.get('balances') or []): + if balance > 0: + available_to_spend += balance + confirmed_inputs.append(proposed_inputs[i]) + + if available_to_spend < want_to_spend: + raise with_context( + exc = BadApiResponse( + 'Insufficient balance; found {found}, need {need} ' + '(``exc.context`` has more info).'.format( + found = available_to_spend, + need = want_to_spend, + ), + ), + + context = { + 'available_to_spend': available_to_spend, + 'confirmed_inputs': confirmed_inputs, + 'request': request, + 'want_to_spend': want_to_spend, + }, + ) + else: + # No inputs provided. Scan addresses for unspent inputs. + gi_response = GetInputsCommand(self.adapter)( + seed = seed, + threshold = want_to_spend, + ) + + confirmed_inputs = [ + input_['address'] + for input_ in gi_response['inputs'] + ] + + bundle.add_inputs(confirmed_inputs) + + if bundle.balance: + if not change_address: + change_address = GetNewAddressesCommand(self.adapter)(seed=seed)[0] + + bundle.send_unspent_inputs_to(change_address) + + bundle.finalize() + + # :todo: Sign inputs. + + return bundle class PrepareTransfersRequestFilter(RequestFilter): @@ -41,13 +112,13 @@ def __init__(self): 'seed': f.Required | Trytes(result_type=Seed), 'transfers': - f.Required | f.Array | f.FilterRepeater(f.Type(Transaction)), + f.Required | f.Array | f.FilterRepeater(f.Type(ProposedTransaction)), # Optional parameters. 'change_address': Trytes(result_type=Address), 'inputs': - f.Array | f.FilterRepeater(Trytes(result_type=TransactionHash)), + f.Array | f.FilterRepeater(Trytes(result_type=Address)), }, allow_missing_keys = { diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index 87fddc1..ac84029 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -32,13 +32,11 @@ def get_response_filter(self): def _execute(self, request): # Call ``getTransactionsToApprove`` to locate trunk and branch # transactions so that we can attach the bundle to the Tangle. - gta_command = GetTransactionsToApproveCommand( - adapter = self.adapter, - prepare_request = self.prepare_request, + gta_response = GetTransactionsToApproveCommand(self.adapter)( + depth = request['depth'], ) - gta_response = gta_command(depth=request['depth']) - AttachToTangleCommand(self.adapter, prepare_request=self.prepare_request)( + AttachToTangleCommand(self.adapter)( branch_transaction = gta_response.get('branchTransaction'), trunk_transaction = gta_response.get('trunkTransaction'), @@ -46,11 +44,7 @@ def _execute(self, request): trytes = request['trytes'], ) - # By this point, ``request['trytes']`` has already been validated, - # so we can bypass validation for `broadcastAndStore`. - return BroadcastAndStoreCommand(self.adapter, prepare_request=False)( - trytes = request['trytes'], - ) + return BroadcastAndStoreCommand(self.adapter)(trytes=request['trytes']) class SendTrytesRequestFilter(RequestFilter): diff --git a/iota/types.py b/iota/types.py index 230848f..56f9aac 100644 --- a/iota/types.py +++ b/iota/types.py @@ -2,10 +2,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from calendar import timegm as unix_timestamp from codecs import encode, decode +from datetime import datetime from itertools import chain from typing import Generator, Iterable, List, MutableSequence, \ - Optional, Text, Union + Optional, Text, Tuple, Union from iota import TrytesCodec from iota.crypto import Curl, HASH_LENGTH @@ -17,8 +19,10 @@ 'Address', 'AddressChecksum', 'Bundle', - 'BundleId', + 'BundleHash', 'Hash', + 'ProposedBundle', + 'ProposedTransaction', 'Tag', 'Transaction', 'TransactionHash', @@ -246,6 +250,10 @@ def __bytes__(self): if PY2: __str__ = __bytes__ + def __bool__(self): + # type: () -> bool + return bool(self._trytes) and any(t != b'9' for t in self) + def __len__(self): # type: () -> int return len(self._trytes) @@ -253,7 +261,7 @@ def __len__(self): def __iter__(self): # type: () -> Generator[binary_type] # :see: http://stackoverflow.com/a/14267935/ - return (self._trytes[i:i + 1] for i in range(len(self))) + return (binary_type(self._trytes[i:i + 1]) for i in range(len(self))) def __contains__(self, other): # type: (TrytesCompatible) -> bool @@ -472,6 +480,16 @@ def __init__(self, trytes): # Make the address sans checksum accessible. self.address = self[:self.LEN] # type: TryteString + self.balance = None # type: Optional[int] + """ + Balance owned by this address. + Must be set manually via the ``getInputs`` command. + + References: + - :py:class:`iota.commands.extended.get_inputs` + - :py:meth:`ProposedBundle.add_inputs` + """ + def is_checksum_valid(self): # type: () -> bool """ @@ -531,9 +549,9 @@ def __init__(self, trytes): ) -class BundleId(Hash): +class BundleHash(Hash): """ - A TryteString that acts as a bundle ID. + A TryteString that acts as a bundle hash. """ pass @@ -568,9 +586,262 @@ class TransactionHash(Hash): pass +class ProposedTransaction(object): + """ + A transaction that has not yet been attached to the Tangle. + + Provide to :py:meth:`iota.api.Iota.prepare_transfers` to attach to + tangle and publish/store. + """ + MESSAGE_LEN = 2187 + """ + Max number of trytes allowed in a transaction message. + + If a transaction's message is longer than this, it will be split into + multiple transactions automatically when it is added to a bundle. + + See :py:meth:`ProposedBundle.add_transaction` for more info. + """ + + def __init__(self, address, value, tag=None, message=None, timestamp=None): + # type: (Address, int, Optional[Tag], Optional[TrytesCompatible], Optional[int]) -> None + super(ProposedTransaction, self).__init__() + + # See :py:class:`Transaction` for descriptions of these attributes. + self.address = address + self.message = TryteString(message or b'', pad=2187) + self.tag = Tag(tag or b'') + self.value = value + + # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, + # but for compatibility with Python 2, we have to do this the + # old-fashioned way. + # :see: http://stackoverflow.com/q/2775864/ + self.timestamp = timestamp or unix_timestamp(datetime.utcnow().timetuple()) + + # These attributes are set by :py:meth:`ProposedBundle.finalize`. + self.current_index = None # type: Optional[int] + self.last_index = None # type: Optional[int] + self.trunk_transaction_hash = None # type: Optional[TransactionHash] + self.branch_transaction_hash = None # type: Optional[TransactionHash] + self.signature_message_fragment = None # type: Optional[TryteString] + self.nonce = None # type: Optional[Hash] + + @property + def timestamp_trits(self): + # type: () -> List[int] + """ + Returns the ``timestamp`` attribute expressed as trits. + """ + return trits_from_int(self.timestamp, pad=27) + + @property + def value_trits(self): + # type: () -> List[int] + """ + Returns the ``value`` attribute expressed as trits. + """ + return trits_from_int(self.value, pad=81) + + @property + def current_index_trits(self): + # type: () -> List[int] + """ + Returns the ``current_index`` attribute expressed as trits. + """ + return trits_from_int(self.current_index, pad=27) + + @property + def last_index_trits(self): + # type: () -> List[int] + """ + Returns the ``last_index`` attribute expressed as trits. + """ + return trits_from_int(self.last_index, pad=27) + + +class ProposedBundle(object): + """ + A collection of proposed transactions, to be treated as an atomic + unit when attached to the Tangle. + + Conceptually, a bundle is similar to a block in a blockchain. + """ + def __init__(self, transactions=None): + # type: (Optional[Iterable[ProposedTransaction]]) -> None + super(ProposedBundle, self).__init__() + + self.hash = None # type: Optional[Hash] + self.tag = None # type: Optional[Tag] + + self.transactions = [] # type: List[ProposedTransaction] + + if transactions: + for t in transactions: + self.add_transaction(t) + + @property + def balance(self): + # type: () -> int + """ + Returns the bundle balance. + In order for a bundle to be valid, its balance must be 0: + + - A positive balance means that there aren't enough inputs to + cover the spent amount. + Add more inputs using :py:meth:`add_inputs`. + - A negative balance means that there are unspent inputs. + Use :py:meth:`send_unspent_inputs_to` to send the unspent + inputs to a "change" address. + """ + return sum(t.value for t in self.transactions) + + def __len__(self): + # type: () -> int + """ + Returns te number of transactions in the bundle. + """ + return len(self.transactions) + + def __iter__(self): + # type: () -> Generator[ProposedTransaction] + """ + Iterates over transactions in the bundle. + """ + return iter(self.transactions) + + def add_transaction(self, transaction): + # type: (ProposedTransaction) -> None + """ + Adds a transaction to the bundle. + + If the transaction message is too long, it will be split + automatically into multiple transactions. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + self.transactions.append(ProposedTransaction( + address = transaction.address, + value = transaction.value, + tag = transaction.tag, + message = transaction.message[:ProposedTransaction.MESSAGE_LEN], + timestamp = transaction.timestamp, + )) + + # Last-added transaction determines the bundle tag. + self.tag = transaction.tag or self.tag + + # If the message is too long to fit in a single transactions, + # it must be split up into multiple transactions so that it will + # fit. + fragment = transaction.message[ProposedTransaction.MESSAGE_LEN:] + while fragment: + self.transactions.append(ProposedTransaction( + address = transaction.address, + value = 0, + tag = transaction.tag, + message = fragment[:ProposedTransaction.MESSAGE_LEN], + timestamp = transaction.timestamp, + )) + + fragment = fragment[ProposedTransaction.MESSAGE_LEN:] + + def add_inputs(self, inputs): + # type: (Iterable[Address]) -> None + """ + Adds inputs to spend in the bundle. + + :param inputs: + Addresses to use as the inputs for this bundle. + + IMPORTANT: Must have ``balance`` attribute set! + Use :py:meth:`iota.api.get_inputs` to load inputs with balances. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + for i in inputs: + # Add the input as a transaction. + self.add_transaction(ProposedTransaction( + address = i, + value = -i.balance, + tag = self.tag, + )) + + def send_unspent_inputs_to(self, address): + # type: (Address) -> None + """ + Adds a transaction to send "change" (unspent inputs) to the + specified address. + + If the bundle has no unspent inputs, this method does nothing. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + # Negative balance means that there are unspent inputs. + # See :py:meth:`balance` for more info. + unspent_inputs = -self.balance + + if unspent_inputs > 0: + self.add_transaction(ProposedTransaction( + address = address, + value = unspent_inputs, + tag = self.tag, + )) + + def finalize(self): + # type: () -> None + """ + Finalizes the bundle, preparing it to be attached to the Tangle. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + balance = self.balance + if balance > 0: + raise ValueError( + 'Inputs are insufficient to cover bundle spend ' + '(balance: {balance}).'.format( + balance = balance, + ), + ) + elif balance < 0: + raise ValueError( + 'Bundle has unspent inputs (balance: {balance}).'.format( + balance = balance, + ), + ) + + sponge = Curl() + last_index = len(self) - 1 + + for (i, t) in enumerate(self): # type: Tuple[int, ProposedTransaction] + t.current_index = i + t.last_index = last_index + + sponge.absorb( + # Ensure checksum is not included. + t.address.address.as_trits() + + t.value_trits + + t.tag.as_trits() + + t.timestamp_trits + + t.current_index_trits + + t.last_index_trits + ) + + bundle_hash = [0] * HASH_LENGTH # type: MutableSequence[int] + sponge.squeeze(bundle_hash) + self.hash = Hash.from_trits(bundle_hash) + + for t in self: + t.bundle_hash = self.hash + + class Transaction(object): """ - A message [to be] published to the Tangle. + A transaction that has been attached to the Tangle. """ @classmethod def from_tryte_string(cls, trytes): @@ -589,13 +860,13 @@ def from_tryte_string(cls, trytes): return cls( hash_ = TransactionHash.from_trits(hash_), signature_message_fragment = tryte_string[0:2187], - recipient = Address(tryte_string[2187:2268]), + address = Address(tryte_string[2187:2268]), value = int_from_trits(tryte_string[2268:2295].as_trits()), tag = Tag(tryte_string[2295:2322]), timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), current_index = int_from_trits(tryte_string[2331:2340].as_trits()), last_index = int_from_trits(tryte_string[2340:2349].as_trits()), - bundle_id = BundleId(tryte_string[2349:2430]), + bundle_id = BundleHash(tryte_string[2349:2430]), trunk_transaction_id = TransactionHash(tryte_string[2430:2511]), branch_transaction_id = TransactionHash(tryte_string[2511:2592]), nonce = Hash(tryte_string[2592:2673]), @@ -605,7 +876,7 @@ def __init__( self, hash_, signature_message_fragment, - recipient, + address, value, tag, timestamp, @@ -617,19 +888,62 @@ def __init__( nonce, ): # type: (Hash, TryteString, Address, int, Tag, int, int, int, Hash, TransactionHash, TransactionHash, Hash) -> None - self.hash = hash_ - self.bundle_id = bundle_id + self.hash = hash_ + """ + Transaction ID, generated by taking a hash of the transaction + trits. + """ + + self.bundle_id = bundle_id + """ + Bundle ID, generated by taking a hash of all the transactions in + the bundle. + """ - self.recipient = recipient - self.value = value + self.address = address + """ + The address associated with this transaction. + If ``value`` is != 0, the associated address' balance is adjusted + as a result of this transaction. + """ + + self.value = value + """ + Amount to adjust the balance of ``address``. + Can be negative (i.e., for spending inputs). + """ self.tag = tag + """ + Optional classification tag applied to this transaction. + """ + + self.nonce = nonce + """ + Unique value used to increase security of the transaction hash. + """ + + self.timestamp = timestamp + """ + Timestamp used to increase the security of the transaction hash. - self.nonce = nonce - self.timestamp = timestamp + IMPORTANT: This value is easy to forge! + Do not rely on it when resolving conflicts! + """ + + self.current_index = current_index + """ + The position of the transaction inside the bundle. - self.current_index = current_index - self.last_index = last_index + For value transfers, the "spend" transaction is generally in the + 0th position, followed by inputs, and the "change" transaction is + last. + """ + + self.last_index = last_index + """ + The position of the final transaction inside the bundle. + """ self.branch_transaction_id = branch_transaction_id self.trunk_transaction_id = trunk_transaction_id @@ -662,7 +976,7 @@ def as_tryte_string(self): """ return ( self.signature_message_fragment - + self.recipient + + self.address + TryteString.from_trits(trits_from_int(self.value, pad=81)) + self.tag + TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) diff --git a/test/__init__.py b/test/__init__.py index 230ae2c..dd71723 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -5,7 +5,9 @@ from collections import defaultdict from typing import Dict, List, Optional, Text -from iota.adapter import BaseAdapter, BadApiResponse +from iota import BadApiResponse +from iota.adapter import BaseAdapter +from iota.exceptions import with_context class MockAdapter(BaseAdapter): @@ -60,19 +62,21 @@ def send_request(self, payload, **kwargs): try: response = self.responses[command].pop(0) except (KeyError, IndexError): - raise BadApiResponse( - message = ( + raise with_context( + exc = BadApiResponse( 'Unknown request {command!r} (expected one of: {seeds!r}).'.format( command = command, seeds = list(sorted(self.responses.keys())), - ) + ), ), - request = payload, + context = { + 'request': payload, + }, ) error = response.get('exception') or response.get('error') if error: - raise BadApiResponse(error, payload) + raise with_context(BadApiResponse(error), context={'request': payload}) return response diff --git a/test/types_test.py b/test/types_test.py index 1dbc81f..c7c6b13 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -8,7 +8,7 @@ from iota import ( Address, AddressChecksum, - BundleId, + BundleHash, Hash, Tag, Transaction, @@ -82,6 +82,21 @@ def test_comparison_error_wrong_type(self): self.assertFalse(trytes is 'RBTC9D9DCDQAEASBYBCCKBFA') self.assertTrue(trytes is not 'RBTC9D9DCDQAEASBYBCCKBFA') + def test_bool_cast(self): + """ + Casting a TryteString as a boolean. + """ + # Empty TryteString evaluates to False. + self.assertIs(bool(TryteString(b'')), False) + + # TryteString that is nothing but padding also evaluates to False. + self.assertIs(bool(TryteString(b'9')), False) + self.assertIs(bool(TryteString(b'', pad=1024)), False) + + # A single non-padding tryte evaluates to True. + self.assertIs(bool(TryteString(b'A')), True) + self.assertIs(bool(TryteString(b'9'*1024 + b'Z')), True) + def test_container(self): """ Checking whether a TryteString contains a sequence. @@ -818,6 +833,96 @@ def test_init_error_too_long(self): AddressChecksum(b'FOXM9MUBX9') +class ProposedBundleTestCase(TestCase): + def test_add_transaction_short_message(self): + """ + Adding a transaction to a bundle, with a message short enough to + fit inside a single transaction. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_transaction_long_message(self): + """ + Adding a transaction to a bundle, with a message so long that it + has to be split into multiple transactions. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_transaction_error_already_finalized(self): + """ + Attempting to add a transaction to a bundle that is already + finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_inputs_balanced(self): + """ + Adding inputs to cover the exact amount of the bundle spend. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_inputs_with_change(self): + """ + Adding inputs to a bundle results in unspent inputs. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_inputs_error_already_finalized(self): + """ + Attempting to add inputs to a bundle that is already finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_send_unspent_inputs_to_unbalanced(self): + """ + Invoking ``send_unspent_inputs_to`` on an unbalanced bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_send_unspent_inputs_to_balanced(self): + """ + Invoking ``send_unspent_inputs_to`` on a balanced bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_send_unspent_inputs_to_error_already_finalized(self): + """ + Invoking ``send_unspent_inputs_to`` on a bundle that is already + finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_finalize_happy_path(self): + """ + Finalizing a bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_finalize_error_already_finalized(self): + """ + Attempting to finalize a bundle that is already finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_finalize_error_unbalanced(self): + """ + Attempting to finalize an unbalanced bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + # noinspection SpellCheckingInspection class TagTestCase(TestCase): def test_init_automatic_pad(self): @@ -973,7 +1078,7 @@ def test_from_tryte_string(self): ) self.assertEqual( - transaction.recipient, + transaction.address, Address( b'9999999999999999999999999999999999999999' @@ -990,7 +1095,7 @@ def test_from_tryte_string(self): self.assertEqual( transaction.bundle_id, - BundleId( + BundleHash( b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' ), @@ -1089,7 +1194,7 @@ def test_as_tryte_string(self): b'999999999' ), - recipient = + address = Address( b'9999999999999999999999999999999999999999' b'99999999999999999999999999999999999999999' @@ -1102,7 +1207,7 @@ def test_as_tryte_string(self): last_index = 1, bundle_id = - BundleId( + BundleHash( b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' ), From 2607a06b9642e659077f0ab1e16c712b55860f2c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 22 Dec 2016 22:25:29 -0500 Subject: [PATCH 179/239] Fixed TryteString boolean cast in Python 2. --- iota/types.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/iota/types.py b/iota/types.py index 56f9aac..1dfa729 100644 --- a/iota/types.py +++ b/iota/types.py @@ -246,14 +246,15 @@ def __bytes__(self): """ return binary_type(self._trytes) - # :bc: Magic method has a different name in Python 2. - if PY2: - __str__ = __bytes__ - def __bool__(self): # type: () -> bool return bool(self._trytes) and any(t != b'9' for t in self) + # :bc: Magic methods have different names in Python 2. + if PY2: + __nonzero__ = __bool__ + __str__ = __bytes__ + def __len__(self): # type: () -> int return len(self._trytes) From a86242c44f0f6ec488182502203a16c8fe2d13f1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 12:52:56 -0500 Subject: [PATCH 180/239] Partially-implemented signature fragments. - Renamed `SigningKey` to `PrivateKey`. - Implemented `PrivateKey.create_signature`. - Automatically attach balance and key_index to addresses in ``getInputs`` and ``getBalances``. - Moved transaction-related classes into own package. - Refactored magic number 3. - Renamed ``as_json`` to ``as_json_compatible``. --- iota/__init__.py | 14 + iota/commands/extended/get_inputs.py | 2 +- iota/commands/extended/prepare_transfers.py | 19 +- iota/crypto/addresses.py | 32 +- iota/crypto/signing.py | 14 +- iota/crypto/types.py | 65 ++- iota/json.py | 4 +- iota/transaction.py | 542 ++++++++++++++++++++ iota/types.py | 446 +--------------- test/crypto/signing_test.py | 32 +- test/crypto/types_test.py | 123 ++++- test/transaction_test.py | 459 +++++++++++++++++ test/types_test.py | 440 +--------------- 13 files changed, 1262 insertions(+), 930 deletions(-) create mode 100644 iota/transaction.py create mode 100644 test/transaction_test.py diff --git a/iota/__init__.py b/iota/__init__.py index aff90ba..6c7261c 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -2,7 +2,20 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +# Define a few magic constants. DEFAULT_PORT = 14265 +""" +Default port to use when configuring an adapter, if the port is not +specified. +""" + +TRITS_PER_TRYTE = 3 +""" +Number of trits in a tryte. +Changing this will probably break everything, but there's a chance it +could create a sexy new altcoin instead. +In that way, it's kind of like toxic waste in a superhero story. +""" # Activate TrytesCodec. from .codecs import * @@ -10,6 +23,7 @@ # Make some imports accessible from the top level of the package. # Note that order is important, to prevent circular imports. from .types import * +from .transaction import * from .adapter import * from .api import * diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index e42164f..1181c01 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -74,7 +74,7 @@ def _execute(self, request): result['inputs'].append({ 'address': addresses[i], 'balance': balance, - 'keyIndex': start + i, + 'keyIndex': addresses[i].key_index, }) result['totalBalance'] += balance diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfers.py index ffdfdfa..a92f5ea 100644 --- a/iota/commands/extended/prepare_transfers.py +++ b/iota/commands/extended/prepare_transfers.py @@ -5,11 +5,13 @@ from typing import List, Optional import filters as f -from iota import Address, BadApiResponse, ProposedBundle, ProposedTransaction +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.signing import KeyGenerator from iota.crypto.types import Seed from iota.exceptions import with_context from iota.filters import Trytes @@ -49,16 +51,22 @@ def _execute(self, request): # Inputs provided. Check to make sure we have sufficient # balance. available_to_spend = 0 - confirmed_inputs = [] + confirmed_inputs = [] # type: List[Address] gb_response = GetBalancesCommand(self.adapter)( addresses = [i.address for i in proposed_inputs], ) for i, balance in enumerate(gb_response.get('balances') or []): + input_ = proposed_inputs[i] + if balance > 0: available_to_spend += balance - confirmed_inputs.append(proposed_inputs[i]) + + # Update the address balance from the API response, just in + # case somebody tried to cheat. + input_.balance = balance + confirmed_inputs.append(input_) if available_to_spend < want_to_spend: raise with_context( @@ -99,9 +107,10 @@ def _execute(self, request): bundle.finalize() - # :todo: Sign inputs. + if confirmed_inputs: + bundle.sign_inputs(KeyGenerator(seed)) - return bundle + return bundle class PrepareTransfersRequestFilter(RequestFilter): diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 29b9dc9..0467ec6 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -4,10 +4,10 @@ from typing import Generator, Iterable, List, MutableSequence -from iota import Address, TryteString, TrytesCompatible +from iota import Address, TRITS_PER_TRYTE, TryteString, TrytesCompatible from iota.crypto import Curl from iota.crypto.signing import KeyGenerator -from iota.crypto.types import SigningKey +from iota.crypto.types import PrivateKey from iota.exceptions import with_context __all__ = [ @@ -144,23 +144,29 @@ def create_generator(self, start=0, step=1): current = start while current >= 0: - digest = next(digest_generator) # type: List[int] - - # Multiply by 3 to convert from trits to trytes. - address_trits = [0] * (Address.LEN * 3) # type: MutableSequence[int] + yield self.address_from_digest(next(digest_generator), current) + current += step - sponge = Curl() - sponge.absorb(digest) - sponge.squeeze(address_trits) + @staticmethod + def address_from_digest(digest_trits, key_index): + # type: (Iterable[int], int) -> Address + """ + Generates an address from a private key digest. + """ + address_trits = [0] * (Address.LEN * TRITS_PER_TRYTE) # type: MutableSequence[int] - yield Address.from_trits(address_trits) + sponge = Curl() + sponge.absorb(digest_trits) + sponge.squeeze(address_trits) - current += step + address = Address.from_trits(address_trits) + address.key_index = key_index + return address def _create_digest_generator(self, start, step): # type: (int, int) -> Generator[List[int]] """ - Initializes a generator to create SigningKey digests. + Initializes a generator to create PrivateKey digests. Implemented as a separate method so that it can be mocked during unit tests. @@ -171,5 +177,5 @@ def _create_digest_generator(self, start, step): ) while True: - signing_key = next(key_generator) # type: SigningKey + signing_key = next(key_generator) # type: PrivateKey yield signing_key.get_digest_trits() diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index b66f75a..19bfc68 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -4,9 +4,9 @@ from typing import Generator, List, MutableSequence -from iota import TryteString, TrytesCompatible +from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible from iota.crypto import Curl, HASH_LENGTH -from iota.crypto.types import SigningKey +from iota.crypto.types import PrivateKey from iota.exceptions import with_context __all__ = [ @@ -30,7 +30,7 @@ def __init__(self, seed): self.seed = TryteString(seed) def get_keys(self, start, count=1, step=1, iterations=1): - # type: (int, int, int, int) -> List[SigningKey] + # type: (int, int, int, int) -> List[PrivateKey] """ Generates and returns one or more keys at the specified index(es). @@ -106,7 +106,7 @@ def get_keys(self, start, count=1, step=1, iterations=1): return keys def create_generator(self, start=0, step=1, iterations=1): - # type: (int, int) -> Generator[SigningKey] + # type: (int, int) -> Generator[PrivateKey] """ Creates a generator that can be used to progressively generate new keys. @@ -161,7 +161,7 @@ def create_generator(self, start=0, step=1, iterations=1): sponge = self._create_sponge(current) # Multiply by 3 to convert trytes into trits. - block_length = SigningKey.BLOCK_LEN * 3 + block_length = PrivateKey.BLOCK_LEN * TRITS_PER_TRYTE key = [0] * (block_length * iterations) buffer = [0] * HASH_LENGTH # type: MutableSequence[int] @@ -177,7 +177,9 @@ def create_generator(self, start=0, step=1, iterations=1): key[key_start:key_stop] = buffer - yield SigningKey.from_trits(key) + private_key = PrivateKey.from_trits(key) + private_key.key_index = current + yield private_key current += step diff --git a/iota/crypto/types.py b/iota/crypto/types.py index 957eb01..4367df5 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -2,18 +2,17 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from math import ceil from os import urandom from typing import Callable, List, Optional -from iota import Hash, TryteString, TrytesCompatible +from iota import Hash, TRITS_PER_TRYTE, TryteString, TrytesCompatible from iota.crypto import HASH_LENGTH, Curl -from math import ceil - from iota.exceptions import with_context from six import binary_type __all__ = [ - 'SigningKey', + 'PrivateKey', ] @@ -47,9 +46,9 @@ def random(cls, length=Hash.LEN, source=urandom): return cls.from_bytes(source(int(ceil(length / 2)))) -class SigningKey(TryteString): +class PrivateKey(TryteString): """ - A TryteString that acts as a signing key, e.g., for generating + A TryteString that acts as a private key, e.g., for generating message signatures, new addresses, etc. """ BLOCK_LEN = 2187 @@ -58,9 +57,9 @@ class SigningKey(TryteString): by a certain number of trytes. """ - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(SigningKey, self).__init__(trytes) + def __init__(self, trytes, key_index=None): + # type: (TrytesCompatible, Optional[int]) -> None + super(PrivateKey, self).__init__(trytes) if len(self._trytes) % self.BLOCK_LEN: raise with_context( @@ -76,6 +75,8 @@ def __init__(self, trytes): }, ) + self.key_index = key_index + @property def block_count(self): # type: () -> int @@ -84,6 +85,49 @@ def block_count(self): """ return len(self) // self.BLOCK_LEN + def create_signature(self, trytes, blocks=None): + # type: (TryteString, Optional[int]) -> TryteString + """ + Creates a signature for the specified trytes. + + :param trytes: + The trytes to use to build the signature. + + :param blocks: + Max length of the resulting signature, expressed in blocks. + + See :py:attr:`BLOCK_LEN` for more info. + """ + signature = self.as_trits() + if blocks is not None: + signature = signature[:self.BLOCK_LEN*blocks*TRITS_PER_TRYTE] + + hash_count = int(ceil(len(signature) / HASH_LENGTH)) + + source = trytes.as_trits() + source += [0] * max(0, hash_count - len(source)) + + sponge = Curl() + + # Build signature, one hash at a time. + for i in range(hash_count): + start = i * HASH_LENGTH + stop = start + HASH_LENGTH + + fragment = signature[start:stop] + + # Use value from the source trits to make the signature + # deterministic. + for j in range(13 - source[i]): + sponge.reset() + sponge.absorb(fragment) + sponge.squeeze(fragment) + + # Copy the signature fragment to the final signature. + signature[start:stop] = fragment + + return signature + def get_digest_trits(self): # type: () -> List[int] """ @@ -96,8 +140,7 @@ def get_digest_trits(self): through a PBKDF, yielding a constant-length hash that can be used for crypto. """ - # Multiply by 3 to convert trytes into trits. - block_size = self.BLOCK_LEN * 3 + block_size = self.BLOCK_LEN * TRITS_PER_TRYTE raw_trits = self.as_trits() # Initialize list with the correct length to improve performance. diff --git a/iota/json.py b/iota/json.py index 6336d95..a1f5f7d 100644 --- a/iota/json.py +++ b/iota/json.py @@ -10,6 +10,6 @@ class JsonEncoder(BaseJsonEncoder): """JSON encoder with support for custom types.""" def default(self, o): - if hasattr(o, 'as_json'): - return o.as_json() + if hasattr(o, 'as_json_compatible'): + return o.as_json_compatible() return super(JsonEncoder, self).default(o) diff --git a/iota/transaction.py b/iota/transaction.py new file mode 100644 index 0000000..212ab76 --- /dev/null +++ b/iota/transaction.py @@ -0,0 +1,542 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from calendar import timegm as unix_timestamp +from datetime import datetime + +from typing import Generator, Iterable, List, MutableSequence, Optional, Tuple + +from iota import Address, Hash, Tag, TrytesCompatible, TryteString, \ + int_from_trits, trits_from_int +from iota.crypto import Curl, HASH_LENGTH +from iota.crypto.addresses import AddressGenerator +from iota.crypto.signing import KeyGenerator +from iota.exceptions import with_context + +__all__ = [ + 'Bundle', + 'BundleHash', + 'ProposedBundle', + 'ProposedTransaction', + 'Transaction', + 'TransactionHash', +] + + +# Custom types for type hints and docstrings. +Bundle = Iterable['Transaction'] + + +class BundleHash(Hash): + """ + A TryteString that acts as a bundle hash. + """ + pass + + +class TransactionHash(Hash): + """ + A TryteString that acts as a transaction hash. + """ + pass + + +class ProposedTransaction(object): + """ + A transaction that has not yet been attached to the Tangle. + + Provide to :py:meth:`iota.api.Iota.prepare_transfers` to attach to + tangle and publish/store. + """ + MESSAGE_LEN = 2187 + """ + Max number of trytes allowed in a transaction message. + + If a transaction's message is longer than this, it will be split into + multiple transactions automatically when it is added to a bundle. + + See :py:meth:`ProposedBundle.add_transaction` for more info. + """ + + def __init__(self, address, value, tag=None, message=None, timestamp=None): + # type: (Address, int, Optional[Tag], Optional[TrytesCompatible], Optional[int]) -> None + super(ProposedTransaction, self).__init__() + + # See :py:class:`Transaction` for descriptions of these attributes. + self.address = address + self.message = TryteString(message or b'', pad=2187) + self.tag = Tag(tag or b'') + self.value = value + + # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, + # but for compatibility with Python 2, we have to do this the + # old-fashioned way. + # :see: http://stackoverflow.com/q/2775864/ + self.timestamp = timestamp or unix_timestamp(datetime.utcnow().timetuple()) + + # These attributes are set by :py:meth:`ProposedBundle.finalize`. + self.current_index = None # type: Optional[int] + self.last_index = None # type: Optional[int] + self.trunk_transaction_hash = None # type: Optional[TransactionHash] + self.branch_transaction_hash = None # type: Optional[TransactionHash] + self.signature_message_fragment = None # type: Optional[TryteString] + self.nonce = None # type: Optional[Hash] + + @property + def timestamp_trits(self): + # type: () -> List[int] + """ + Returns the ``timestamp`` attribute expressed as trits. + """ + return trits_from_int(self.timestamp, pad=27) + + @property + def value_trits(self): + # type: () -> List[int] + """ + Returns the ``value`` attribute expressed as trits. + """ + return trits_from_int(self.value, pad=81) + + @property + def current_index_trits(self): + # type: () -> List[int] + """ + Returns the ``current_index`` attribute expressed as trits. + """ + return trits_from_int(self.current_index, pad=27) + + @property + def last_index_trits(self): + # type: () -> List[int] + """ + Returns the ``last_index`` attribute expressed as trits. + """ + return trits_from_int(self.last_index, pad=27) + + +class ProposedBundle(object): + """ + A collection of proposed transactions, to be treated as an atomic + unit when attached to the Tangle. + + Conceptually, a bundle is similar to a block in a blockchain. + """ + def __init__(self, transactions=None): + # type: (Optional[Iterable[ProposedTransaction]]) -> None + super(ProposedBundle, self).__init__() + + self.hash = None # type: Optional[Hash] + self.tag = None # type: Optional[Tag] + + self._transactions = [] # type: List[ProposedTransaction] + + if transactions: + for t in transactions: + self.add_transaction(t) + + @property + def balance(self): + # type: () -> int + """ + Returns the bundle balance. + In order for a bundle to be valid, its balance must be 0: + + - A positive balance means that there aren't enough inputs to + cover the spent amount. + Add more inputs using :py:meth:`add_inputs`. + - A negative balance means that there are unspent inputs. + Use :py:meth:`send_unspent_inputs_to` to send the unspent + inputs to a "change" address. + """ + return sum(t.value for t in self._transactions) + + def __len__(self): + # type: () -> int + """ + Returns te number of transactions in the bundle. + """ + return len(self._transactions) + + def __iter__(self): + # type: () -> Generator[ProposedTransaction] + """ + Iterates over transactions in the bundle. + """ + return iter(self._transactions) + + def __getitem__(self, index): + # type: (int) -> ProposedTransaction + """ + Returns the transaction at the specified index. + """ + return self._transactions[index] + + def add_transaction(self, transaction): + # type: (ProposedTransaction) -> None + """ + Adds a transaction to the bundle. + + If the transaction message is too long, it will be split + automatically into multiple transactions. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + if transaction.value < 0: + raise ValueError('Use ``add_inputs`` to add inputs to the bundle.') + + self._transactions.append(ProposedTransaction( + address = transaction.address, + value = transaction.value, + tag = transaction.tag, + message = transaction.message[:ProposedTransaction.MESSAGE_LEN], + timestamp = transaction.timestamp, + )) + + # Last-added transaction determines the bundle tag. + self.tag = transaction.tag or self.tag + + # If the message is too long to fit in a single transactions, + # it must be split up into multiple transactions so that it will + # fit. + fragment = transaction.message[ProposedTransaction.MESSAGE_LEN:] + while fragment: + self._transactions.append(ProposedTransaction( + address = transaction.address, + value = 0, + tag = transaction.tag, + message = fragment[:ProposedTransaction.MESSAGE_LEN], + timestamp = transaction.timestamp, + )) + + fragment = fragment[ProposedTransaction.MESSAGE_LEN:] + + def add_inputs(self, inputs): + # type: (Iterable[Address]) -> None + """ + Adds inputs to spend in the bundle. + + Note that each input requires two transactions, in order to + hold the entire signature. + + :param inputs: + Addresses to use as the inputs for this bundle. + + IMPORTANT: Must have ``balance`` and ``key_index`` attributes! + Use :py:meth:`iota.api.get_inputs` to prepare inputs. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + for i in inputs: + if i.balance is None: + raise with_context( + exc = ValueError( + 'Address {address} has null ``balance`` ' + '(``exc.context`` has more info).'.format( + address = i, + ), + ), + + context = { + 'address': i, + }, + ) + + if i.key_index is None: + raise with_context( + exc = ValueError( + 'Address {address} has null ``key_index`` ' + '(``exc.context`` has more info).'.format( + address = i, + ), + ), + + context = { + 'address': i, + }, + ) + + # Add the input as a transaction. + self.add_transaction(ProposedTransaction( + address = i, + value = -i.balance, + tag = self.tag, + )) + + # Signatures require two transactions to store, due to + # transaction length limit. + self.add_transaction(ProposedTransaction( + address = i, + value = 0, + tag = self.tag, + )) + + def send_unspent_inputs_to(self, address): + # type: (Address) -> None + """ + Adds a transaction to send "change" (unspent inputs) to the + specified address. + + If the bundle has no unspent inputs, this method does nothing. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + # Negative balance means that there are unspent inputs. + # See :py:meth:`balance` for more info. + unspent_inputs = -self.balance + + if unspent_inputs > 0: + self.add_transaction(ProposedTransaction( + address = address, + value = unspent_inputs, + tag = self.tag, + )) + + def finalize(self): + # type: () -> None + """ + Finalizes the bundle, preparing it to be attached to the Tangle. + """ + if self.hash: + raise RuntimeError('Bundle is already finalized.') + + balance = self.balance + if balance > 0: + raise ValueError( + 'Inputs are insufficient to cover bundle spend ' + '(balance: {balance}).'.format( + balance = balance, + ), + ) + elif balance < 0: + raise ValueError( + 'Bundle has unspent inputs (balance: {balance}).'.format( + balance = balance, + ), + ) + + sponge = Curl() + last_index = len(self) - 1 + + for (i, t) in enumerate(self): # type: Tuple[int, ProposedTransaction] + t.current_index = i + t.last_index = last_index + + sponge.absorb( + # Ensure checksum is not included. + t.address.address.as_trits() + + t.value_trits + + t.tag.as_trits() + + t.timestamp_trits + + t.current_index_trits + + t.last_index_trits + ) + + bundle_hash = [0] * HASH_LENGTH # type: MutableSequence[int] + sponge.squeeze(bundle_hash) + self.hash = Hash.from_trits(bundle_hash) + + for t in self: + t.bundle_hash = self.hash + + def sign_inputs(self, key_generator): + # type: (KeyGenerator) -> None + """ + Sign inputs in a finalized bundle. + """ + if not self.hash: + raise RuntimeError('Cannot sign inputs until bundle is finalized.') + + # Use a counter for the loop so that we can skip ahead as we go. + i = 0 + while i < len(self): + txn = self[i] + + if txn.value < 0: + # In order to sign the input, we need to know the index of + # the private key used to generate it. + if txn.address.key_index is None: + raise with_context( + exc = ValueError( + 'Unable to sign input {input}; key index is None ' + '(``exc.context`` has more info).'.format( + input = txn.address, + ), + ), + + context = { + 'transaction': txn, + }, + ) + + private_key =\ + key_generator.get_keys( + start = txn.address.key_index, + iterations = AddressGenerator.DIGEST_ITERATIONS + )[0] + + # Signatures are too long to fit inside a single transaction, + # so we must split it into two parts (this is why we created + # two transactions per input in :py:meth:`add_inputs`). + txn.signature_message_fragment = private_key.create_signature( + trytes = self.hash[:9], + blocks = 1, + ) + + # :todo: Second signature fragment. + + i += 1 + + +class Transaction(object): + """ + A transaction that has been attached to the Tangle. + """ + @classmethod + def from_tryte_string(cls, trytes): + # type: (TrytesCompatible) -> Transaction + """ + Creates a Transaction object from a sequence of trytes. + """ + tryte_string = TryteString(trytes) + + hash_ = [0] * HASH_LENGTH # type: MutableSequence[int] + + sponge = Curl() + sponge.absorb(tryte_string.as_trits()) + sponge.squeeze(hash_) + + return cls( + hash_ = TransactionHash.from_trits(hash_), + signature_message_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]), + timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), + current_index = int_from_trits(tryte_string[2331:2340].as_trits()), + last_index = int_from_trits(tryte_string[2340:2349].as_trits()), + bundle_id = BundleHash(tryte_string[2349:2430]), + trunk_transaction_id = TransactionHash(tryte_string[2430:2511]), + branch_transaction_id = TransactionHash(tryte_string[2511:2592]), + nonce = Hash(tryte_string[2592:2673]), + ) + + def __init__( + self, + hash_, + signature_message_fragment, + address, + value, + tag, + timestamp, + current_index, + last_index, + bundle_id, + trunk_transaction_id, + branch_transaction_id, + nonce, + ): + # type: (Hash, TryteString, Address, int, Tag, int, int, int, Hash, TransactionHash, TransactionHash, Hash) -> None + self.hash = hash_ + """ + Transaction ID, generated by taking a hash of the transaction + trits. + """ + + self.bundle_id = bundle_id + """ + Bundle ID, generated by taking a hash of all the transactions in + the bundle. + """ + + self.address = address + """ + The address associated with this transaction. + If ``value`` is != 0, the associated address' balance is adjusted + as a result of this transaction. + """ + + self.value = value + """ + Amount to adjust the balance of ``address``. + Can be negative (i.e., for spending inputs). + """ + + self.tag = tag + """ + Optional classification tag applied to this transaction. + """ + + self.nonce = nonce + """ + Unique value used to increase security of the transaction hash. + """ + + self.timestamp = timestamp + """ + Timestamp used to increase the security of the transaction hash. + + IMPORTANT: This value is easy to forge! + Do not rely on it when resolving conflicts! + """ + + self.current_index = current_index + """ + The position of the transaction inside the bundle. + + For value transfers, the "spend" transaction is generally in the + 0th position, followed by inputs, and the "change" transaction is + last. + """ + + self.last_index = last_index + """ + The position of the final transaction inside the bundle. + """ + + self.branch_transaction_id = branch_transaction_id + self.trunk_transaction_id = trunk_transaction_id + + self.signature_message_fragment =\ + TryteString(signature_message_fragment or b'') + + self.is_confirmed = None # type: Optional[bool] + """ + Whether this transaction has been confirmed by neighbor nodes. + Must be set manually via the ``getInclusionStates`` API command. + + References: + - :py:meth:`iota.api.StrictIota.get_inclusion_states` + - :py:meth:`iota.api.Iota.get_transfers` + """ + + @property + def is_tail(self): + # type: () -> bool + """ + Returns whether this transaction is a tail. + """ + return self.current_index == 0 + + def as_tryte_string(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction. + """ + return ( + self.signature_message_fragment + + self.address + + TryteString.from_trits(trits_from_int(self.value, pad=81)) + + self.tag + + TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) + + TryteString.from_trits(trits_from_int(self.current_index, pad=27)) + + TryteString.from_trits(trits_from_int(self.last_index, pad=27)) + + self.bundle_id + + self.trunk_transaction_id + + self.branch_transaction_id + + self.nonce + ) diff --git a/iota/types.py b/iota/types.py index 1dfa729..46f125e 100644 --- a/iota/types.py +++ b/iota/types.py @@ -2,14 +2,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from calendar import timegm as unix_timestamp from codecs import encode, decode -from datetime import datetime from itertools import chain from typing import Generator, Iterable, List, MutableSequence, \ - Optional, Text, Tuple, Union + Optional, Text, Union -from iota import TrytesCodec +from iota import TRITS_PER_TRYTE, TrytesCodec from iota.crypto import Curl, HASH_LENGTH from iota.exceptions import with_context from six import PY2, binary_type @@ -18,14 +16,8 @@ __all__ = [ 'Address', 'AddressChecksum', - 'Bundle', - 'BundleHash', 'Hash', - 'ProposedBundle', - 'ProposedTransaction', 'Tag', - 'Transaction', - 'TransactionHash', 'TryteString', 'TrytesCompatible', 'int_from_trits', @@ -34,7 +26,6 @@ # Custom types for type hints and docstrings. -Bundle = Iterable['Transaction'] TrytesCompatible = Union[binary_type, bytearray, 'TryteString'] @@ -362,10 +353,10 @@ def as_bytes(self, errors='strict'): # :bc: In Python 2, `decode` does not accept keyword arguments. return decode(self._trytes, 'trytes', errors) - def as_json(self): + def as_json_compatible(self): # type: () -> Text """ - Converts the TryteString into a JSON representation. + Converts the TryteString into a JSON-compatible value. See :py:class:`iota.json.JsonEncoder`. """ @@ -455,8 +446,8 @@ class Address(TryteString): """ LEN = Hash.LEN - def __init__(self, trytes): - # type: (TrytesCompatible) -> None + def __init__(self, trytes, key_index=None): + # type: (TrytesCompatible, Optional[int]) -> None super(Address, self).__init__(trytes, pad=self.LEN) self.checksum = None @@ -491,6 +482,11 @@ def __init__(self, trytes): - :py:meth:`ProposedBundle.add_inputs` """ + self.key_index = key_index + """ + Index of the key used to generate this address. + """ + def is_checksum_valid(self): # type: () -> bool """ @@ -519,8 +515,7 @@ def _generate_checksum(self): sponge.absorb(self.address.as_trits()) sponge.squeeze(checksum_trits) - # Multiply by 3 to convert trytes into trits. - checksum_length = (AddressChecksum.LEN * 3) + checksum_length = AddressChecksum.LEN * TRITS_PER_TRYTE return TryteString.from_trits(checksum_trits[:checksum_length]) @@ -550,13 +545,6 @@ def __init__(self, trytes): ) -class BundleHash(Hash): - """ - A TryteString that acts as a bundle hash. - """ - pass - - class Tag(TryteString): """ A TryteString that acts as a transaction tag. @@ -578,413 +566,3 @@ def __init__(self, trytes): 'trytes': trytes, }, ) - - -class TransactionHash(Hash): - """ - A TryteString that acts as a transaction hash. - """ - pass - - -class ProposedTransaction(object): - """ - A transaction that has not yet been attached to the Tangle. - - Provide to :py:meth:`iota.api.Iota.prepare_transfers` to attach to - tangle and publish/store. - """ - MESSAGE_LEN = 2187 - """ - Max number of trytes allowed in a transaction message. - - If a transaction's message is longer than this, it will be split into - multiple transactions automatically when it is added to a bundle. - - See :py:meth:`ProposedBundle.add_transaction` for more info. - """ - - def __init__(self, address, value, tag=None, message=None, timestamp=None): - # type: (Address, int, Optional[Tag], Optional[TrytesCompatible], Optional[int]) -> None - super(ProposedTransaction, self).__init__() - - # See :py:class:`Transaction` for descriptions of these attributes. - self.address = address - self.message = TryteString(message or b'', pad=2187) - self.tag = Tag(tag or b'') - self.value = value - - # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, - # but for compatibility with Python 2, we have to do this the - # old-fashioned way. - # :see: http://stackoverflow.com/q/2775864/ - self.timestamp = timestamp or unix_timestamp(datetime.utcnow().timetuple()) - - # These attributes are set by :py:meth:`ProposedBundle.finalize`. - self.current_index = None # type: Optional[int] - self.last_index = None # type: Optional[int] - self.trunk_transaction_hash = None # type: Optional[TransactionHash] - self.branch_transaction_hash = None # type: Optional[TransactionHash] - self.signature_message_fragment = None # type: Optional[TryteString] - self.nonce = None # type: Optional[Hash] - - @property - def timestamp_trits(self): - # type: () -> List[int] - """ - Returns the ``timestamp`` attribute expressed as trits. - """ - return trits_from_int(self.timestamp, pad=27) - - @property - def value_trits(self): - # type: () -> List[int] - """ - Returns the ``value`` attribute expressed as trits. - """ - return trits_from_int(self.value, pad=81) - - @property - def current_index_trits(self): - # type: () -> List[int] - """ - Returns the ``current_index`` attribute expressed as trits. - """ - return trits_from_int(self.current_index, pad=27) - - @property - def last_index_trits(self): - # type: () -> List[int] - """ - Returns the ``last_index`` attribute expressed as trits. - """ - return trits_from_int(self.last_index, pad=27) - - -class ProposedBundle(object): - """ - A collection of proposed transactions, to be treated as an atomic - unit when attached to the Tangle. - - Conceptually, a bundle is similar to a block in a blockchain. - """ - def __init__(self, transactions=None): - # type: (Optional[Iterable[ProposedTransaction]]) -> None - super(ProposedBundle, self).__init__() - - self.hash = None # type: Optional[Hash] - self.tag = None # type: Optional[Tag] - - self.transactions = [] # type: List[ProposedTransaction] - - if transactions: - for t in transactions: - self.add_transaction(t) - - @property - def balance(self): - # type: () -> int - """ - Returns the bundle balance. - In order for a bundle to be valid, its balance must be 0: - - - A positive balance means that there aren't enough inputs to - cover the spent amount. - Add more inputs using :py:meth:`add_inputs`. - - A negative balance means that there are unspent inputs. - Use :py:meth:`send_unspent_inputs_to` to send the unspent - inputs to a "change" address. - """ - return sum(t.value for t in self.transactions) - - def __len__(self): - # type: () -> int - """ - Returns te number of transactions in the bundle. - """ - return len(self.transactions) - - def __iter__(self): - # type: () -> Generator[ProposedTransaction] - """ - Iterates over transactions in the bundle. - """ - return iter(self.transactions) - - def add_transaction(self, transaction): - # type: (ProposedTransaction) -> None - """ - Adds a transaction to the bundle. - - If the transaction message is too long, it will be split - automatically into multiple transactions. - """ - if self.hash: - raise RuntimeError('Bundle is already finalized.') - - self.transactions.append(ProposedTransaction( - address = transaction.address, - value = transaction.value, - tag = transaction.tag, - message = transaction.message[:ProposedTransaction.MESSAGE_LEN], - timestamp = transaction.timestamp, - )) - - # Last-added transaction determines the bundle tag. - self.tag = transaction.tag or self.tag - - # If the message is too long to fit in a single transactions, - # it must be split up into multiple transactions so that it will - # fit. - fragment = transaction.message[ProposedTransaction.MESSAGE_LEN:] - while fragment: - self.transactions.append(ProposedTransaction( - address = transaction.address, - value = 0, - tag = transaction.tag, - message = fragment[:ProposedTransaction.MESSAGE_LEN], - timestamp = transaction.timestamp, - )) - - fragment = fragment[ProposedTransaction.MESSAGE_LEN:] - - def add_inputs(self, inputs): - # type: (Iterable[Address]) -> None - """ - Adds inputs to spend in the bundle. - - :param inputs: - Addresses to use as the inputs for this bundle. - - IMPORTANT: Must have ``balance`` attribute set! - Use :py:meth:`iota.api.get_inputs` to load inputs with balances. - """ - if self.hash: - raise RuntimeError('Bundle is already finalized.') - - for i in inputs: - # Add the input as a transaction. - self.add_transaction(ProposedTransaction( - address = i, - value = -i.balance, - tag = self.tag, - )) - - def send_unspent_inputs_to(self, address): - # type: (Address) -> None - """ - Adds a transaction to send "change" (unspent inputs) to the - specified address. - - If the bundle has no unspent inputs, this method does nothing. - """ - if self.hash: - raise RuntimeError('Bundle is already finalized.') - - # Negative balance means that there are unspent inputs. - # See :py:meth:`balance` for more info. - unspent_inputs = -self.balance - - if unspent_inputs > 0: - self.add_transaction(ProposedTransaction( - address = address, - value = unspent_inputs, - tag = self.tag, - )) - - def finalize(self): - # type: () -> None - """ - Finalizes the bundle, preparing it to be attached to the Tangle. - """ - if self.hash: - raise RuntimeError('Bundle is already finalized.') - - balance = self.balance - if balance > 0: - raise ValueError( - 'Inputs are insufficient to cover bundle spend ' - '(balance: {balance}).'.format( - balance = balance, - ), - ) - elif balance < 0: - raise ValueError( - 'Bundle has unspent inputs (balance: {balance}).'.format( - balance = balance, - ), - ) - - sponge = Curl() - last_index = len(self) - 1 - - for (i, t) in enumerate(self): # type: Tuple[int, ProposedTransaction] - t.current_index = i - t.last_index = last_index - - sponge.absorb( - # Ensure checksum is not included. - t.address.address.as_trits() - + t.value_trits - + t.tag.as_trits() - + t.timestamp_trits - + t.current_index_trits - + t.last_index_trits - ) - - bundle_hash = [0] * HASH_LENGTH # type: MutableSequence[int] - sponge.squeeze(bundle_hash) - self.hash = Hash.from_trits(bundle_hash) - - for t in self: - t.bundle_hash = self.hash - - -class Transaction(object): - """ - A transaction that has been attached to the Tangle. - """ - @classmethod - def from_tryte_string(cls, trytes): - # type: (TrytesCompatible) -> Transaction - """ - Creates a Transaction object from a sequence of trytes. - """ - tryte_string = TryteString(trytes) - - hash_ = [0] * HASH_LENGTH # type: MutableSequence[int] - - sponge = Curl() - sponge.absorb(tryte_string.as_trits()) - sponge.squeeze(hash_) - - return cls( - hash_ = TransactionHash.from_trits(hash_), - signature_message_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]), - timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), - current_index = int_from_trits(tryte_string[2331:2340].as_trits()), - last_index = int_from_trits(tryte_string[2340:2349].as_trits()), - bundle_id = BundleHash(tryte_string[2349:2430]), - trunk_transaction_id = TransactionHash(tryte_string[2430:2511]), - branch_transaction_id = TransactionHash(tryte_string[2511:2592]), - nonce = Hash(tryte_string[2592:2673]), - ) - - def __init__( - self, - hash_, - signature_message_fragment, - address, - value, - tag, - timestamp, - current_index, - last_index, - bundle_id, - trunk_transaction_id, - branch_transaction_id, - nonce, - ): - # type: (Hash, TryteString, Address, int, Tag, int, int, int, Hash, TransactionHash, TransactionHash, Hash) -> None - self.hash = hash_ - """ - Transaction ID, generated by taking a hash of the transaction - trits. - """ - - self.bundle_id = bundle_id - """ - Bundle ID, generated by taking a hash of all the transactions in - the bundle. - """ - - self.address = address - """ - The address associated with this transaction. - If ``value`` is != 0, the associated address' balance is adjusted - as a result of this transaction. - """ - - self.value = value - """ - Amount to adjust the balance of ``address``. - Can be negative (i.e., for spending inputs). - """ - - self.tag = tag - """ - Optional classification tag applied to this transaction. - """ - - self.nonce = nonce - """ - Unique value used to increase security of the transaction hash. - """ - - self.timestamp = timestamp - """ - Timestamp used to increase the security of the transaction hash. - - IMPORTANT: This value is easy to forge! - Do not rely on it when resolving conflicts! - """ - - self.current_index = current_index - """ - The position of the transaction inside the bundle. - - For value transfers, the "spend" transaction is generally in the - 0th position, followed by inputs, and the "change" transaction is - last. - """ - - self.last_index = last_index - """ - The position of the final transaction inside the bundle. - """ - - self.branch_transaction_id = branch_transaction_id - self.trunk_transaction_id = trunk_transaction_id - - self.signature_message_fragment =\ - TryteString(signature_message_fragment or b'') - - self.is_confirmed = None # type: Optional[bool] - """ - Whether this transaction has been confirmed by neighbor nodes. - Must be set manually via the ``getInclusionStates`` API command. - - References: - - :py:meth:`iota.api.StrictIota.get_inclusion_states` - - :py:meth:`iota.api.Iota.get_transfers` - """ - - @property - def is_tail(self): - # type: () -> bool - """ - Returns whether this transaction is a tail. - """ - return self.current_index == 0 - - def as_tryte_string(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction. - """ - return ( - self.signature_message_fragment - + self.address - + TryteString.from_trits(trits_from_int(self.value, pad=81)) - + self.tag - + TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) - + TryteString.from_trits(trits_from_int(self.current_index, pad=27)) - + TryteString.from_trits(trits_from_int(self.last_index, pad=27)) - + self.bundle_id - + self.trunk_transaction_id - + self.branch_transaction_id - + self.nonce - ) diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 72f7159..7f8f14b 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -5,7 +5,7 @@ from unittest import TestCase from iota.crypto.signing import KeyGenerator -from iota.crypto.types import SigningKey +from iota.crypto.types import PrivateKey # noinspection SpellCheckingInspection @@ -40,7 +40,7 @@ def test_get_keys_single(self): # Note that the result is always a list, even when generating a # single key. [ - SigningKey( + PrivateKey( b'BWFTZWBZVFOSQYHQFXOPYTZ9SWB9RYYHBOUA9NOYSWGALF9MSVNEDW9A9FLGBRWKED' b'MPEIPRKBMRXRLLFJCAGVIMXPISRGXIJQ9BOBHKJEUKDEUUWYXJGCGAWHYBQHBPMRTZ' b'FPBGNLMKPZYXZPXFSPFUWZNRWYXUEWMP9URKVVJOSWEPJKSMPLWZPIZGOTVAA9QQOC' @@ -83,7 +83,7 @@ def test_get_keys_single(self): kg.get_keys(start=1), [ - SigningKey( + PrivateKey( b'WEVFRAOIRJBSBKQWF9JOTQXWUDLOIJRAC9WOOJNW99UAXVMUMSCMAABBXI99PRTAQL' b'UWJKVMM9DPSZSU9SAUN9URDWGGXIHWJJCDBAY9OQMQURNHZBD9E9CGERZC9RSUQVMZ' b'VYUTYXLH9CCEQPQLVDICQD9UCH9RPP9NSNZEBYERXHDEBEZUZOKNNEMXBYZPAYIIOB' @@ -130,7 +130,7 @@ def test_get_keys_single(self): kg.get_keys(start=13), [ - SigningKey( + PrivateKey( b'ZIGSOMJRQUXQHMNP9NCEGWCNXXVMJW9BXYRTMVUWRVTFQ9GCMJOOENTBSJDKPQTWML' b'FGEMPNODQWJ9BIZJSFWOOQNYLHIGUAJJXGISEMZKVPQOLQIMKESDECLJLDJFTRSBCQ' b'UEAJTCMUDYYWWENQZAPI9B9B9RBOCEAUAQHQNCZTWDOCYCRXZNHYTTTTNUNROHJGCF' @@ -171,7 +171,7 @@ def test_get_keys_single(self): def test_get_keys_long_seed(self): """ - Generating a SigningKey from a seed longer than 1 hash. + Generating a PrivateKey from a seed longer than 1 hash. This catches a regression caused by :py:meth:`iota.crypto.pycurl.Curl.squeeze` processing the wrong @@ -189,7 +189,7 @@ def test_get_keys_long_seed(self): kg.get_keys(start=0), [ - SigningKey( + PrivateKey( b'V9ZUOXOFKFSYMYRUKLXFTMLKBTDGXJWZZSEQINISGCXCPDQ9MFLTIEXXOEKPJPU9TW' b'WQAOWDNBVHODJSMTULXCDKTBPEBQMWNXRUWY9WPOFQZDOLYXWEABWU9DNFIUEETSHR' b'RNXXAGENAIQQYDBWIVXFZIKLNSEHVNKZGKJPAFTICTRNXCRDBXBEKBQF9XKS9LSVZU' @@ -240,7 +240,7 @@ def test_get_keys_multiple(self): kg.get_keys(start=1, count=2), [ - SigningKey( + PrivateKey( b'VBGOUVYVPMMVMNYIASWCSDOECOVM9BLBAWECSHKURDUVTFBRMUCIADFORQXPPCDVGM' b'QAGEPRS9IVKLZTJOVOBFXWC9PDXBL9WCRYKCNNFWOAJCKKV9KZICCQKUOFUAUYTRJH' b'DTCRDLWCXKSPK9ZMVVFMGXBPQRCVAFMRAGVHWFKQEDPXPR9UTYOSKPMGMZZWLW9SBZ' @@ -277,7 +277,7 @@ def test_get_keys_multiple(self): b'REONEIFHT' ), - SigningKey( + PrivateKey( b'H9VZYRIEGPFTUOMXYHPDLGNNWEXKHOMNHI9YIEOCBXRQJTZMYW9LHXTZDQEYMWUAGY' b'SJASVKHVXRJBMZINYZX9WBUERAH9EV9XDESZESVMYMQRNCNERUUGCYLBKHZDDLAPGU' b'MIEMROYBKAIETO9JVWLFPOHJZTFOTU9FNMOECNZEOFG9DKVSQNQBXWLVDTBCPMRAUN' @@ -365,7 +365,7 @@ def test_get_keys_step_negative(self): # This is the same as ``kg.get_keys(start=0, count=2)``, except # the order is reversed. [ - SigningKey( + PrivateKey( b'ODJGKFVKDKETMOUH9OCDBIDQCEMEDVKMEOIKSDTIQFECONPDCZUITROKXYCTNEMVQI' b'KZWKIJQZVYEAEUFGWAZUJEQMZLBPPYLJYEBOEZNYDFWKLYB9S9GTVZOXGGQ9CVYVVX' b'ZNIPHGIARTYMUXOJVTQZSASWVK9CSRUJODBJRPCCDDFEPZRNYHIGMFRFXAFGPMACLZ' @@ -402,7 +402,7 @@ def test_get_keys_step_negative(self): b'KVEPFJDTR' ), - SigningKey( + PrivateKey( b'TIOBFKKFAELHQOCLGJBGJZWVNSZMBPK9D9GTBZWDJBFFIKJWJBKIAAAAPDMYOCPDRN' b'BMK9QGJZAMIQZKSBNVVDLIVNRYVWQSHSJZZNGJGWNHGDMYCDZLIIUAPIPBKYSWIFNP' b'ZLVUGCN9MIQTYFYJWVIKXOUOFHAMCYZEVBHYABYPEQHXPHIPM9NWENCXRLUYQFILME' @@ -456,7 +456,7 @@ def test_iterations(self): [ # 2 iterations = key is twice as long! - SigningKey( + PrivateKey( b'VBGOUVYVPMMVMNYIASWCSDOECOVM9BLBAWECSHKURDUVTFBRMUCIADFORQXPPCDVGM' b'QAGEPRS9IVKLZTJOVOBFXWC9PDXBL9WCRYKCNNFWOAJCKKV9KZICCQKUOFUAUYTRJH' b'DTCRDLWCXKSPK9ZMVVFMGXBPQRCVAFMRAGVHWFKQEDPXPR9UTYOSKPMGMZZWLW9SBZ' @@ -550,7 +550,7 @@ def test_generator(self): self.assertEqual( next(generator), - SigningKey( + PrivateKey( b'LZDQHHPRICEESIHX9VVYCHLYSJDMXPLFOMMVOMUZKHSXLQYSQNDFHWLNKTCAJYPWUM' b'RMILEVQCHTGILDRABOOXPNRQHWBXVBRIZELVFEHTMYITSVBBBUQCZMDEUFGCTKHISF' b'MI9XQOBAIOVMCKNIGJQANHDMCWHYFJLDQLPMHLULLZZXYNZUNDLMRITVORDUEGNWKH' @@ -591,7 +591,7 @@ def test_generator(self): self.assertEqual( next(generator), - SigningKey( + PrivateKey( b'BTQKIPUTHFDMCKGVLBCFGEFLJHCHOANY9GBIRSODCOWTJGUWGFMDXWEB9HDNP9M9ZU' b'HVZQULQHEMXV9GMXDNYDHEJQZ9BSPIPHIVWNZDJWWDWONDKNVRFPZWNXZAVVTZCCMF' b'DBOYJZCGKIFJDKLMTCGGFUJAZ9FPOIBDSHDZCUYNVXCCDAFZLF9IMKZWTZFUMZSVCT' @@ -642,7 +642,7 @@ def test_generator_with_offset(self): self.assertEqual( next(generator), - SigningKey( + PrivateKey( b'CBKFYOTDYHFWSIOUYAUAHQWOOQDNNQBTSSJPHREUWFBXZFYFHPHZJN9ILAMZYOXLBQ' b'MCN9T9F9VSCUSFVQDZROPWBKZ9ENENGFVDLMBNDOAAKZXVBXVAYJLSGVD9ZZSMDCRT' b'JUUJPTNRVYYTOALRFOIGAXHUIXWCMXEUGTH9UJKYWTWBEOVWPMNVHJSXXKBAUPYCLS' @@ -683,7 +683,7 @@ def test_generator_with_offset(self): self.assertEqual( next(generator), - SigningKey( + PrivateKey( b'OTOFWEAUQFRSWBTJPPAACBIPCTMYAESBAGVMPVMH9IQAEEKEXVUCSOWORDIQBRZZLD' b'BXNAKQAQ9XMVRKMBQTBDFGJZEQBVRLEOUGHZNCJTSZQDDICMLCWZWF9FGAIFDNWUNV' b'XMVVXKNCCMRD9NNAKEMILISEKDBYTLPEUXTIGFRWCXKNEOEWBZPHLKPVSNYCEZVNVL' @@ -737,7 +737,7 @@ def test_generator_with_iterations(self): self.assertEqual( next(generator), - SigningKey( + PrivateKey( b'CBKFYOTDYHFWSIOUYAUAHQWOOQDNNQBTSSJPHREUWFBXZFYFHPHZJN9ILAMZYOXLBQ' b'MCN9T9F9VSCUSFVQDZROPWBKZ9ENENGFVDLMBNDOAAKZXVBXVAYJLSGVD9ZZSMDCRT' b'JUUJPTNRVYYTOALRFOIGAXHUIXWCMXEUGTH9UJKYWTWBEOVWPMNVHJSXXKBAUPYCLS' diff --git a/test/crypto/types_test.py b/test/crypto/types_test.py index d9b95b6..52ce21c 100644 --- a/test/crypto/types_test.py +++ b/test/crypto/types_test.py @@ -5,16 +5,131 @@ from unittest import TestCase from iota import TryteString -from iota.crypto.types import SigningKey +from iota.crypto.types import PrivateKey # noinspection SpellCheckingInspection -class SigningKeyTestCase(TestCase): +class PrivateKeyTestCase(TestCase): + def test_create_signature_fragment(self): + """ + Creating a signature fragment. + """ + key = PrivateKey( + b'MPRSBNPSAZRHZSBXSJASXSEHLYVLBXHCUNNQUNYB9CYEVWNBJKZUODPCZGOBRJ9V9C' + b'CARPTZEQRXGYDBEZISGMMXGHHHATAUDRTWUHEKJAMKUUKCK9NUUEWEFHPTVAARNLJB' + b'XRTFHQDXAVAOWYGJJNLCRVWQFHXPTMLFPGDKEBZCSSOQZEPNRTUKUJ9TRHLWQKOCBZ' + b'WBXBQVKUDTKV9IKCDQZRXRGQGIKCQ9BUNYMDFALF9QUFVKHQEHFICORMLLELVXM9VM' + b'ZIBUFKHLFGHDCPD9CBZVU9PMXWTAKWCBPBRBUG9PIAYYHKTP9VGDQIP9GUPKK9DVZI' + b'UYYTBTBYCDDF9LPBRGNGW9GHHFGRQPYZKKLCUDUAPAKZVIWL9RSJFQO9L9WKMBSWTY' + b'LCJJGWTWNLLKMHOUQVKTOUQGEOYNLEHTHJQIPV9ACBEBU9DN9KZWCEZIDEGQYQEYVY' + b'WUMPHMJAXOVEXOUMJ9MANMTGBOJAHUGHEYDPUQLNBIRUCLTXRFQIBTEWGUAHIJL9RK' + b'LMGKURCYGJLXGFLDF9HPSQUCGSZQDUQGZSYKRKHCCPBAPEBWJUYOBBGUXVSWHRAKWT' + b'TMHGJAWM9RPCLMVVNOWNXPBNTPFYWQIPQLNOJJYNRDTLAUBZPLVUACUSPNBHCOPO9Z' + b'XSIETGTFDLMRESLN9WXUNQ9SFQFD9CNIETCSVDMNLGNMKOOTOCODTDCPLZPUYON9HD' + b'QLDGSCWHKDLQOGDHZHPURB9TSUGVSPKDIZE9ETVMLVMNDRJOOUUFDTSKDDTOGXEVFN' + b'BDSBRDZPZTUGYZNVKOIFZYRUIZI9LRIFCCXPHJMHCDOUPKJVUGFPTFI9XPGRDHRZUQ' + b'UXCK9MTBINIJFFCQTECFTVTILTKRYJTI9ADGPHFBKNLAPLPKKAOOIEKQLBCDGTZZEF' + b'KDBRICXLWZQXXCYVDFAJGKJSS9PADMDCJGJSYSIAOUFHW9CHTDXLVE9PBFCMBZKIHZ' + b'9VBCXUDBUUEFNRHI9FXC9KREKFVDGHBFDHNEGCIM9CVSJEQSU9FUKVYSVWCTLM9LAB' + b'JCWMPTUHULMGQBVRFCYSGXEQJHMZIVGTIS9QVPTTULFKLSMIPMBUTBXTUQRBOJVUMP' + b'BGWEHIFRMNWLHQY9XKFCSGISDWSDILBOSYYWUSICMRWYUWTJY9GW9CC9ZQOKWMIYYD' + b'ZRZEKKKJVD9DWTTBEPAJQOVOYPVDGRILVPYXRJZSUXKHCWGVPQNDWKCXJO99AUPVZB' + b'VOFELMUKYCRETU9QVUYXXEWHBFFWQOLIHFLRKLGECUMUMHWYESXOUPWLEFMJZVXWJH' + b'EBSYVSRSIFXRWBCAEDNUZWEHAKUPAIGIQKCVNURYELBRSKENYWEVPEORWIQHHCSGYW' + b'XADSSJGDGTVDUHUNJBPTOTKOS9KKSWSJPRGUB9AFTBBUAMEIBBGIQCCZDRHCCYADMI' + b'ATAOUOJOWNUWUKKMYVHNLJSZMTBHYODMJW9RFZNUJCCGTKMVPTBLLUOKXWVJEKSBQW' + b'TGNJBEU99LIUBZIHIMX9GHTEXPDMH9OXPFFGMYQFGEJV9TQJPI9ZFVCFZHMRLDVLUG' + b'MALNTWOMCNQGHQKZRHSLKFCHDFTOBEOQZRAPDOFEKTQZVRUDCYVZCLONUKMUYIIJRD' + b'BVHOZINOYECRMGGIXZNSAGZUOYCNSCEB9EPMZDVMIIBHHLEKSWIDGBOMJRMU9KXKPX' + b'DTPULQZNSFKNIFWNPWBRSAQHESXMNCMPIMQLPOKEDFWPYCVYHMXLFFMIQGGBTAM9CP' + b'COUJBMZWLYIXOGVFYGLRKSYMPMQBNUMSBV9JXWB9BLJNXOORUTLGI9WSAGFDUVL9DE' + b'YGVGYVWUCXXLJQFDGXJWYWNHIUJXFCRWJLVUD9CTLXKZSLGGG9UOKYUX9ESVPNUYLV' + b'LGSBVCA99HYBFYZYBEKVSRBFWDURRA9OVXQLKSUAEMKNT9FVYPPRQHCIVPXUS9KP9R' + b'OEZ9NOZHYLIURCFY9XFYUEKWQHYKNZEJWKJMFFDJCEWDGVSQLNSQRMJALTGQJIFNED' + b'PSYXCIRKYURDVHTNWGRZLDETFXQGWAJTGWAIXXCBNSMMVFCJELLMLEFFYOGNISGWKJ' + b'KLAXTGLAYNWEMXUZBKTCFNRRHWMCXMIDPATZCYQFFGNAKTGSIZYPQXDIYUGFCO9YBR' + b'LAOEGJUJNL9SHU9NPYQGOLUTDTNQHZPNQGWXXNWYUHLNTCRCOZTVZJQGNSFQLKSNEU' + b'LEEYHDMWAWVVHCXVVYNRDLPWDLRGHNPEAOCYESSMABTSMDMPNJHMAKWVEZSQCATTPD' + b'IMRA9JOZPVKAZWMEEKTQNQVCRQBVLPCJCKBPNSWFJBM9JBNCUETNUVOHHCZZLMBIOE' + b'QEWUOGNIETNOKGFLLKKZUIRDLBAHMZQ9JUWXO9QHZ9XZNAFGQTCJYJUYNKIYTTNQXQ' + b'DSUWKEZACQBYRWLTTINDLBRSZPESTWV9KKYCBEEVPKFQIK9RGIADVNDWWMPQOWLYAR' + b'VK9BNZLFEZGUHZW9KFZAEDUTVTSGSTGFPCJNX9MGLDFXMTYWXEHOYBTQCVCEXNQNRI' + b'YEUGEUJ9AUXMNUTIVYB9VRNVTAAEQTQRPEKYOLECFGPZWWOTIEZMSA9P9QCMCSDMUS' + b'HDJOQEUUKFCSHQTWHEHJWQPEXQNKVPNUEMBA9CVOEVJEGJORYWZAOIVWHAMCRVPVTW' + b'IMEJNHZ9PBSIPNEUWAQBNSOAWYA9GLPWCNTSAIHLSQD9LJJIVLLVXWSNQHWLYQMQFB' + b'WTAUYUGAWOICQJFWTJXN9BLVQDZSTTTGJCCWGWEBGTGODNDCGCSXZMBTSFEZIKMCOZ' + b'WJU9IVFAEIGHLCLUNERARLHWQICKGYYEUBBPEEHHHGPTDPEMHMNRYKIXOASAVPN9TT' + b'YDEYZHVDGVVTICNCXRMJKJLWJDVPNPQCQESACMEZRDVCFTGMEEHEUFAWZDLNFT9PNI' + b'Q9WTWZAMDRYSBSCXIOOYAMQBAHJEFLDUQHPZYDQRPA9MEKAMRBZGTUBRTNYYGBGAY9' + b'YIERRWFRMNKGTZSXBONWLGSQVBL9HYSUSUHTVPTIHFFXWDCZSGTBDZQMLPUCYWWVBF' + b'9JNJPCQODJYOHXRUTEPRJZW9MJHKQLGIFGQRQWRSBFPASG9YLKOHOKRZQQZPM9MNQC' + b'AWAZIU9JRPDXBNMBIYFJUYAEWQSMLSZUPLBKHGIJTNXYQASQUMPMKWIZ9DMINF9CYG' + b'ZMILHRJOAJQEOVVALRYQGPPTUOD9OIOHIGRGGJMVKCZUNK9QVBLBTMXJRYWNVY9MZN' + b'HZLVJUNZEYICXLLMQSYFPRZQAYJCX9KKFFTPZFCUKRV9EMRHPGPQ9ZNBYXXZXL9LGZ' + b'LJZPKVA9HHZEBIIPJYIEBQJO9LIOPTWSUMOLHG9AEJWGANZBFG9IDPFTDDBFYBDPDJ' + b'GQEPDFYAYADLQOODRXA9CBUDYXEJWITAKRMBACRDXDCPGGPDCQRHACTJIIVD9XTLVR' + b'NNTWCMDTZXXVUOHBOYWCGIFCNEH9AG9KD99LOGNAYIMCRLVTCCCRFWHKVE99AOWOGU' + b'UXNITAEYHJHVVOKOVFXVOSXBTGVIVNDJAZITQKXXXLLIMOAKOMZIIXEEYXGNQVYFQQ' + b'TJJEDXSDPFA9JZNMWGADIUNVFOURDTGPHCKPEHJMIKZFISIYFOVWWYZYKFRXRH9TCJ' + b'GOLCHPYDVFIPRCYLUINFQRBXJ9EDFRJPGPERLTJNTNJMDYHVLYSY9ZALOOEGBNLJQV' + b'PAOPVMQPZ9QSUYW9FQFQYKFOYYYPQHIFEWPVWURDY9DWBBVOADLLEFXNLGIFYQXKY9' + b'CDDYQXV9EP9MMJW9SUBHL9BGCXYF9LMGUIJDEDJOTSRZZGPKOWHOGFI9QJRNIN9FVB' + b'WVFOAJGTYJBOWEPFZDHGKYVGB9IHHCHZODFWRJQSSYDV9IURZBCJALECSLQCXYHAUC' + b'GJZNB99QZFZ9FCPFFRXJCUYGWMKRGICUDAUYBKIPPXRQOVBYPKALVCRBQATSBOONFP' + b'EMTIYNOSEUC9JLBFMMQTVTBBXVJV9FIOXQDSJCZVTERZHSMTK9JKUYUHRJWOXXTZDD' + b'XMJSUTPFQJHKDBMOUWSZRHUJAQOTPXINF9WDGRG99OSMKXGY9WCD9AJVXEDNLWSBWB' + b'IHO9ODBQSQL9TUMJZASGNTWUQGNNIWSBNGXRSCLYCGRNTEYTGJAIIXX9YNGHLDZ9QV' + b'BCQNMVIQ9EXTSCQAJJROGKIHJJTKLOABYQDHMXJFEKFN9L9FSYAMVOFTXIOSEOBBUA' + b'UYSHWINHCJRQTIXWWPZBGKIGJC9TDKFAKZTIFBSDFSXVJCCICIBEAIGNUARDZGGBBH' + b'RLMWQ9RSJB9TPLSDEP' + ) + + self.assertEqual( + TryteString.from_trits( + key.create_signature(TryteString(b'WGXG9AGGI'), blocks=1) + ), + + b'GNCUHGZVIYRQQBXXBUONVL9COKOYDERJAWWI9YRWBVUJLDQQCBMFOORHRTVPKUDFWK' + b'YPOOCNVDQOCTJNVCFOENDWEGCXOXZK9BNFGAUTSLGPIWCIWFQAGQTVECGNXLFCUTFQ' + b'UJRUQKPJEDUFZTABRPTDXCIE99ZSGZFQCJYRZRSSUEENDBMDRZPLI9KCTVAYTFVRQB' + b'HDFLQZJBHIFFDVJPIJFCMOYNKOXQNQHSNDBWXRHLVBXBRLICTY9KJKWK9TZBSNIHKM' + b'ADAWFIK9YZVZFZWSZANYEIGQKOVRDGGTGFYFLGKESJKLUJMCKGGAJZHFQJXWIJ9HFI' + b'URAXKZKTNULKABTUDTNVQEJRMVUXGOOWAUMSQCUBAOLEFO9PTVPCIFMFJGYAPNFIDW' + b'T9TASFHSPXTRVEBRNJOVJIPGMNEWHGZZTQRRHEG9CXBLUJJSCENYKRXKRFPHAOEY9G' + b'RNLWUCLXXOBQOSSOVEXZGHBMJKQ9YDQPRGSSNXOFZMUOGKWENWIVIVMDDLPACSKPRS' + b'99PNRADWBWSVTXIGGW9THJEQNWEGCWOVSLVWITMUWIUQGSGBYXNKSKUCLFEHWLOOID' + b'OKTIOJGPUZQBLHYUKCH9IKPJCRWXBNXGSHIZXYCSMOOQZROSDMNGEMHSKKDOYPKXAI' + b'DS9UUINN9RSVSDJUODJ99VEJMLLQRJFPYAZDKTNXPHZYBV9AERYOUJKBXACGZTWWGV' + b'NVIDNQPRK9HNEGOVOKQBXOXHZLHFNXDSFJECQVRJFJ9VHSCZACCHRKLWEGE9LAFEQY' + b'TFYAGUSYZGFXOIUVATAZCTDMOMVOVMWTDFXRNLHPSYMQKVGRRINYVOBVUXMPZPXBMM' + b'OFPNNGDCZHBPQBGMZJKOHOIKTXYZAAFKZEZPTESTUQH9HPNUQKUD9GJSSVDIJQGFRI' + b'FULCYSKXTASWYBMXSNJWFFJA9IQOBJYVZVYDXZYMWIYJLXIPGGKULHDBEXYCLQQFXA' + b'DJEEVJW9DFWDJQTS9AJWQQZLOYOSQW9DEHHENQVVOKFSBNHSJQHTPGIBLNTCXATNOH' + b'TIOQFDIQEZFWWHCUEGNAYAPMUGSNCZQIWGKCYAWZWGVLEZVSZIJKD9EHINYFUVZLPR' + b'NIVUNZBVUGMQAUKEDPQWZNKWAJZDVHIFLSQWBGVOXIVBZTYRDBFNXUFFNSSJHJLYJA' + b'9MYYLKRFEWMQNYJWYTDNIVNJFCEHBDE99SZTKKGZUQO9AMVGYCIUOCLECKEUBSWBNB' + b'9QYSWEUAVROPHIFQPAQUIIPGGTGYSWDFPYYCRFBHBHAUDUX9OKZPIZEOFEIMVHVAKR' + b'QJ9HXVC99RNOEOAZVPPIJCWGKCAJOOWRFXSWLAPBNIQGNNKXSBLMNFJCUBYDMPKS9V' + b'ASYWPE9WPMZLKSMOFQBPULYKIVBTHTUUNGGRPOOMSSSNUCP9ASWMGONSIANVRPEYLF' + b'OPJNSBCXKQSVPECWUWEHIOSVRAKAUTDCPGGMPQXDJFQAAFJQTIDUUFGDMGVZDJCNOS' + b'YOEAFJSKXUPHNGDJH9TSKJZGPUETDIFCUQUVARHETQFSRLSBCJCLBR9BJNDTNTWH9P' + b'HU99TPAMMYYWJSISCKTBGMROVKWVZTOOIUTOERQKHGPTOZAWKKRGRYLZTJPPUURSMF' + b'H9H9EMWIWKJYMWBLNQPFHBUGFKDJHEHORXXDDSFXAAQBSKIORGQNHZYRSUSRNLIBID' + b'AVDPZKOLLPOWTCPPUJFTOLMGSPASTNIXBHIJJSBRMREVBCZIPRVBQVPQ9AYYPOVK9L' + b'BOQQCGIXAYOXYIC9SYFMGAGZJOIHUZCATPJJOHVAZXLPUVLDJKBXQNPRTWHKH9ZHGC' + b'ILKVHYOXNVVWQFMXYDKEAQMXZLNZTFKWRTBTCZLTRCFVQSYG9ZSSZTQJQSHMBOMIQS' + b'KNWRG9MEUHWPGAJPJBDKPQOCXDYAIYMUKBOCMTVVZXLPIYQ9XZOJWMICKLECKCRJUX' + b'LEYRZCIQNFOIHKJPHTKFTS9YQMBXLCWZBZOWPEWMHLDEHBADWNCSGUYYYNGDGPMJGK' + b'MUQ9QCPVPJIOQAZOXMIP9POREGZIDEXJOWXHYTOILXMQBXRGBBNAOIJLRANGZGZSOM' + b'XQUEEUWXS9HSNOTUATDNMNIOLERHGBLZN9REGCRXIMGZWTHIGGL9VFGZZFURDCLVCT' + b'VKMUTYC9C' + ) + def test_get_digest_trits(self): """ - Generating digest trits from a valid SigningKey. + Generating digest trits from a valid PrivateKey. """ - key = SigningKey( + key = PrivateKey( b'BWFTZWBZVFOSQYHQFXOPYTZ9SWB9RYYHBOUA9NOYSWGALF9MSVNEDW9A9FLGBRWKED' b'MPEIPRKBMRXRLLFJCAGVIMXPISRGXIJQ9BOBHKJEUKDEUUWYXJGCGAWHYBQHBPMRTZ' b'FPBGNLMKPZYXZPXFSPFUWZNRWYXUEWMP9URKVVJOSWEPJKSMPLWZPIZGOTVAA9QQOC' diff --git a/test/transaction_test.py b/test/transaction_test.py new file mode 100644 index 0000000..5b87910 --- /dev/null +++ b/test/transaction_test.py @@ -0,0 +1,459 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from iota import Address, BundleHash, Hash, Tag, Transaction, \ + TransactionHash, TryteString +from six import binary_type + + +class ProposedBundleTestCase(TestCase): + def test_add_transaction_short_message(self): + """ + Adding a transaction to a bundle, with a message short enough to + fit inside a single transaction. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_transaction_long_message(self): + """ + Adding a transaction to a bundle, with a message so long that it + has to be split into multiple transactions. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_transaction_error_already_finalized(self): + """ + Attempting to add a transaction to a bundle that is already + finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_transaction_error_negative_value(self): + """ + Attempting to add a transaction with a negative value to a bundle. + + Use :py:meth:`ProposedBundle.add_inputs` to add inputs to a bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_inputs_balanced(self): + """ + Adding inputs to cover the exact amount of the bundle spend. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_inputs_with_change(self): + """ + Adding inputs to a bundle results in unspent inputs. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_add_inputs_error_already_finalized(self): + """ + Attempting to add inputs to a bundle that is already finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_send_unspent_inputs_to_unbalanced(self): + """ + Invoking ``send_unspent_inputs_to`` on an unbalanced bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_send_unspent_inputs_to_balanced(self): + """ + Invoking ``send_unspent_inputs_to`` on a balanced bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_send_unspent_inputs_to_error_already_finalized(self): + """ + Invoking ``send_unspent_inputs_to`` on a bundle that is already + finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_finalize_happy_path(self): + """ + Finalizing a bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_finalize_error_already_finalized(self): + """ + Attempting to finalize a bundle that is already finalized. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_finalize_error_unbalanced(self): + """ + Attempting to finalize an unbalanced bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_sign_inputs(self): + """ + Signing inputs in a finalized bundle. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_sign_inputs_error_not_finalized(self): + """ + Attempting to sign inputs in a bundle that hasn't been finalized + yet. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + +# noinspection SpellCheckingInspection +class TransactionHashTestCase(TestCase): + def test_init_automatic_pad(self): + """ + Transaction hashes are automatically padded to 81 trytes. + """ + txn = TransactionHash( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' + ) + + self.assertEqual( + binary_type(txn), + + # Note the extra 9's added to the end. + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' + ) + + def test_init_error_too_long(self): + """ + Attempting to create a transaction hash longer than 81 trytes. + """ + with self.assertRaises(ValueError): + TransactionHash( + b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' + b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' + ) + + +class TransactionTestCase(TestCase): + # noinspection SpellCheckingInspection + def test_from_tryte_string(self): + """ + Initializing a Transaction object from a TryteString. + """ + # :see: http://iotasupport.com/news/index.php/2016/12/02/fixing-the-latest-solid-subtangle-milestone-issue/ + trytes = ( + 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' + ) + + transaction = Transaction.from_tryte_string(trytes) + + self.assertIsInstance(transaction, Transaction) + + self.assertEqual( + transaction.hash, + + Hash( + b'QODOAEJHCFUYFTTPRONYSMMSFDNFWFX9UCMESVWA' + b'FCVUQYOIJGJMBMGQSFIAFQFMVECYIFXHRGHHEOTMK' + ), + ) + + self.assertEqual( + transaction.signature_message_fragment, + + TryteString( + 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'999999999' + ), + ) + + self.assertEqual( + transaction.address, + + Address( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + ) + + self.assertEqual(transaction.value, 0) + self.assertEqual(transaction.tag, Tag(b'999999999999999999999999999')) + self.assertEqual(transaction.timestamp, 1480690413) + self.assertEqual(transaction.current_index, 1) + self.assertEqual(transaction.last_index, 1) + + self.assertEqual( + transaction.bundle_id, + + BundleHash( + b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' + b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' + ), + ) + + self.assertEqual( + transaction.trunk_transaction_id, + + TransactionHash( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + ) + + self.assertEqual( + transaction.branch_transaction_id, + + TransactionHash( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + ) + + self.assertEqual( + transaction.nonce, + + Hash( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + ) + + def test_from_tryte_string_error_too_short(self): + """ + Attempting to create a Transaction from a TryteString that is too + short. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_from_tryte_string_error_too_long(self): + """ + Attempting to create a Transaction from a TryteString that is too + long. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + # noinspection SpellCheckingInspection + def test_as_tryte_string(self): + """ + Converting a Transaction into a TryteString. + """ + transaction = Transaction( + hash_ = + Hash( + b'QODOAEJHCFUYFTTPRONYSMMSFDNFWFX9UCMESVWA' + b'FCVUQYOIJGJMBMGQSFIAFQFMVECYIFXHRGHHEOTMK' + ), + + signature_message_fragment = + TryteString( + 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'999999999' + ), + + address = + Address( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + + value = 0, + tag = Tag(b'999999999999999999999999999'), + timestamp = 1480690413, + current_index = 1, + last_index = 1, + + bundle_id = + BundleHash( + b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' + b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' + ), + + trunk_transaction_id = + TransactionHash( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + + branch_transaction_id = + TransactionHash( + b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' + b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' + ), + + nonce = + Hash( + b'9999999999999999999999999999999999999999' + b'99999999999999999999999999999999999999999' + ), + ) + + self.assertEqual( + transaction.as_tryte_string(), + + 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', + ) diff --git a/test/types_test.py b/test/types_test.py index c7c6b13..ea846cb 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -5,18 +5,8 @@ from os import urandom from unittest import TestCase -from iota import ( - Address, - AddressChecksum, - BundleHash, - Hash, - Tag, - Transaction, - TransactionHash, - TryteString, - TrytesCodec, - TrytesDecodeError, -) +from iota import Address, AddressChecksum, Tag, TryteString, TrytesCodec, \ + TrytesDecodeError from six import binary_type @@ -833,96 +823,6 @@ def test_init_error_too_long(self): AddressChecksum(b'FOXM9MUBX9') -class ProposedBundleTestCase(TestCase): - def test_add_transaction_short_message(self): - """ - Adding a transaction to a bundle, with a message short enough to - fit inside a single transaction. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_add_transaction_long_message(self): - """ - Adding a transaction to a bundle, with a message so long that it - has to be split into multiple transactions. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_add_transaction_error_already_finalized(self): - """ - Attempting to add a transaction to a bundle that is already - finalized. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_add_inputs_balanced(self): - """ - Adding inputs to cover the exact amount of the bundle spend. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_add_inputs_with_change(self): - """ - Adding inputs to a bundle results in unspent inputs. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_add_inputs_error_already_finalized(self): - """ - Attempting to add inputs to a bundle that is already finalized. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_send_unspent_inputs_to_unbalanced(self): - """ - Invoking ``send_unspent_inputs_to`` on an unbalanced bundle. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_send_unspent_inputs_to_balanced(self): - """ - Invoking ``send_unspent_inputs_to`` on a balanced bundle. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_send_unspent_inputs_to_error_already_finalized(self): - """ - Invoking ``send_unspent_inputs_to`` on a bundle that is already - finalized. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_finalize_happy_path(self): - """ - Finalizing a bundle. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_finalize_error_already_finalized(self): - """ - Attempting to finalize a bundle that is already finalized. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_finalize_error_unbalanced(self): - """ - Attempting to finalize an unbalanced bundle. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - # noinspection SpellCheckingInspection class TagTestCase(TestCase): def test_init_automatic_pad(self): @@ -940,339 +840,3 @@ def test_init_error_too_long(self): with self.assertRaises(ValueError): # 28 chars = no va. Tag(b'COLOREDCOINS9999999999999999') - - -# noinspection SpellCheckingInspection -class TransactionHashTestCase(TestCase): - def test_init_automatic_pad(self): - """ - Transaction hashes are automatically padded to 81 trytes. - """ - txn = TransactionHash( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC' - ) - - self.assertEqual( - binary_type(txn), - - # Note the extra 9's added to the end. - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC9999' - ) - - def test_init_error_too_long(self): - """ - Attempting to create a transaction hash longer than 81 trytes. - """ - with self.assertRaises(ValueError): - TransactionHash( - b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' - b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' - ) - - -class TransactionTestCase(TestCase): - # noinspection SpellCheckingInspection - def test_from_tryte_string(self): - """ - Initializing a Transaction object from a TryteString. - """ - # :see: http://iotasupport.com/news/index.php/2016/12/02/fixing-the-latest-solid-subtangle-milestone-issue/ - trytes = ( - 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' - ) - - transaction = Transaction.from_tryte_string(trytes) - - self.assertIsInstance(transaction, Transaction) - - self.assertEqual( - transaction.hash, - - Hash( - b'QODOAEJHCFUYFTTPRONYSMMSFDNFWFX9UCMESVWA' - b'FCVUQYOIJGJMBMGQSFIAFQFMVECYIFXHRGHHEOTMK' - ), - ) - - self.assertEqual( - transaction.signature_message_fragment, - - TryteString( - 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'999999999' - ), - ) - - self.assertEqual( - transaction.address, - - Address( - b'9999999999999999999999999999999999999999' - b'99999999999999999999999999999999999999999' - ), - ) - - self.assertEqual(transaction.value, 0) - self.assertEqual(transaction.tag, Tag(b'999999999999999999999999999')) - self.assertEqual(transaction.timestamp, 1480690413) - self.assertEqual(transaction.current_index, 1) - self.assertEqual(transaction.last_index, 1) - - self.assertEqual( - transaction.bundle_id, - - BundleHash( - b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' - b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' - ), - ) - - self.assertEqual( - transaction.trunk_transaction_id, - - TransactionHash( - b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' - b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' - ), - ) - - self.assertEqual( - transaction.branch_transaction_id, - - TransactionHash( - b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' - b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' - ), - ) - - self.assertEqual( - transaction.nonce, - - Hash( - b'9999999999999999999999999999999999999999' - b'99999999999999999999999999999999999999999' - ), - ) - - def test_from_tryte_string_error_too_short(self): - """ - Attempting to create a Transaction from a TryteString that is too - short. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - def test_from_tryte_string_error_too_long(self): - """ - Attempting to create a Transaction from a TryteString that is too - long. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - - # noinspection SpellCheckingInspection - def test_as_tryte_string(self): - """ - Converting a Transaction into a TryteString. - """ - transaction = Transaction( - hash_ = - Hash( - b'QODOAEJHCFUYFTTPRONYSMMSFDNFWFX9UCMESVWA' - b'FCVUQYOIJGJMBMGQSFIAFQFMVECYIFXHRGHHEOTMK' - ), - - signature_message_fragment = - TryteString( - 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'999999999' - ), - - address = - Address( - b'9999999999999999999999999999999999999999' - b'99999999999999999999999999999999999999999' - ), - - value = 0, - tag = Tag(b'999999999999999999999999999'), - timestamp = 1480690413, - current_index = 1, - last_index = 1, - - bundle_id = - BundleHash( - b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' - b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' - ), - - trunk_transaction_id = - TransactionHash( - b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' - b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' - ), - - branch_transaction_id = - TransactionHash( - b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' - b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' - ), - - nonce = - Hash( - b'9999999999999999999999999999999999999999' - b'99999999999999999999999999999999999999999' - ), - ) - - self.assertEqual( - transaction.as_tryte_string(), - - 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', - ) From ab1e62d60b6bd380647825c563b87f6902b9a697 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 15:48:56 -0500 Subject: [PATCH 181/239] Implemented signature fragment generator. --- iota/crypto/signing.py | 59 +++++++- iota/crypto/types.py | 44 +----- iota/transaction.py | 71 ++++++---- iota/types.py | 61 ++++++++- test/crypto/signing_test.py | 263 +++++++++++++++++++++++++++++++++++- test/crypto/types_test.py | 115 ---------------- test/types_test.py | 17 +++ 7 files changed, 439 insertions(+), 191 deletions(-) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 19bfc68..2d6f8f2 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -4,13 +4,14 @@ from typing import Generator, List, MutableSequence -from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible +from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible, Hash from iota.crypto import Curl, HASH_LENGTH from iota.crypto.types import PrivateKey from iota.exceptions import with_context __all__ = [ 'KeyGenerator', + 'SignatureFragmentGenerator', ] @@ -211,3 +212,59 @@ def _create_sponge(self, index): sponge.absorb(seed) return sponge + + +class SignatureFragmentGenerator(object): + """ + Used to generate signature fragments progressively. + + Each instance can generate 1 signature per block (2187 trytes) in the + private key. + """ + def __init__(self, private_key): + # type: (PrivateKey) -> None + super(SignatureFragmentGenerator, self).__init__() + + self._key_chunks = private_key.iter_chunks(PrivateKey.BLOCK_LEN) + self._sponge = Curl() + + def send(self, source_trytes): + # type: (TryteString) -> TryteString + """ + Sends a source string to the generator to create the next fragment. + + :param source_trytes: + Trytes to use to generate the signature. + + Note: should be 27 trytes long. + If too short, will be padded. + If too long, extra trytes will be ignored. + """ + key_trytes = next(self._key_chunks) # type: TryteString + + hashes_per_block = PrivateKey.BLOCK_LEN // Hash.LEN + + # Ensure ``source_trits`` is long enough. + # It must have at least 1 trit per hash. + source_trits = source_trytes.as_trits() + source_trits += [0] * max(0, hashes_per_block - len(source_trytes)) + + signature = key_trytes.as_trits() + + # Build the signature, one hash at a time. + for i in range(hashes_per_block): + hash_start = i * HASH_LENGTH + hash_end = hash_start + HASH_LENGTH + + fragment = signature[hash_start:hash_end] + + # Use value from the source trits to make the signature + # deterministic. + for _ in range(13 - source_trits[i]): + self._sponge.reset() + self._sponge.absorb(fragment) + self._sponge.squeeze(fragment) + + signature[hash_start:hash_end] = fragment + + return TryteString.from_trits(signature) diff --git a/iota/crypto/types.py b/iota/crypto/types.py index 4367df5..f028806 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -13,6 +13,7 @@ __all__ = [ 'PrivateKey', + 'Seed', ] @@ -85,49 +86,6 @@ def block_count(self): """ return len(self) // self.BLOCK_LEN - def create_signature(self, trytes, blocks=None): - # type: (TryteString, Optional[int]) -> TryteString - """ - Creates a signature for the specified trytes. - - :param trytes: - The trytes to use to build the signature. - - :param blocks: - Max length of the resulting signature, expressed in blocks. - - See :py:attr:`BLOCK_LEN` for more info. - """ - signature = self.as_trits() - if blocks is not None: - signature = signature[:self.BLOCK_LEN*blocks*TRITS_PER_TRYTE] - - hash_count = int(ceil(len(signature) / HASH_LENGTH)) - - source = trytes.as_trits() - source += [0] * max(0, hash_count - len(source)) - - sponge = Curl() - - # Build signature, one hash at a time. - for i in range(hash_count): - start = i * HASH_LENGTH - stop = start + HASH_LENGTH - - fragment = signature[start:stop] - - # Use value from the source trits to make the signature - # deterministic. - for j in range(13 - source[i]): - sponge.reset() - sponge.absorb(fragment) - sponge.squeeze(fragment) - - # Copy the signature fragment to the final signature. - signature[start:stop] = fragment - - return signature - def get_digest_trits(self): # type: () -> List[int] """ diff --git a/iota/transaction.py b/iota/transaction.py index 212ab76..39a752b 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -4,14 +4,14 @@ from calendar import timegm as unix_timestamp from datetime import datetime - -from typing import Generator, Iterable, List, MutableSequence, Optional, Tuple +from typing import Generator, Iterable, List, MutableSequence, \ + Optional, Tuple from iota import Address, Hash, Tag, TrytesCompatible, TryteString, \ int_from_trits, trits_from_int from iota.crypto import Curl, HASH_LENGTH from iota.crypto.addresses import AddressGenerator -from iota.crypto.signing import KeyGenerator +from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator from iota.exceptions import with_context __all__ = [ @@ -230,49 +230,50 @@ def add_inputs(self, inputs): if self.hash: raise RuntimeError('Bundle is already finalized.') - for i in inputs: - if i.balance is None: + for addy in inputs: + if addy.balance is None: raise with_context( exc = ValueError( 'Address {address} has null ``balance`` ' '(``exc.context`` has more info).'.format( - address = i, + address = addy, ), ), context = { - 'address': i, + 'address': addy, }, ) - if i.key_index is None: + if addy.key_index is None: raise with_context( exc = ValueError( 'Address {address} has null ``key_index`` ' '(``exc.context`` has more info).'.format( - address = i, + address = addy, ), ), context = { - 'address': i, + 'address': addy, }, ) # Add the input as a transaction. self.add_transaction(ProposedTransaction( - address = i, - value = -i.balance, + address = addy, + value = -addy.balance, tag = self.tag, )) - # Signatures require two transactions to store, due to + # Signatures require multiple transactions to store, due to # transaction length limit. - self.add_transaction(ProposedTransaction( - address = i, - value = 0, - tag = self.tag, - )) + for _ in range(AddressGenerator.DIGEST_ITERATIONS): + self.add_transaction(ProposedTransaction( + address = addy, + value = 0, + tag = self.tag, + )) def send_unspent_inputs_to(self, address): # type: (Address) -> None @@ -327,7 +328,7 @@ def finalize(self): t.last_index = last_index sponge.absorb( - # Ensure checksum is not included. + # Ensure address checksum is not included in the result. t.address.address.as_trits() + t.value_trits + t.tag.as_trits() @@ -362,7 +363,7 @@ def sign_inputs(self, key_generator): if txn.address.key_index is None: raise with_context( exc = ValueError( - 'Unable to sign input {input}; key index is None ' + 'Unable to sign input {input}; ``key_index`` is None ' '(``exc.context`` has more info).'.format( input = txn.address, ), @@ -373,21 +374,24 @@ def sign_inputs(self, key_generator): }, ) - private_key =\ + signature_fragment_generator = SignatureFragmentGenerator( key_generator.get_keys( start = txn.address.key_index, iterations = AddressGenerator.DIGEST_ITERATIONS - )[0] - - # Signatures are too long to fit inside a single transaction, - # so we must split it into two parts (this is why we created - # two transactions per input in :py:meth:`add_inputs`). - txn.signature_message_fragment = private_key.create_signature( - trytes = self.hash[:9], - blocks = 1, + )[0], ) - # :todo: Second signature fragment. + hash_fragment_iterator = self.hash.iter_chunks(9) + + # We can only fit one signature fragment into each transaction, + # so we have to split the entire signature among the extra + # transactions we created for this input in + # :py:meth:`add_inputs`. + for j in range(AddressGenerator.DIGEST_ITERATIONS): + self[i+j].signature_message_fragment =\ + signature_fragment_generator.send(next(hash_fragment_iterator)) + + i += AddressGenerator.DIGEST_ITERATIONS - 1 i += 1 @@ -503,6 +507,13 @@ def __init__( self.signature_message_fragment =\ TryteString(signature_message_fragment or b'') + """ + Cryptographic signature used to verify the transaction. + + Signatures are usually too long to fit into a single transaction, + so they are split out into multiple transactions in the same bundle + (hence it's called a fragment). + """ self.is_confirmed = None # type: Optional[bool] """ diff --git a/iota/types.py b/iota/types.py index 46f125e..2d9a613 100644 --- a/iota/types.py +++ b/iota/types.py @@ -4,7 +4,7 @@ from codecs import encode, decode from itertools import chain -from typing import Generator, Iterable, List, MutableSequence, \ +from typing import Generator, Iterable, Iterator, List, MutableSequence, \ Optional, Text, Union from iota import TRITS_PER_TRYTE, TrytesCodec @@ -339,6 +339,17 @@ def __ne__(self, other): # type: (TrytesCompatible) -> bool return not (self == other) + def iter_chunks(self, chunk_size): + # type: (int) -> ChunkIterator + """ + Iterates over the TryteString, in chunks of constant size. + + :param chunk_size: + Number of trytes per chunk. + The final chunk will be padded if it is too short. + """ + return ChunkIterator(self, chunk_size) + def as_bytes(self, errors='strict'): # type: (Text) -> binary_type """ @@ -415,6 +426,54 @@ def _tryte_from_int(n): return trits_from_int(n, pad=3) +class ChunkIterator(Iterator[TryteString]): + """ + Iterates over a TryteString, in chunks of constant size. + """ + def __init__(self, trytes, chunk_size): + # type: (TryteString, int) -> None + """ + :param trytes: + TryteString to iterate over. + + :param chunk_size: + Number of trytes per chunk. + The final chunk will be padded if it is too short. + """ + super(ChunkIterator, self).__init__() + + self.trytes = trytes + self.chunk_size = chunk_size + + self._offset = 0 + + def __iter__(self): + return self + + def __next__(self): + # type: () -> TryteString + """ + Returns the next chunk in the iterator. + + :raise: + - :py:class:`StopIteration` if there are no more chunks + available. + """ + if self._offset >= len(self.trytes): + raise StopIteration + + chunk = self.trytes[self._offset:self._offset+self.chunk_size] + chunk += b'9' * max(0, self.chunk_size - len(chunk)) + + self._offset += self.chunk_size + + return chunk + + if PY2: + # In Python 2, iterator methods are named a little differently. + next = __next__ + + class Hash(TryteString): """ A TryteString that is exactly one hash long. diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 7f8f14b..41f3671 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -4,7 +4,8 @@ from unittest import TestCase -from iota.crypto.signing import KeyGenerator +from iota import TryteString +from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator from iota.crypto.types import PrivateKey @@ -807,3 +808,263 @@ def test_generator_with_iterations(self): b'I9K9NDMQVNOWBHTZL9' ), ) + + +# noinspection SpellCheckingInspection +class SignatureFragmentGeneratorTestCase(TestCase): + def test_single_block(self): + """ + Creating signature fragments from a PrivateKey exactly 1 block + long. + """ + generator = SignatureFragmentGenerator( + PrivateKey( + b'QFBNTRFTTBHNIPZQNQPXOFQWQCP9KMMMVEPLHYQMVMCWSVSWS9HEEJHNRGKELXEEKI' + b'DBREGBJTIKHBMODRBXEOKYOXTPPYGVSROOMLBHQKFALHNUYEVUAMEYPNRLFFNCDGBF' + b'YZQRLGLVQEPCYGP9HRUWRVIRZYFTFJNEJKCLUMFDXULSOWKVSEMVFXHAHLXBZITIHR' + b'BEDHBAOG9YYKCQDMZZJRDEYTTGVCDPMYUBEAIQUUV9KWPJBNJFFVXTKFJOWXD9RRJF' + b'HBPDMVVQMDTVCRTNFNHVCOLHOLXHIWKJW9ICHUBHCSMDKHVXGEQKFRI9DROBXBPWYV' + b'PDUOEOKOKSYEHKQSDLQIPT9JLKRSZIAGJPBTAMQOADXCZDRFMLTUB9UDYQXCIIYDDX' + b'OMIKLEJKZU9KGJHQLYAWFPPLRADBQDOAEZNENKTPJUQFEWPERZWJNYVSSVNQOBWZAT' + b'WMZOVPPRKBAISZLIDVBZECDULBGKAINVFPAI9QGAGMSQFMSPSNFSKXKNCNRHUMSQPA' + b'CPAPMNSDXAPHQXLFJZKLFGLWNZKUSACXTYKAPKLCGDMRIVGGZSAGLXSYDAKXVEHVPV' + b'VVLGUNACNOVJGOFGZTNILHJTRGSHCLBPJPPRYZMRLKC9EWINJRREXTOIIPJ9MQKHNM' + b'SKCNGLILLJZFZKXSLSLCBBCYSFIJPFPTCB9ALKC9VAZHG9URISFCWTVVUJSMDRZVKP' + b'9AIZJTEUPFEXFHCFLDL9NEVZH9QDC9WXDBXXAWIHQWIZQVXIHL9C9LMGCAGPJZKESY' + b'KADPDLYUHAFQVUJEDFQOSATEZTPIVRQTEMCHTJEODWAJHH9QQAFAUMISXHOLXRWOIZ' + b'GLSDWIOUYZA9SQWFUKEUGGUQUYWXYVKPINSCEPQXQETIOHXLQPHSPNHRBYPOQMWHQA' + b'PXAHZMAPPGCYZIHFQLOIYVOYKRXKRCPYHZMITNYQHVELAUUOELPWGVQNLPIDYNSCMN' + b'QWPSSOGIRHJMEONAIU9ZYRK9HWCKGINSEYVEKLBWCTRUZJFAVBLIWASYZYJFONCRVE' + b'GIAAJFFFOXTRLITRSJSSFGPNJPKNEBJZLQKIV9QPDSMZQIAVZJSKQMBAYIUCKTJMWP' + b'UJULPOCPMMNJ9OYSPPSYRQSRDLVNDDXLWYDEXDGXJOOLXYKBJWBOWSTQVBVHR9OXCA' + b'OOKIW9JCSKHGGSESTAUQIRCIMUPJAMMOEEQYHCHOSTKFVAHYNZ9NJSVWZHGWBRPNQK' + b'ZTHSV9MPBSZHEWCXFXUZLJFOPTPGTXUWPPXXJQMCPOAI9CYTNYCLS9O9CFLXPVCFID' + b'TSL9YT9SNSROTNSYCTKTICYZYAJTTPI9YILQSXZQXPTRTPOGALQMHZIRHPRTRXLT99' + b'9ICOCEUZDXBENFISVLBMUYMVQHQMPLEDCOSAFPVLLORRVJJPLBCZDSKQJXHKXHVVOE' + b'DG9OSKILVSWRHZL9WDKFR9HDEUTDTZQDPEWQQMXCRQSAXPNHHYDWGXHJCPEKJF9SAA' + b'FLENDPJFHKSYBDZKZXXBKUKKAEBBYWKEYSSURNWTQRAYJ9TZ9FFXNA9LXNZAQVCRWQ' + b'KNNAYGPIKUTCJZAMCMJNTRBIQNBMJVTPVMOTTNSGYABZECIOBBIDCKLRSJDESXGVNY' + b'HACSAVTMFICFHLBEBGDMWKEBEICRUFYGMXWXJJ9MHMCRLTAOM9GPOYDNWLYLOQCVKR' + b'HXZFRZKBRFCF9LOGAIGQLCZR9OIRL9HYSAMMJQEXJZMMKMCGGTYOKXVXFBDXDQOQKK' + b'IZG9TMO9CAPGRQSUWE9WKHAIERQRJRZFKS9AHYNNGDNVLXPLLARQHWCFZKATLJNEGG' + b'JAYVDGKSVQDYCIUEELIQCSVTCCFWVTYOMYIHSMN9JMTFSUBOQGCPBZYAFMCQMMQUTN' + b'OOPVKCGRVOEJMIPZKSOLGNBXHTKLWHMTOZRCKHRAIBOAPPSMCLBGXWHBYSTYPGXETK' + b'FHALEOMABXFV9MLUMZBVOFRI9UASHROBDN9EFCTUFZFYLBRUOVZWJENH9QHUJTX9EL' + b'NBKYYTDZSEHJCPORZDIHYABRLZRQZPECYFFNRVCRSNENPFCRJWZPMAQKYUXVPDFFVX' + b'EHIPLPZSFDJCALFZNPAKSLPD9QEBZIZNCAMKBZQFWRCFSVIEBDDSVMQIYDCWLQQGPC' + b'HAXQWELNH' + ), + ) + + self.assertEqual( + generator.send(TryteString(b'UDUX9OKZP')), + + TryteString( + b'CGWXNMRWQRICZ9QULUMMHLTGIC9ILGBPBLX9OXQOAFDUPFOEQMQBZPVIVDWCBOFCN9' + b'KEYSWXIXTUFTJKTFRCEXTVBWZXCTXHCLXRWVFUEGYPBFWAASUQVGOHDYCWZAQVALEZ' + b'GQEGJJYDFSKSRGJAJ9NMMXQIEEUVYBUAELKYVEQARXTOEKCBH9VKKSYMOYBONNJNIL' + b'9KOU9RRIBIHGQJQFMGUONGWMGYJYLJFPUIZQATQBDTZLC9XRPUODTHNU9XFFC9EIGE' + b'VHHTSUQBGWWNSTACF9RTFDSYCVJARDKZYXBYZYNWETUTKDWGXAKFOEHUEHHEXOEVEA' + b'IOBEBORQDVEBZWNMGBWN9AP9WZSTA9EBCBYBCQYKSMGEXRZTLLPLKMG9AS9DESQFOZ' + b'9LF9SWXMENMELCPUSLDDYKG9GYT99LKYIFIWEGJMNXGWOGVVH9LEZCLQSRFZTAKRPR' + b'ZXIOPVBJBBPCBWWDNVVAESOMOOOIE9VTGB9BRNEKGUGSMOYOBRDLVAXCZKXPOSWALF' + b'MMKKVOWROOTSCC9LFOEUNSKMBMTIIPEKFZQOGCJKMJKMCULHLWOYFBCOCIIZCWTEVH' + b'PLJIVTMKO9AVDCJPYMTYLQCSKWTUGZAFMMJRBWUQNCUWGVCLMZHHTQZFZWWQILFJJU' + b'SAKHGNU9KOKVVOBOVGVCZOQMELMYXHCMJXRGDDDXTWC9ATJFAZENRSIVGHDZPCIHEW' + b'NX9MYPJIJEMXQEV9QW9ZFENKFWCNURXPSEAPBJHSNVKSPEYMRJIOKFIRPUFFNCHQWU' + b'JNOZFENUSBIYKOUFWIJNMLNFYEADOT9DXVIRGGBQJPCQXSEGOSQVFSBUYNXAURWQJL' + b'ERIG9GSBREQGEJTSLPNNUFMFKRLCJKVLFANPNKLS9XSRPPZNIYOBMKDKRQILMVWIMT' + b'BKB9FSCZJPHK9L9XNCCMNANBLANGJYAQJXKKBUSRGJXGRMQBBAQVEHVLK9ASZBIVWQ' + b'MYDXVDWPBZPHE9FTMMPEOGDSYMMTSZODGFKUYJQK9SYPXXAPYPEBTQYVDIQFMZJWRG' + b'KQRZQBAQCRDFKZSEGZVDYSPQIKSJWPBKLKIEIICCSAMVICGYSGSKE9KERRDDYCHJ9H' + b'GNTCCMYQHXBHDHEZPKVYGTJJOUWSHNQQCAKLQHLTTBLHPTMQWHUXXQFIAIYGYCLAEB' + b'NMCXLJVPIZOKHGEPVPDIAXNBEXBPF9XCNKFBVXXZYZMDYDQQORPKKIBJCRSQGMZWQU' + b'KZDHQPXPSVSQRDRMYXQQEFXQE9BPAVAVMJNSXMEINAKUJNPXTMMWKAOWQFPGTPYJUI' + b'BCMAZYBKNMMJMIKDWIN9HQFSCV9BCO9ZCJLAV9HHSMWSZWKESZUMQVALTFERWP9U9C' + b'SGBMBQWBDULGEGSYBVUERZKPBEBMVGZSZAGFTZODSSKBYGQNPPAANDNIRNXCOZPBFD' + b'DEONIKKXBNMUI9JFDRHJLYCZZTEPCPBZEOZMPYYSVOOUNDS9IEKNQMHO9TUOGJ9QBT' + b'9HJ9UGMCCZW9UMWJSWPHFRSOKA9PNPTDDRVAYIJDEZRM9XILJMB9QTFCWKQDZGBGWG' + b'KQCNZVEQLGYYDKSQUJFWC9KDJJWEPNBNYIRLXDIJRKFLNAHAUNKWXHCKBXVZTJKD9D' + b'KTLXJ9XFPLM9MZBYNFFDEOCNKAIGSDMORBXQZMKWWOIANNUQEKAGYVSFWXHNIBTSIT' + b'REGQACPJXCIXJIGXCUKBTOAWXEAUSSRERIH9MYHOSZDAFXZWSHXUJ9ELEZEROSIUSL' + b'TYVVNAXAAXQXPQGSPUV9EUSLPZQSQZGFTZHEMPLALLFKQSYCNNADAMSTJAWDXXSMSE' + b'AG9T99RF9LFHG9FEPUVQLBOUUFTVUXTDGAQQEMJWMZWFWHJHBTVONFREZUBZXNFITC' + b'YPKUV9TJJOTDQPRFO9HABDWYYWP9OVFPPKXMFOHDYOGGR9XEORCSZHQZSEDIMQZSNO' + b'Q9BC9IUHSWCLJYLOUTXGWIUH9HEHDRPYTI9KRRNLEYGPCPEDSLKDNPGJXQPKHVYGPT' + b'FFBVUVAEQEKEJJVAZCTCICKAPCN9RSMJSXBOKRXUZPZWFRULCPLANWMJNKEVRIKECA' + b'DFHLDDRHJMOFOXYPOQGSQXLMQAMYFPTTKVFYYWNHPP9CLOROZWBNVHCFGSCIGCPSOA' + b'YBFLZDHBO' + ), + ) + + # A generator can only generate one signature fragment per block in + # its private key. + with self.assertRaises(StopIteration): + generator.send(TryteString(b'')) + + def test_multiple_blocks(self): + """ + Creating signature fragments from a PrivateKey longer than 1 block. + """ + generator = SignatureFragmentGenerator( + PrivateKey( + b'MPRSBNPSAZRHZSBXSJASXSEHLYVLBXHCUNNQUNYB9CYEVWNBJKZUODPCZGOBRJ9V9C' + b'CARPTZEQRXGYDBEZISGMMXGHHHATAUDRTWUHEKJAMKUUKCK9NUUEWEFHPTVAARNLJB' + b'XRTFHQDXAVAOWYGJJNLCRVWQFHXPTMLFPGDKEBZCSSOQZEPNRTUKUJ9TRHLWQKOCBZ' + b'WBXBQVKUDTKV9IKCDQZRXRGQGIKCQ9BUNYMDFALF9QUFVKHQEHFICORMLLELVXM9VM' + b'ZIBUFKHLFGHDCPD9CBZVU9PMXWTAKWCBPBRBUG9PIAYYHKTP9VGDQIP9GUPKK9DVZI' + b'UYYTBTBYCDDF9LPBRGNGW9GHHFGRQPYZKKLCUDUAPAKZVIWL9RSJFQO9L9WKMBSWTY' + b'LCJJGWTWNLLKMHOUQVKTOUQGEOYNLEHTHJQIPV9ACBEBU9DN9KZWCEZIDEGQYQEYVY' + b'WUMPHMJAXOVEXOUMJ9MANMTGBOJAHUGHEYDPUQLNBIRUCLTXRFQIBTEWGUAHIJL9RK' + b'LMGKURCYGJLXGFLDF9HPSQUCGSZQDUQGZSYKRKHCCPBAPEBWJUYOBBGUXVSWHRAKWT' + b'TMHGJAWM9RPCLMVVNOWNXPBNTPFYWQIPQLNOJJYNRDTLAUBZPLVUACUSPNBHCOPO9Z' + b'XSIETGTFDLMRESLN9WXUNQ9SFQFD9CNIETCSVDMNLGNMKOOTOCODTDCPLZPUYON9HD' + b'QLDGSCWHKDLQOGDHZHPURB9TSUGVSPKDIZE9ETVMLVMNDRJOOUUFDTSKDDTOGXEVFN' + b'BDSBRDZPZTUGYZNVKOIFZYRUIZI9LRIFCCXPHJMHCDOUPKJVUGFPTFI9XPGRDHRZUQ' + b'UXCK9MTBINIJFFCQTECFTVTILTKRYJTI9ADGPHFBKNLAPLPKKAOOIEKQLBCDGTZZEF' + b'KDBRICXLWZQXXCYVDFAJGKJSS9PADMDCJGJSYSIAOUFHW9CHTDXLVE9PBFCMBZKIHZ' + b'9VBCXUDBUUEFNRHI9FXC9KREKFVDGHBFDHNEGCIM9CVSJEQSU9FUKVYSVWCTLM9LAB' + b'JCWMPTUHULMGQBVRFCYSGXEQJHMZIVGTIS9QVPTTULFKLSMIPMBUTBXTUQRBOJVUMP' + b'BGWEHIFRMNWLHQY9XKFCSGISDWSDILBOSYYWUSICMRWYUWTJY9GW9CC9ZQOKWMIYYD' + b'ZRZEKKKJVD9DWTTBEPAJQOVOYPVDGRILVPYXRJZSUXKHCWGVPQNDWKCXJO99AUPVZB' + b'VOFELMUKYCRETU9QVUYXXEWHBFFWQOLIHFLRKLGECUMUMHWYESXOUPWLEFMJZVXWJH' + b'EBSYVSRSIFXRWBCAEDNUZWEHAKUPAIGIQKCVNURYELBRSKENYWEVPEORWIQHHCSGYW' + b'XADSSJGDGTVDUHUNJBPTOTKOS9KKSWSJPRGUB9AFTBBUAMEIBBGIQCCZDRHCCYADMI' + b'ATAOUOJOWNUWUKKMYVHNLJSZMTBHYODMJW9RFZNUJCCGTKMVPTBLLUOKXWVJEKSBQW' + b'TGNJBEU99LIUBZIHIMX9GHTEXPDMH9OXPFFGMYQFGEJV9TQJPI9ZFVCFZHMRLDVLUG' + b'MALNTWOMCNQGHQKZRHSLKFCHDFTOBEOQZRAPDOFEKTQZVRUDCYVZCLONUKMUYIIJRD' + b'BVHOZINOYECRMGGIXZNSAGZUOYCNSCEB9EPMZDVMIIBHHLEKSWIDGBOMJRMU9KXKPX' + b'DTPULQZNSFKNIFWNPWBRSAQHESXMNCMPIMQLPOKEDFWPYCVYHMXLFFMIQGGBTAM9CP' + b'COUJBMZWLYIXOGVFYGLRKSYMPMQBNUMSBV9JXWB9BLJNXOORUTLGI9WSAGFDUVL9DE' + b'YGVGYVWUCXXLJQFDGXJWYWNHIUJXFCRWJLVUD9CTLXKZSLGGG9UOKYUX9ESVPNUYLV' + b'LGSBVCA99HYBFYZYBEKVSRBFWDURRA9OVXQLKSUAEMKNT9FVYPPRQHCIVPXUS9KP9R' + b'OEZ9NOZHYLIURCFY9XFYUEKWQHYKNZEJWKJMFFDJCEWDGVSQLNSQRMJALTGQJIFNED' + b'PSYXCIRKYURDVHTNWGRZLDETFXQGWAJTGWAIXXCBNSMMVFCJELLMLEFFYOGNISGWKJ' + b'KLAXTGLAYNWEMXUZBKTCFNRRHWMCXMIDPATZCYQFFGNAKTGSIZYPQXDIYUGFCO9YBR' + b'LAOEGJUJNL9SHU9NPYQGOLUTDTNQHZPNQGWXXNWYUHLNTCRCOZTVZJQGNSFQLKSNEU' + b'LEEYHDMWAWVVHCXVVYNRDLPWDLRGHNPEAOCYESSMABTSMDMPNJHMAKWVEZSQCATTPD' + b'IMRA9JOZPVKAZWMEEKTQNQVCRQBVLPCJCKBPNSWFJBM9JBNCUETNUVOHHCZZLMBIOE' + b'QEWUOGNIETNOKGFLLKKZUIRDLBAHMZQ9JUWXO9QHZ9XZNAFGQTCJYJUYNKIYTTNQXQ' + b'DSUWKEZACQBYRWLTTINDLBRSZPESTWV9KKYCBEEVPKFQIK9RGIADVNDWWMPQOWLYAR' + b'VK9BNZLFEZGUHZW9KFZAEDUTVTSGSTGFPCJNX9MGLDFXMTYWXEHOYBTQCVCEXNQNRI' + b'YEUGEUJ9AUXMNUTIVYB9VRNVTAAEQTQRPEKYOLECFGPZWWOTIEZMSA9P9QCMCSDMUS' + b'HDJOQEUUKFCSHQTWHEHJWQPEXQNKVPNUEMBA9CVOEVJEGJORYWZAOIVWHAMCRVPVTW' + b'IMEJNHZ9PBSIPNEUWAQBNSOAWYA9GLPWCNTSAIHLSQD9LJJIVLLVXWSNQHWLYQMQFB' + b'WTAUYUGAWOICQJFWTJXN9BLVQDZSTTTGJCCWGWEBGTGODNDCGCSXZMBTSFEZIKMCOZ' + b'WJU9IVFAEIGHLCLUNERARLHWQICKGYYEUBBPEEHHHGPTDPEMHMNRYKIXOASAVPN9TT' + b'YDEYZHVDGVVTICNCXRMJKJLWJDVPNPQCQESACMEZRDVCFTGMEEHEUFAWZDLNFT9PNI' + b'Q9WTWZAMDRYSBSCXIOOYAMQBAHJEFLDUQHPZYDQRPA9MEKAMRBZGTUBRTNYYGBGAY9' + b'YIERRWFRMNKGTZSXBONWLGSQVBL9HYSUSUHTVPTIHFFXWDCZSGTBDZQMLPUCYWWVBF' + b'9JNJPCQODJYOHXRUTEPRJZW9MJHKQLGIFGQRQWRSBFPASG9YLKOHOKRZQQZPM9MNQC' + b'AWAZIU9JRPDXBNMBIYFJUYAEWQSMLSZUPLBKHGIJTNXYQASQUMPMKWIZ9DMINF9CYG' + b'ZMILHRJOAJQEOVVALRYQGPPTUOD9OIOHIGRGGJMVKCZUNK9QVBLBTMXJRYWNVY9MZN' + b'HZLVJUNZEYICXLLMQSYFPRZQAYJCX9KKFFTPZFCUKRV9EMRHPGPQ9ZNBYXXZXL9LGZ' + b'LJZPKVA9HHZEBIIPJYIEBQJO9LIOPTWSUMOLHG9AEJWGANZBFG9IDPFTDDBFYBDPDJ' + b'GQEPDFYAYADLQOODRXA9CBUDYXEJWITAKRMBACRDXDCPGGPDCQRHACTJIIVD9XTLVR' + b'NNTWCMDTZXXVUOHBOYWCGIFCNEH9AG9KD99LOGNAYIMCRLVTCCCRFWHKVE99AOWOGU' + b'UXNITAEYHJHVVOKOVFXVOSXBTGVIVNDJAZITQKXXXLLIMOAKOMZIIXEEYXGNQVYFQQ' + b'TJJEDXSDPFA9JZNMWGADIUNVFOURDTGPHCKPEHJMIKZFISIYFOVWWYZYKFRXRH9TCJ' + b'GOLCHPYDVFIPRCYLUINFQRBXJ9EDFRJPGPERLTJNTNJMDYHVLYSY9ZALOOEGBNLJQV' + b'PAOPVMQPZ9QSUYW9FQFQYKFOYYYPQHIFEWPVWURDY9DWBBVOADLLEFXNLGIFYQXKY9' + b'CDDYQXV9EP9MMJW9SUBHL9BGCXYF9LMGUIJDEDJOTSRZZGPKOWHOGFI9QJRNIN9FVB' + b'WVFOAJGTYJBOWEPFZDHGKYVGB9IHHCHZODFWRJQSSYDV9IURZBCJALECSLQCXYHAUC' + b'GJZNB99QZFZ9FCPFFRXJCUYGWMKRGICUDAUYBKIPPXRQOVBYPKALVCRBQATSBOONFP' + b'EMTIYNOSEUC9JLBFMMQTVTBBXVJV9FIOXQDSJCZVTERZHSMTK9JKUYUHRJWOXXTZDD' + b'XMJSUTPFQJHKDBMOUWSZRHUJAQOTPXINF9WDGRG99OSMKXGY9WCD9AJVXEDNLWSBWB' + b'IHO9ODBQSQL9TUMJZASGNTWUQGNNIWSBNGXRSCLYCGRNTEYTGJAIIXX9YNGHLDZ9QV' + b'BCQNMVIQ9EXTSCQAJJROGKIHJJTKLOABYQDHMXJFEKFN9L9FSYAMVOFTXIOSEOBBUA' + b'UYSHWINHCJRQTIXWWPZBGKIGJC9TDKFAKZTIFBSDFSXVJCCICIBEAIGNUARDZGGBBH' + b'RLMWQ9RSJB9TPLSDEP' + ) + ) + + self.assertEqual( + generator.send(TryteString(b'WGXG9AGGI')), + + TryteString( + b'GNCUHGZVIYRQQBXXBUONVL9COKOYDERJAWWI9YRWBVUJLDQQCBMFOORHRTVPKUDFWK' + b'YPOOCNVDQOCTJNVCFOENDWEGCXOXZK9BNFGAUTSLGPIWCIWFQAGQTVECGNXLFCUTFQ' + b'UJRUQKPJEDUFZTABRPTDXCIE99ZSGZFQCJYRZRSSUEENDBMDRZPLI9KCTVAYTFVRQB' + b'HDFLQZJBHIFFDVJPIJFCMOYNKOXQNQHSNDBWXRHLVBXBRLICTY9KJKWK9TZBSNIHKM' + b'ADAWFIK9YZVZFZWSZANYEIGQKOVRDGGTGFYFLGKESJKLUJMCKGGAJZHFQJXWIJ9HFI' + b'URAXKZKTNULKABTUDTNVQEJRMVUXGOOWAUMSQCUBAOLEFO9PTVPCIFMFJGYAPNFIDW' + b'T9TASFHSPXTRVEBRNJOVJIPGMNEWHGZZTQRRHEG9CXBLUJJSCENYKRXKRFPHAOEY9G' + b'RNLWUCLXXOBQOSSOVEXZGHBMJKQ9YDQPRGSSNXOFZMUOGKWENWIVIVMDDLPACSKPRS' + b'99PNRADWBWSVTXIGGW9THJEQNWEGCWOVSLVWITMUWIUQGSGBYXNKSKUCLFEHWLOOID' + b'OKTIOJGPUZQBLHYUKCH9IKPJCRWXBNXGSHIZXYCSMOOQZROSDMNGEMHSKKDOYPKXAI' + b'DS9UUINN9RSVSDJUODJ99VEJMLLQRJFPYAZDKTNXPHZYBV9AERYOUJKBXACGZTWWGV' + b'NVIDNQPRK9HNEGOVOKQBXOXHZLHFNXDSFJECQVRJFJ9VHSCZACCHRKLWEGE9LAFEQY' + b'TFYAGUSYZGFXOIUVATAZCTDMOMVOVMWTDFXRNLHPSYMQKVGRRINYVOBVUXMPZPXBMM' + b'OFPNNGDCZHBPQBGMZJKOHOIKTXYZAAFKZEZPTESTUQH9HPNUQKUD9GJSSVDIJQGFRI' + b'FULCYSKXTASWYBMXSNJWFFJA9IQOBJYVZVYDXZYMWIYJLXIPGGKULHDBEXYCLQQFXA' + b'DJEEVJW9DFWDJQTS9AJWQQZLOYOSQW9DEHHENQVVOKFSBNHSJQHTPGIBLNTCXATNOH' + b'TIOQFDIQEZFWWHCUEGNAYAPMUGSNCZQIWGKCYAWZWGVLEZVSZIJKD9EHINYFUVZLPR' + b'NIVUNZBVUGMQAUKEDPQWZNKWAJZDVHIFLSQWBGVOXIVBZTYRDBFNXUFFNSSJHJLYJA' + b'9MYYLKRFEWMQNYJWYTDNIVNJFCEHBDE99SZTKKGZUQO9AMVGYCIUOCLECKEUBSWBNB' + b'9QYSWEUAVROPHIFQPAQUIIPGGTGYSWDFPYYCRFBHBHAUDUX9OKZPIZEOFEIMVHVAKR' + b'QJ9HXVC99RNOEOAZVPPIJCWGKCAJOOWRFXSWLAPBNIQGNNKXSBLMNFJCUBYDMPKS9V' + b'ASYWPE9WPMZLKSMOFQBPULYKIVBTHTUUNGGRPOOMSSSNUCP9ASWMGONSIANVRPEYLF' + b'OPJNSBCXKQSVPECWUWEHIOSVRAKAUTDCPGGMPQXDJFQAAFJQTIDUUFGDMGVZDJCNOS' + b'YOEAFJSKXUPHNGDJH9TSKJZGPUETDIFCUQUVARHETQFSRLSBCJCLBR9BJNDTNTWH9P' + b'HU99TPAMMYYWJSISCKTBGMROVKWVZTOOIUTOERQKHGPTOZAWKKRGRYLZTJPPUURSMF' + b'H9H9EMWIWKJYMWBLNQPFHBUGFKDJHEHORXXDDSFXAAQBSKIORGQNHZYRSUSRNLIBID' + b'AVDPZKOLLPOWTCPPUJFTOLMGSPASTNIXBHIJJSBRMREVBCZIPRVBQVPQ9AYYPOVK9L' + b'BOQQCGIXAYOXYIC9SYFMGAGZJOIHUZCATPJJOHVAZXLPUVLDJKBXQNPRTWHKH9ZHGC' + b'ILKVHYOXNVVWQFMXYDKEAQMXZLNZTFKWRTBTCZLTRCFVQSYG9ZSSZTQJQSHMBOMIQS' + b'KNWRG9MEUHWPGAJPJBDKPQOCXDYAIYMUKBOCMTVVZXLPIYQ9XZOJWMICKLECKCRJUX' + b'LEYRZCIQNFOIHKJPHTKFTS9YQMBXLCWZBZOWPEWMHLDEHBADWNCSGUYYYNGDGPMJGK' + b'MUQ9QCPVPJIOQAZOXMIP9POREGZIDEXJOWXHYTOILXMQBXRGBBNAOIJLRANGZGZSOM' + b'XQUEEUWXS9HSNOTUATDNMNIOLERHGBLZN9REGCRXIMGZWTHIGGL9VFGZZFURDCLVCT' + b'VKMUTYC9C' + ), + ) + + self.assertEqual( + # Just to be tricky, try sending the same source trytes for the + # second iteration. + generator.send(TryteString(b'WGXG9AGGI')), + + # The generator used the second block of the private key this + # time, so we get a different result. + TryteString( + b'QFBNTRFTTBHNIPZQNQPXOFQWQCP9KMMMVEPLHYQMVMCWSVSWS9HEEJHNRGKELXEEKI' + b'DBREGBJTIKHBMODRBXEOKYOXTPPYGVSROOMLBHQKFALHNUYEVUAMEYPNRLFFNCDGBF' + b'YZQRLGLVQEPCYGP9HRUWRVIRZYFTFJNEJKCLUMFDXULSOWKVSEMVFXHAHLXBZITIHR' + b'BEDHBAOG9YYKCQDMZZJRDEYTTGVCDPMYUBEAIQUUV9KWPJBNJFFVXTKFJOWXD9RRJF' + b'HBPDMVVQMDTVCRTNFNHVCOLHOLXHIWKJW9ICHUBHCSMDKHVXGEQKFRI9DROBXBPWYV' + b'PDUOEOKOKSYEHKQSDLQIPT9JLKRSZIAGJPBTAMQOADXCZDRFMLTUB9UDYQXCIIYDDX' + b'OMIKLEJKZU9KGJHQLYAWFPPLRADBQDOAEZNENKTPJUQFEWPERZWJNYVSSVNQOBWZAT' + b'WMZOVPPRKBAISZLIDVBZECDULBGKAINVFPAI9QGAGMSQFMSPSNFSKXKNCNRHUMSQPA' + b'CPAPMNSDXAPHQXLFJZKLFGLWNZKUSACXTYKAPKLCGDMRIVGGZSAGLXSYDAKXVEHVPV' + b'VVLGUNACNOVJGOFGZTNILHJTRGSHCLBPJPPRYZMRLKC9EWINJRREXTOIIPJ9MQKHNM' + b'SKCNGLILLJZFZKXSLSLCBBCYSFIJPFPTCB9ALKC9VAZHG9URISFCWTVVUJSMDRZVKP' + b'9AIZJTEUPFEXFHCFLDL9NEVZH9QDC9WXDBXXAWIHQWIZQVXIHL9C9LMGCAGPJZKESY' + b'KADPDLYUHAFQVUJEDFQOSATEZTPIVRQTEMCHTJEODWAJHH9QQAFAUMISXHOLXRWOIZ' + b'GLSDWIOUYZA9SQWFUKEUGGUQUYWXYVKPINSCEPQXQETIOHXLQPHSPNHRBYPOQMWHQA' + b'PXAHZMAPPGCYZIHFQLOIYVOYKRXKRCPYHZMITNYQHVELAUUOELPWGVQNLPIDYNSCMN' + b'QWPSSOGIRHJMEONAIU9ZYRK9HWCKGINSEYVEKLBWCTRUZJFAVBLIWASYZYJFONCRVE' + b'GIAAJFFFOXTRLITRSJSSFGPNJPKNEBJZLQKIV9QPDSMZQIAVZJSKQMBAYIUCKTJMWP' + b'UJULPOCPMMNJ9OYSPPSYRQSRDLVNDDXLWYDEXDGXJOOLXYKBJWBOWSTQVBVHR9OXCA' + b'OOKIW9JCSKHGGSESTAUQIRCIMUPJAMMOEEQYHCHOSTKFVAHYNZ9NJSVWZHGWBRPNQK' + b'ZTHSV9MPBSZHEWCXFXUZLJFOPTPGTXUWPPXXJQMCPOAI9CYTNYCLS9O9CFLXPVCFID' + b'TSL9YT9SNSROTNSYCTKTICYZYAJTTPI9YILQSXZQXPTRTPOGALQMHZIRHPRTRXLT99' + b'9ICOCEUZDXBENFISVLBMUYMVQHQMPLEDCOSAFPVLLORRVJJPLBCZDSKQJXHKXHVVOE' + b'DG9OSKILVSWRHZL9WDKFR9HDEUTDTZQDPEWQQMXCRQSAXPNHHYDWGXHJCPEKJF9SAA' + b'FLENDPJFHKSYBDZKZXXBKUKKAEBBYWKEYSSURNWTQRAYJ9TZ9FFXNA9LXNZAQVCRWQ' + b'KNNAYGPIKUTCJZAMCMJNTRBIQNBMJVTPVMOTTNSGYABZECIOBBIDCKLRSJDESXGVNY' + b'HACSAVTMFICFHLBEBGDMWKEBEICRUFYGMXWXJJ9MHMCRLTAOM9GPOYDNWLYLOQCVKR' + b'HXZFRZKBRFCF9LOGAIGQLCZR9OIRL9HYSAMMJQEXJZMMKMCGGTYOKXVXFBDXDQOQKK' + b'IZG9TMO9CAPGRQSUWE9WKHAIERQRJRZFKS9AHYNNGDNVLXPLLARQHWCFZKATLJNEGG' + b'JAYVDGKSVQDYCIUEELIQCSVTCCFWVTYOMYIHSMN9JMTFSUBOQGCPBZYAFMCQMMQUTN' + b'OOPVKCGRVOEJMIPZKSOLGNBXHTKLWHMTOZRCKHRAIBOAPPSMCLBGXWHBYSTYPGXETK' + b'FHALEOMABXFV9MLUMZBVOFRI9UASHROBDN9EFCTUFZFYLBRUOVZWJENH9QHUJTX9EL' + b'NBKYYTDZSEHJCPORZDIHYABRLZRQZPECYFFNRVCRSNENPFCRJWZPMAQKYUXVPDFFVX' + b'EHIPLPZSFDJCALFZNPAKSLPD9QEBZIZNCAMKBZQFWRCFSVIEBDDSVMQIYDCWLQQGPC' + b'HAXQWELNH' + ), + ) + + # A generator can only generate one signature fragment per block in + # its private key. + with self.assertRaises(StopIteration): + generator.send(TryteString(b'')) diff --git a/test/crypto/types_test.py b/test/crypto/types_test.py index 52ce21c..974e769 100644 --- a/test/crypto/types_test.py +++ b/test/crypto/types_test.py @@ -10,121 +10,6 @@ # noinspection SpellCheckingInspection class PrivateKeyTestCase(TestCase): - def test_create_signature_fragment(self): - """ - Creating a signature fragment. - """ - key = PrivateKey( - b'MPRSBNPSAZRHZSBXSJASXSEHLYVLBXHCUNNQUNYB9CYEVWNBJKZUODPCZGOBRJ9V9C' - b'CARPTZEQRXGYDBEZISGMMXGHHHATAUDRTWUHEKJAMKUUKCK9NUUEWEFHPTVAARNLJB' - b'XRTFHQDXAVAOWYGJJNLCRVWQFHXPTMLFPGDKEBZCSSOQZEPNRTUKUJ9TRHLWQKOCBZ' - b'WBXBQVKUDTKV9IKCDQZRXRGQGIKCQ9BUNYMDFALF9QUFVKHQEHFICORMLLELVXM9VM' - b'ZIBUFKHLFGHDCPD9CBZVU9PMXWTAKWCBPBRBUG9PIAYYHKTP9VGDQIP9GUPKK9DVZI' - b'UYYTBTBYCDDF9LPBRGNGW9GHHFGRQPYZKKLCUDUAPAKZVIWL9RSJFQO9L9WKMBSWTY' - b'LCJJGWTWNLLKMHOUQVKTOUQGEOYNLEHTHJQIPV9ACBEBU9DN9KZWCEZIDEGQYQEYVY' - b'WUMPHMJAXOVEXOUMJ9MANMTGBOJAHUGHEYDPUQLNBIRUCLTXRFQIBTEWGUAHIJL9RK' - b'LMGKURCYGJLXGFLDF9HPSQUCGSZQDUQGZSYKRKHCCPBAPEBWJUYOBBGUXVSWHRAKWT' - b'TMHGJAWM9RPCLMVVNOWNXPBNTPFYWQIPQLNOJJYNRDTLAUBZPLVUACUSPNBHCOPO9Z' - b'XSIETGTFDLMRESLN9WXUNQ9SFQFD9CNIETCSVDMNLGNMKOOTOCODTDCPLZPUYON9HD' - b'QLDGSCWHKDLQOGDHZHPURB9TSUGVSPKDIZE9ETVMLVMNDRJOOUUFDTSKDDTOGXEVFN' - b'BDSBRDZPZTUGYZNVKOIFZYRUIZI9LRIFCCXPHJMHCDOUPKJVUGFPTFI9XPGRDHRZUQ' - b'UXCK9MTBINIJFFCQTECFTVTILTKRYJTI9ADGPHFBKNLAPLPKKAOOIEKQLBCDGTZZEF' - b'KDBRICXLWZQXXCYVDFAJGKJSS9PADMDCJGJSYSIAOUFHW9CHTDXLVE9PBFCMBZKIHZ' - b'9VBCXUDBUUEFNRHI9FXC9KREKFVDGHBFDHNEGCIM9CVSJEQSU9FUKVYSVWCTLM9LAB' - b'JCWMPTUHULMGQBVRFCYSGXEQJHMZIVGTIS9QVPTTULFKLSMIPMBUTBXTUQRBOJVUMP' - b'BGWEHIFRMNWLHQY9XKFCSGISDWSDILBOSYYWUSICMRWYUWTJY9GW9CC9ZQOKWMIYYD' - b'ZRZEKKKJVD9DWTTBEPAJQOVOYPVDGRILVPYXRJZSUXKHCWGVPQNDWKCXJO99AUPVZB' - b'VOFELMUKYCRETU9QVUYXXEWHBFFWQOLIHFLRKLGECUMUMHWYESXOUPWLEFMJZVXWJH' - b'EBSYVSRSIFXRWBCAEDNUZWEHAKUPAIGIQKCVNURYELBRSKENYWEVPEORWIQHHCSGYW' - b'XADSSJGDGTVDUHUNJBPTOTKOS9KKSWSJPRGUB9AFTBBUAMEIBBGIQCCZDRHCCYADMI' - b'ATAOUOJOWNUWUKKMYVHNLJSZMTBHYODMJW9RFZNUJCCGTKMVPTBLLUOKXWVJEKSBQW' - b'TGNJBEU99LIUBZIHIMX9GHTEXPDMH9OXPFFGMYQFGEJV9TQJPI9ZFVCFZHMRLDVLUG' - b'MALNTWOMCNQGHQKZRHSLKFCHDFTOBEOQZRAPDOFEKTQZVRUDCYVZCLONUKMUYIIJRD' - b'BVHOZINOYECRMGGIXZNSAGZUOYCNSCEB9EPMZDVMIIBHHLEKSWIDGBOMJRMU9KXKPX' - b'DTPULQZNSFKNIFWNPWBRSAQHESXMNCMPIMQLPOKEDFWPYCVYHMXLFFMIQGGBTAM9CP' - b'COUJBMZWLYIXOGVFYGLRKSYMPMQBNUMSBV9JXWB9BLJNXOORUTLGI9WSAGFDUVL9DE' - b'YGVGYVWUCXXLJQFDGXJWYWNHIUJXFCRWJLVUD9CTLXKZSLGGG9UOKYUX9ESVPNUYLV' - b'LGSBVCA99HYBFYZYBEKVSRBFWDURRA9OVXQLKSUAEMKNT9FVYPPRQHCIVPXUS9KP9R' - b'OEZ9NOZHYLIURCFY9XFYUEKWQHYKNZEJWKJMFFDJCEWDGVSQLNSQRMJALTGQJIFNED' - b'PSYXCIRKYURDVHTNWGRZLDETFXQGWAJTGWAIXXCBNSMMVFCJELLMLEFFYOGNISGWKJ' - b'KLAXTGLAYNWEMXUZBKTCFNRRHWMCXMIDPATZCYQFFGNAKTGSIZYPQXDIYUGFCO9YBR' - b'LAOEGJUJNL9SHU9NPYQGOLUTDTNQHZPNQGWXXNWYUHLNTCRCOZTVZJQGNSFQLKSNEU' - b'LEEYHDMWAWVVHCXVVYNRDLPWDLRGHNPEAOCYESSMABTSMDMPNJHMAKWVEZSQCATTPD' - b'IMRA9JOZPVKAZWMEEKTQNQVCRQBVLPCJCKBPNSWFJBM9JBNCUETNUVOHHCZZLMBIOE' - b'QEWUOGNIETNOKGFLLKKZUIRDLBAHMZQ9JUWXO9QHZ9XZNAFGQTCJYJUYNKIYTTNQXQ' - b'DSUWKEZACQBYRWLTTINDLBRSZPESTWV9KKYCBEEVPKFQIK9RGIADVNDWWMPQOWLYAR' - b'VK9BNZLFEZGUHZW9KFZAEDUTVTSGSTGFPCJNX9MGLDFXMTYWXEHOYBTQCVCEXNQNRI' - b'YEUGEUJ9AUXMNUTIVYB9VRNVTAAEQTQRPEKYOLECFGPZWWOTIEZMSA9P9QCMCSDMUS' - b'HDJOQEUUKFCSHQTWHEHJWQPEXQNKVPNUEMBA9CVOEVJEGJORYWZAOIVWHAMCRVPVTW' - b'IMEJNHZ9PBSIPNEUWAQBNSOAWYA9GLPWCNTSAIHLSQD9LJJIVLLVXWSNQHWLYQMQFB' - b'WTAUYUGAWOICQJFWTJXN9BLVQDZSTTTGJCCWGWEBGTGODNDCGCSXZMBTSFEZIKMCOZ' - b'WJU9IVFAEIGHLCLUNERARLHWQICKGYYEUBBPEEHHHGPTDPEMHMNRYKIXOASAVPN9TT' - b'YDEYZHVDGVVTICNCXRMJKJLWJDVPNPQCQESACMEZRDVCFTGMEEHEUFAWZDLNFT9PNI' - b'Q9WTWZAMDRYSBSCXIOOYAMQBAHJEFLDUQHPZYDQRPA9MEKAMRBZGTUBRTNYYGBGAY9' - b'YIERRWFRMNKGTZSXBONWLGSQVBL9HYSUSUHTVPTIHFFXWDCZSGTBDZQMLPUCYWWVBF' - b'9JNJPCQODJYOHXRUTEPRJZW9MJHKQLGIFGQRQWRSBFPASG9YLKOHOKRZQQZPM9MNQC' - b'AWAZIU9JRPDXBNMBIYFJUYAEWQSMLSZUPLBKHGIJTNXYQASQUMPMKWIZ9DMINF9CYG' - b'ZMILHRJOAJQEOVVALRYQGPPTUOD9OIOHIGRGGJMVKCZUNK9QVBLBTMXJRYWNVY9MZN' - b'HZLVJUNZEYICXLLMQSYFPRZQAYJCX9KKFFTPZFCUKRV9EMRHPGPQ9ZNBYXXZXL9LGZ' - b'LJZPKVA9HHZEBIIPJYIEBQJO9LIOPTWSUMOLHG9AEJWGANZBFG9IDPFTDDBFYBDPDJ' - b'GQEPDFYAYADLQOODRXA9CBUDYXEJWITAKRMBACRDXDCPGGPDCQRHACTJIIVD9XTLVR' - b'NNTWCMDTZXXVUOHBOYWCGIFCNEH9AG9KD99LOGNAYIMCRLVTCCCRFWHKVE99AOWOGU' - b'UXNITAEYHJHVVOKOVFXVOSXBTGVIVNDJAZITQKXXXLLIMOAKOMZIIXEEYXGNQVYFQQ' - b'TJJEDXSDPFA9JZNMWGADIUNVFOURDTGPHCKPEHJMIKZFISIYFOVWWYZYKFRXRH9TCJ' - b'GOLCHPYDVFIPRCYLUINFQRBXJ9EDFRJPGPERLTJNTNJMDYHVLYSY9ZALOOEGBNLJQV' - b'PAOPVMQPZ9QSUYW9FQFQYKFOYYYPQHIFEWPVWURDY9DWBBVOADLLEFXNLGIFYQXKY9' - b'CDDYQXV9EP9MMJW9SUBHL9BGCXYF9LMGUIJDEDJOTSRZZGPKOWHOGFI9QJRNIN9FVB' - b'WVFOAJGTYJBOWEPFZDHGKYVGB9IHHCHZODFWRJQSSYDV9IURZBCJALECSLQCXYHAUC' - b'GJZNB99QZFZ9FCPFFRXJCUYGWMKRGICUDAUYBKIPPXRQOVBYPKALVCRBQATSBOONFP' - b'EMTIYNOSEUC9JLBFMMQTVTBBXVJV9FIOXQDSJCZVTERZHSMTK9JKUYUHRJWOXXTZDD' - b'XMJSUTPFQJHKDBMOUWSZRHUJAQOTPXINF9WDGRG99OSMKXGY9WCD9AJVXEDNLWSBWB' - b'IHO9ODBQSQL9TUMJZASGNTWUQGNNIWSBNGXRSCLYCGRNTEYTGJAIIXX9YNGHLDZ9QV' - b'BCQNMVIQ9EXTSCQAJJROGKIHJJTKLOABYQDHMXJFEKFN9L9FSYAMVOFTXIOSEOBBUA' - b'UYSHWINHCJRQTIXWWPZBGKIGJC9TDKFAKZTIFBSDFSXVJCCICIBEAIGNUARDZGGBBH' - b'RLMWQ9RSJB9TPLSDEP' - ) - - self.assertEqual( - TryteString.from_trits( - key.create_signature(TryteString(b'WGXG9AGGI'), blocks=1) - ), - - b'GNCUHGZVIYRQQBXXBUONVL9COKOYDERJAWWI9YRWBVUJLDQQCBMFOORHRTVPKUDFWK' - b'YPOOCNVDQOCTJNVCFOENDWEGCXOXZK9BNFGAUTSLGPIWCIWFQAGQTVECGNXLFCUTFQ' - b'UJRUQKPJEDUFZTABRPTDXCIE99ZSGZFQCJYRZRSSUEENDBMDRZPLI9KCTVAYTFVRQB' - b'HDFLQZJBHIFFDVJPIJFCMOYNKOXQNQHSNDBWXRHLVBXBRLICTY9KJKWK9TZBSNIHKM' - b'ADAWFIK9YZVZFZWSZANYEIGQKOVRDGGTGFYFLGKESJKLUJMCKGGAJZHFQJXWIJ9HFI' - b'URAXKZKTNULKABTUDTNVQEJRMVUXGOOWAUMSQCUBAOLEFO9PTVPCIFMFJGYAPNFIDW' - b'T9TASFHSPXTRVEBRNJOVJIPGMNEWHGZZTQRRHEG9CXBLUJJSCENYKRXKRFPHAOEY9G' - b'RNLWUCLXXOBQOSSOVEXZGHBMJKQ9YDQPRGSSNXOFZMUOGKWENWIVIVMDDLPACSKPRS' - b'99PNRADWBWSVTXIGGW9THJEQNWEGCWOVSLVWITMUWIUQGSGBYXNKSKUCLFEHWLOOID' - b'OKTIOJGPUZQBLHYUKCH9IKPJCRWXBNXGSHIZXYCSMOOQZROSDMNGEMHSKKDOYPKXAI' - b'DS9UUINN9RSVSDJUODJ99VEJMLLQRJFPYAZDKTNXPHZYBV9AERYOUJKBXACGZTWWGV' - b'NVIDNQPRK9HNEGOVOKQBXOXHZLHFNXDSFJECQVRJFJ9VHSCZACCHRKLWEGE9LAFEQY' - b'TFYAGUSYZGFXOIUVATAZCTDMOMVOVMWTDFXRNLHPSYMQKVGRRINYVOBVUXMPZPXBMM' - b'OFPNNGDCZHBPQBGMZJKOHOIKTXYZAAFKZEZPTESTUQH9HPNUQKUD9GJSSVDIJQGFRI' - b'FULCYSKXTASWYBMXSNJWFFJA9IQOBJYVZVYDXZYMWIYJLXIPGGKULHDBEXYCLQQFXA' - b'DJEEVJW9DFWDJQTS9AJWQQZLOYOSQW9DEHHENQVVOKFSBNHSJQHTPGIBLNTCXATNOH' - b'TIOQFDIQEZFWWHCUEGNAYAPMUGSNCZQIWGKCYAWZWGVLEZVSZIJKD9EHINYFUVZLPR' - b'NIVUNZBVUGMQAUKEDPQWZNKWAJZDVHIFLSQWBGVOXIVBZTYRDBFNXUFFNSSJHJLYJA' - b'9MYYLKRFEWMQNYJWYTDNIVNJFCEHBDE99SZTKKGZUQO9AMVGYCIUOCLECKEUBSWBNB' - b'9QYSWEUAVROPHIFQPAQUIIPGGTGYSWDFPYYCRFBHBHAUDUX9OKZPIZEOFEIMVHVAKR' - b'QJ9HXVC99RNOEOAZVPPIJCWGKCAJOOWRFXSWLAPBNIQGNNKXSBLMNFJCUBYDMPKS9V' - b'ASYWPE9WPMZLKSMOFQBPULYKIVBTHTUUNGGRPOOMSSSNUCP9ASWMGONSIANVRPEYLF' - b'OPJNSBCXKQSVPECWUWEHIOSVRAKAUTDCPGGMPQXDJFQAAFJQTIDUUFGDMGVZDJCNOS' - b'YOEAFJSKXUPHNGDJH9TSKJZGPUETDIFCUQUVARHETQFSRLSBCJCLBR9BJNDTNTWH9P' - b'HU99TPAMMYYWJSISCKTBGMROVKWVZTOOIUTOERQKHGPTOZAWKKRGRYLZTJPPUURSMF' - b'H9H9EMWIWKJYMWBLNQPFHBUGFKDJHEHORXXDDSFXAAQBSKIORGQNHZYRSUSRNLIBID' - b'AVDPZKOLLPOWTCPPUJFTOLMGSPASTNIXBHIJJSBRMREVBCZIPRVBQVPQ9AYYPOVK9L' - b'BOQQCGIXAYOXYIC9SYFMGAGZJOIHUZCATPJJOHVAZXLPUVLDJKBXQNPRTWHKH9ZHGC' - b'ILKVHYOXNVVWQFMXYDKEAQMXZLNZTFKWRTBTCZLTRCFVQSYG9ZSSZTQJQSHMBOMIQS' - b'KNWRG9MEUHWPGAJPJBDKPQOCXDYAIYMUKBOCMTVVZXLPIYQ9XZOJWMICKLECKCRJUX' - b'LEYRZCIQNFOIHKJPHTKFTS9YQMBXLCWZBZOWPEWMHLDEHBADWNCSGUYYYNGDGPMJGK' - b'MUQ9QCPVPJIOQAZOXMIP9POREGZIDEXJOWXHYTOILXMQBXRGBBNAOIJLRANGZGZSOM' - b'XQUEEUWXS9HSNOTUATDNMNIOLERHGBLZN9REGCRXIMGZWTHIGGL9VFGZZFURDCLVCT' - b'VKMUTYC9C' - ) - def test_get_digest_trits(self): """ Generating digest trits from a valid PrivateKey. diff --git a/test/types_test.py b/test/types_test.py index ea846cb..31c41cb 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -204,6 +204,23 @@ def test_slice(self): self.assertEqual(ts[-4:], TryteString(b'KBFA')) self.assertEqual(ts[4:-4:4], TryteString(b'9CEY')) + def test_iter_chunks(self): + """ + Iterating over a TryteString in constant-size chunks. + """ + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + + self.assertListEqual( + list(trytes.iter_chunks(9)), + + [ + TryteString(b'RBTC9D9DC'), + TryteString(b'DQAEASBYB'), + # The final chunk is padded as necessary. + TryteString(b'CCKBFA999'), + ], + ) + def test_init_from_tryte_string(self): """ Initializing a TryteString from another TryteString. From 771b029495fdfe57e78e3e82885f180a315e5af0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 15:51:46 -0500 Subject: [PATCH 182/239] Refactored magic number. --- iota/crypto/types.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/iota/crypto/types.py b/iota/crypto/types.py index f028806..6418ba1 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -98,8 +98,10 @@ def get_digest_trits(self): through a PBKDF, yielding a constant-length hash that can be used for crypto. """ - block_size = self.BLOCK_LEN * TRITS_PER_TRYTE - raw_trits = self.as_trits() + block_size = self.BLOCK_LEN * TRITS_PER_TRYTE + hashes_per_block = block_size // HASH_LENGTH + + raw_trits = self.as_trits() # Initialize list with the correct length to improve performance. digest = [0] * HASH_LENGTH # type: List[int] @@ -116,7 +118,7 @@ def get_digest_trits(self): buffer = [] # type: List[int] - for j in range(27): + for j in range(hashes_per_block): hash_start = j * HASH_LENGTH hash_end = hash_start + HASH_LENGTH From d4c24bbd943b91aa5471f88de3531ccc945539b0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 16:56:48 -0500 Subject: [PATCH 183/239] `prepareTransfers` returns array of trytes. --- iota/api.py | 2 +- iota/commands/extended/prepare_transfers.py | 2 +- iota/transaction.py | 400 ++++++++++---------- 3 files changed, 207 insertions(+), 197 deletions(-) diff --git a/iota/api.py b/iota/api.py index 6ca61b8..2861d4f 100644 --- a/iota/api.py +++ b/iota/api.py @@ -418,7 +418,7 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): :return: Array containing the trytes of the new bundle. - This value can be provided to :py:meth:`broadcast_and_store`. + This value can be provided to e.g., :py:meth:`attach_to_tangle`. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfers.py index a92f5ea..44d0c13 100644 --- a/iota/commands/extended/prepare_transfers.py +++ b/iota/commands/extended/prepare_transfers.py @@ -110,7 +110,7 @@ def _execute(self, request): if confirmed_inputs: bundle.sign_inputs(KeyGenerator(seed)) - return bundle + return bundle.as_tryte_strings() class PrepareTransfersRequestFilter(RequestFilter): diff --git a/iota/transaction.py b/iota/transaction.py index 39a752b..7b2c792 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -42,7 +42,164 @@ class TransactionHash(Hash): pass -class ProposedTransaction(object): +class Transaction(object): + """ + A transaction that has been attached to the Tangle. + """ + @classmethod + def from_tryte_string(cls, trytes): + # type: (TrytesCompatible) -> Transaction + """ + Creates a Transaction object from a sequence of trytes. + """ + tryte_string = TryteString(trytes) + + hash_ = [0] * HASH_LENGTH # type: MutableSequence[int] + + sponge = Curl() + sponge.absorb(tryte_string.as_trits()) + sponge.squeeze(hash_) + + return cls( + hash_ = TransactionHash.from_trits(hash_), + signature_message_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]), + timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), + current_index = int_from_trits(tryte_string[2331:2340].as_trits()), + last_index = int_from_trits(tryte_string[2340:2349].as_trits()), + bundle_id = BundleHash(tryte_string[2349:2430]), + trunk_transaction_id = TransactionHash(tryte_string[2430:2511]), + branch_transaction_id = TransactionHash(tryte_string[2511:2592]), + nonce = Hash(tryte_string[2592:2673]), + ) + + def __init__( + self, + hash_, + signature_message_fragment, + address, + value, + tag, + timestamp, + current_index, + last_index, + bundle_id, + trunk_transaction_id, + branch_transaction_id, + nonce, + ): + # type: (Optional[TransactionHash], Optional[TryteString], Address, int, Tag, int, Optional[int], Optional[int], Optional[BundleHash], Optional[TransactionHash], Optional[TransactionHash], Optional[Hash]) -> None + self.hash = hash_ + """ + Transaction ID, generated by taking a hash of the transaction + trits. + """ + + self.bundle_id = bundle_id + """ + Bundle ID, generated by taking a hash of all the transactions in + the bundle. + """ + + self.address = address + """ + The address associated with this transaction. + If ``value`` is != 0, the associated address' balance is adjusted + as a result of this transaction. + """ + + self.value = value + """ + Amount to adjust the balance of ``address``. + Can be negative (i.e., for spending inputs). + """ + + self.tag = tag + """ + Optional classification tag applied to this transaction. + """ + + self.nonce = nonce + """ + Unique value used to increase security of the transaction hash. + """ + + self.timestamp = timestamp + """ + Timestamp used to increase the security of the transaction hash. + + IMPORTANT: This value is easy to forge! + Do not rely on it when resolving conflicts! + """ + + self.current_index = current_index + """ + The position of the transaction inside the bundle. + + For value transfers, the "spend" transaction is generally in the + 0th position, followed by inputs, and the "change" transaction is + last. + """ + + self.last_index = last_index + """ + The position of the final transaction inside the bundle. + """ + + self.branch_transaction_id = branch_transaction_id + self.trunk_transaction_id = trunk_transaction_id + + self.signature_message_fragment =\ + TryteString(signature_message_fragment or b'') + """ + Cryptographic signature used to verify the transaction. + + Signatures are usually too long to fit into a single transaction, + so they are split out into multiple transactions in the same bundle + (hence it's called a fragment). + """ + + self.is_confirmed = None # type: Optional[bool] + """ + Whether this transaction has been confirmed by neighbor nodes. + Must be set manually via the ``getInclusionStates`` API command. + + References: + - :py:meth:`iota.api.StrictIota.get_inclusion_states` + - :py:meth:`iota.api.Iota.get_transfers` + """ + + @property + def is_tail(self): + # type: () -> bool + """ + Returns whether this transaction is a tail. + """ + return self.current_index == 0 + + def as_tryte_string(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction. + """ + return ( + self.signature_message_fragment + + self.address + + TryteString.from_trits(trits_from_int(self.value, pad=81)) + + self.tag + + TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) + + TryteString.from_trits(trits_from_int(self.current_index, pad=27)) + + TryteString.from_trits(trits_from_int(self.last_index, pad=27)) + + self.bundle_id + + self.trunk_transaction_id + + self.branch_transaction_id + + self.nonce + ) + + +class ProposedTransaction(Transaction): """ A transaction that has not yet been attached to the Tangle. @@ -61,27 +218,29 @@ class ProposedTransaction(object): def __init__(self, address, value, tag=None, message=None, timestamp=None): # type: (Address, int, Optional[Tag], Optional[TrytesCompatible], Optional[int]) -> None - super(ProposedTransaction, self).__init__() - - # See :py:class:`Transaction` for descriptions of these attributes. - self.address = address - self.message = TryteString(message or b'', pad=2187) - self.tag = Tag(tag or b'') - self.value = value - - # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, - # but for compatibility with Python 2, we have to do this the - # old-fashioned way. - # :see: http://stackoverflow.com/q/2775864/ - self.timestamp = timestamp or unix_timestamp(datetime.utcnow().timetuple()) - - # These attributes are set by :py:meth:`ProposedBundle.finalize`. - self.current_index = None # type: Optional[int] - self.last_index = None # type: Optional[int] - self.trunk_transaction_hash = None # type: Optional[TransactionHash] - self.branch_transaction_hash = None # type: Optional[TransactionHash] - self.signature_message_fragment = None # type: Optional[TryteString] - self.nonce = None # type: Optional[Hash] + if not timestamp: + # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, + # but for compatibility with Python 2, we have to do this the + # old-fashioned way. + # :see: http://stackoverflow.com/q/2775864/ + timestamp = unix_timestamp(datetime.utcnow().timetuple()) + + super(ProposedTransaction, self).__init__( + hash_ = None, + signature_message_fragment = None, + address = address, + value = value, + tag = Tag(tag or b''), + timestamp = timestamp, + current_index = None, + last_index = None, + bundle_id = None, + trunk_transaction_id = None, + branch_transaction_id = None, + nonce = None, + ) + + self.message = TryteString(message or b'', pad=self.MESSAGE_LEN) @property def timestamp_trits(self): @@ -136,22 +295,6 @@ def __init__(self, transactions=None): for t in transactions: self.add_transaction(t) - @property - def balance(self): - # type: () -> int - """ - Returns the bundle balance. - In order for a bundle to be valid, its balance must be 0: - - - A positive balance means that there aren't enough inputs to - cover the spent amount. - Add more inputs using :py:meth:`add_inputs`. - - A negative balance means that there are unspent inputs. - Use :py:meth:`send_unspent_inputs_to` to send the unspent - inputs to a "change" address. - """ - return sum(t.value for t in self._transactions) - def __len__(self): # type: () -> int """ @@ -173,6 +316,30 @@ def __getitem__(self, index): """ return self._transactions[index] + @property + def balance(self): + # type: () -> int + """ + Returns the bundle balance. + In order for a bundle to be valid, its balance must be 0: + + - A positive balance means that there aren't enough inputs to + cover the spent amount. + Add more inputs using :py:meth:`add_inputs`. + - A negative balance means that there are unspent inputs. + Use :py:meth:`send_unspent_inputs_to` to send the unspent + inputs to a "change" address. + """ + return sum(t.value for t in self._transactions) + + def as_tryte_strings(self): + # type: () -> List[TryteString] + """ + Returns the bundle as a list of TryteStrings, suitable as inputs + for ``attachToTangle``. + """ + return [t.as_tryte_string() for t in self] + def add_transaction(self, transaction): # type: (ProposedTransaction) -> None """ @@ -394,160 +561,3 @@ def sign_inputs(self, key_generator): i += AddressGenerator.DIGEST_ITERATIONS - 1 i += 1 - - -class Transaction(object): - """ - A transaction that has been attached to the Tangle. - """ - @classmethod - def from_tryte_string(cls, trytes): - # type: (TrytesCompatible) -> Transaction - """ - Creates a Transaction object from a sequence of trytes. - """ - tryte_string = TryteString(trytes) - - hash_ = [0] * HASH_LENGTH # type: MutableSequence[int] - - sponge = Curl() - sponge.absorb(tryte_string.as_trits()) - sponge.squeeze(hash_) - - return cls( - hash_ = TransactionHash.from_trits(hash_), - signature_message_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]), - timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), - current_index = int_from_trits(tryte_string[2331:2340].as_trits()), - last_index = int_from_trits(tryte_string[2340:2349].as_trits()), - bundle_id = BundleHash(tryte_string[2349:2430]), - trunk_transaction_id = TransactionHash(tryte_string[2430:2511]), - branch_transaction_id = TransactionHash(tryte_string[2511:2592]), - nonce = Hash(tryte_string[2592:2673]), - ) - - def __init__( - self, - hash_, - signature_message_fragment, - address, - value, - tag, - timestamp, - current_index, - last_index, - bundle_id, - trunk_transaction_id, - branch_transaction_id, - nonce, - ): - # type: (Hash, TryteString, Address, int, Tag, int, int, int, Hash, TransactionHash, TransactionHash, Hash) -> None - self.hash = hash_ - """ - Transaction ID, generated by taking a hash of the transaction - trits. - """ - - self.bundle_id = bundle_id - """ - Bundle ID, generated by taking a hash of all the transactions in - the bundle. - """ - - self.address = address - """ - The address associated with this transaction. - If ``value`` is != 0, the associated address' balance is adjusted - as a result of this transaction. - """ - - self.value = value - """ - Amount to adjust the balance of ``address``. - Can be negative (i.e., for spending inputs). - """ - - self.tag = tag - """ - Optional classification tag applied to this transaction. - """ - - self.nonce = nonce - """ - Unique value used to increase security of the transaction hash. - """ - - self.timestamp = timestamp - """ - Timestamp used to increase the security of the transaction hash. - - IMPORTANT: This value is easy to forge! - Do not rely on it when resolving conflicts! - """ - - self.current_index = current_index - """ - The position of the transaction inside the bundle. - - For value transfers, the "spend" transaction is generally in the - 0th position, followed by inputs, and the "change" transaction is - last. - """ - - self.last_index = last_index - """ - The position of the final transaction inside the bundle. - """ - - self.branch_transaction_id = branch_transaction_id - self.trunk_transaction_id = trunk_transaction_id - - self.signature_message_fragment =\ - TryteString(signature_message_fragment or b'') - """ - Cryptographic signature used to verify the transaction. - - Signatures are usually too long to fit into a single transaction, - so they are split out into multiple transactions in the same bundle - (hence it's called a fragment). - """ - - self.is_confirmed = None # type: Optional[bool] - """ - Whether this transaction has been confirmed by neighbor nodes. - Must be set manually via the ``getInclusionStates`` API command. - - References: - - :py:meth:`iota.api.StrictIota.get_inclusion_states` - - :py:meth:`iota.api.Iota.get_transfers` - """ - - @property - def is_tail(self): - # type: () -> bool - """ - Returns whether this transaction is a tail. - """ - return self.current_index == 0 - - def as_tryte_string(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction. - """ - return ( - self.signature_message_fragment - + self.address - + TryteString.from_trits(trits_from_int(self.value, pad=81)) - + self.tag - + TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) - + TryteString.from_trits(trits_from_int(self.current_index, pad=27)) - + TryteString.from_trits(trits_from_int(self.last_index, pad=27)) - + self.bundle_id - + self.trunk_transaction_id - + self.branch_transaction_id - + self.nonce - ) From 9f47366ce5e5cdf0e92cb79865c6f19f350f9a87 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 17:02:37 -0500 Subject: [PATCH 184/239] Stubbed out ``sendTransfer``. --- iota/api.py | 14 +++++-- iota/commands/extended/send_transfer.py | 42 ++++++++++++++++++++ test/commands/extended/send_transfer_test.py | 31 +++++++++++++++ 3 files changed, 83 insertions(+), 4 deletions(-) create mode 100644 iota/commands/extended/send_transfer.py create mode 100644 test/commands/extended/send_transfer_test.py diff --git a/iota/api.py b/iota/api.py index 2861d4f..473b75c 100644 --- a/iota/api.py +++ b/iota/api.py @@ -5,8 +5,7 @@ from typing import Dict, Iterable, List, Optional, Text from iota import AdapterSpec, Address, Bundle, ProposedBundle, \ - ProposedTransaction, Tag, Transaction, TransactionHash, TryteString, \ - TrytesCompatible + ProposedTransaction, Tag, TransactionHash, TryteString, TrytesCompatible from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import CustomCommand, command_registry from iota.crypto.types import Seed @@ -554,7 +553,7 @@ def send_transfer( change_address = None, min_weight_magnitude = 18, ): - # type: (int, Iterable[Transaction], Optional[Iterable[TransactionHash]], Optional[Address], int) -> Bundle + # type: (int, Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], int) -> Bundle """ Prepares a set of transfers and creates the bundle, then attaches the bundle to the Tangle, and broadcasts and stores the @@ -587,7 +586,14 @@ def send_transfer( References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtransfer """ - raise NotImplementedError('Not implemented yet.') + return self.sendTransfer( + seed = self.seed, + depth = depth, + transfers = transfers, + inputs = inputs, + change_address = change_address, + min_weight_magnitude = min_weight_magnitude, + ) def send_trytes(self, trytes, depth, min_weight_magnitude=18): # type: (Iterable[TryteString], int, int) -> List[TryteString] diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py new file mode 100644 index 0000000..a28b545 --- /dev/null +++ b/iota/commands/extended/send_transfer.py @@ -0,0 +1,42 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter + +__all__ = [ + 'SendTransferCommand', +] + + +class SendTransferCommand(FilterCommand): + """ + Executes ``sendTransfer`` extended API command. + + See :py:meth:`iota.api.Iota.send_transfer` for more info. + """ + command = 'sendTransfer' + + def get_request_filter(self): + return SendTransferRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + +class SendTransferRequestFilter(RequestFilter): + def __init__(self): + super(SendTransferRequestFilter, self).__init__( + { + + }, + + allow_missing_keys = { + + }, + ) diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py new file mode 100644 index 0000000..417d7c7 --- /dev/null +++ b/test/commands/extended/send_transfer_test.py @@ -0,0 +1,31 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from filters.test import BaseFilterTestCase +from iota import Iota +from iota.commands.extended.send_transfer import SendTransferCommand +from test import MockAdapter + + +class SendTransferRequestFilterTestCase(BaseFilterTestCase): + filter_type = SendTransferCommand(MockAdapter()).get_request_filter + skip_value_check = True + + +class SendTransferCommandTestCase(TestCase): + def setUp(self): + super(SendTransferCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verifies that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).sendTransfer, + SendTransferCommand, + ) From 99a8424f5edbf1fe469f146546a3e4ada3d69174 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 17:59:37 -0500 Subject: [PATCH 185/239] Stubbed out unit tests for `sendTransfer`. --- iota/commands/extended/send_transfer.py | 22 ++- test/commands/extended/send_transfer_test.py | 155 +++++++++++++++++++ 2 files changed, 176 insertions(+), 1 deletion(-) diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index a28b545..42bac75 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -2,7 +2,11 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +import filters as f +from iota import Address, ProposedTransaction from iota.commands import FilterCommand, RequestFilter +from iota.crypto.types import Seed +from iota.filters import Trytes __all__ = [ 'SendTransferCommand', @@ -33,10 +37,26 @@ class SendTransferRequestFilter(RequestFilter): def __init__(self): super(SendTransferRequestFilter, self).__init__( { + # Required parameters. + 'seed': f.Required | Trytes(result_type=Seed), + + 'transfers': + f.Required | f.Array | f.FilterRepeater(f.Type(ProposedTransaction)), + + # Optional parameters. + 'change_address': Trytes(result_type=Address), + 'depth': f.Type(int) | f.Min(1), + 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), + + 'inputs': + f.Array | f.FilterRepeater(Trytes(result_type=Address)), }, allow_missing_keys = { - + 'change_address', + 'depth', + 'inputs', + 'min_weight_magnitude', }, ) diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index 417d7c7..73e349a 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -14,6 +14,161 @@ class SendTransferRequestFilterTestCase(BaseFilterTestCase): filter_type = SendTransferCommand(MockAdapter()).get_request_filter skip_value_check = True + def test_pass_happy_path(self): + """ + Request is valid. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_optional_parameters_omitted(self): + """ + Request omits optional parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_empty(self): + """ + Request is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_seed_null(self): + """ + ``seed`` is null. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_seed_wrong_type(self): + """ + ``seed`` is not a TrytesCompatible value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_seed_not_trytes(self): + """ + ``seed`` contains invalid characters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transfers_wrong_type(self): + """ + ``transfers`` is not an array. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transfers_empty(self): + """ + ``transfers`` is an array, but it is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transfers_contents_invalid(self): + """ + ``transfers`` is a non-empty array, but it contains invalid values. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_change_address_wrong_type(self): + """ + ``change_address`` is not a TrytesCompatible value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_change_address_not_trytes(self): + """ + ``change_address`` contains invalid characters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_wrong_type(self): + """ + ``inputs`` is not an array. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_empty(self): + """ + ``inputs`` is an array, but it is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_contents_invalid(self): + """ + ``inputs`` is a non-empty array, but it contains invalid values. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_string(self): + """ + ``depth`` is a string. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_float(self): + """ + ``depth`` is a float. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_too_small(self): + """ + ``depth`` is < 1. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_min_weight_magnitude_string(self): + """ + ``min_weight_magnitude`` is a string. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_min_weight_magnitude_float(self): + """ + ``min_weight_magnitude`` is a float. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_min_weight_magnitude_too_small(self): + """ + ``min_weight_magnitude`` is < 18. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + class SendTransferCommandTestCase(TestCase): def setUp(self): From a15fab78ef0e3e7b13d5e0569902e79b3230e612 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 18:26:25 -0500 Subject: [PATCH 186/239] Implemented `sendTransfer` (note: needs tests). --- iota/commands/extended/send_transfer.py | 28 ++++++++-- iota/transaction.py | 72 ++++++++++++++++++------- 2 files changed, 79 insertions(+), 21 deletions(-) diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index 42bac75..1c7a227 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -2,9 +2,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import List, Optional + import filters as f -from iota import Address, ProposedTransaction +from iota import Address, Bundle, ProposedTransaction from iota.commands import FilterCommand, RequestFilter +from iota.commands.extended.prepare_transfers import PrepareTransfersCommand +from iota.commands.extended.send_trytes import SendTrytesCommand from iota.crypto.types import Seed from iota.filters import Trytes @@ -28,10 +32,28 @@ def get_response_filter(self): pass def _execute(self, request): - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), + change_address = request['change_address'] # type: Optional[Address] + depth = request['depth'] # type: int + inputs = request['inputs'] or [] # type: List[Address] + min_weight_magnitude = request['min_weight_magnitude'] # type: int + seed = request['seed'] # type: Seed + transfers = request['transfers'] # type: List[ProposedTransaction] + + prepared_trytes = PrepareTransfersCommand(self.adapter)( + change_address = change_address, + inputs = inputs, + seed = seed, + transfers = transfers, ) + sent_trytes = SendTrytesCommand(self.adapter)( + depth = depth, + min_weight_magnitude = min_weight_magnitude, + trytes = prepared_trytes, + ) + + return Bundle.from_tryte_strings(sent_trytes) + class SendTransferRequestFilter(RequestFilter): def __init__(self): diff --git a/iota/transaction.py b/iota/transaction.py index 7b2c792..7aa7c26 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -4,8 +4,8 @@ from calendar import timegm as unix_timestamp from datetime import datetime -from typing import Generator, Iterable, List, MutableSequence, \ - Optional, Tuple +from typing import Iterable, Iterator, List, MutableSequence, Optional, \ + Sequence, Tuple from iota import Address, Hash, Tag, TrytesCompatible, TryteString, \ int_from_trits, trits_from_int @@ -24,10 +24,6 @@ ] -# Custom types for type hints and docstrings. -Bundle = Iterable['Transaction'] - - class BundleHash(Hash): """ A TryteString that acts as a bundle hash. @@ -275,13 +271,49 @@ def last_index_trits(self): return trits_from_int(self.last_index, pad=27) -class ProposedBundle(object): +class Bundle(Sequence[Transaction]): """ - A collection of proposed transactions, to be treated as an atomic - unit when attached to the Tangle. + A collection of transactions, treated as an atomic unit on the + Tangle. Conceptually, a bundle is similar to a block in a blockchain. """ + @classmethod + def from_tryte_strings(cls, trytes): + # type: (Iterable[TryteString]) -> Bundle + """ + Creates a Bundle object from a list of tryte values. + """ + return cls(map(Transaction.from_tryte_string, trytes)) + + def __init__(self, transactions=None): + # type: (Optional[Iterable[Transaction]]) -> None + super(Bundle, self).__init__() + + self.transactions = transactions or [] # type: List[Transaction] + + def __contains__(self, transaction): + # type: (Transaction) -> bool + return transaction in self.transactions + + def __getitem__(self, index): + # type: (int) -> Transaction + return self.transactions[index] + + def __iter__(self): + # type: () -> Iterator[Transaction] + return iter(self.transactions) + + def __len__(self): + # type: () -> int + return len(self.transactions) + + +class ProposedBundle(Sequence[ProposedTransaction]): + """ + A collection of proposed transactions, to be treated as an atomic + unit when attached to the Tangle. + """ def __init__(self, transactions=None): # type: (Optional[Iterable[ProposedTransaction]]) -> None super(ProposedBundle, self).__init__() @@ -295,26 +327,30 @@ def __init__(self, transactions=None): for t in transactions: self.add_transaction(t) - def __len__(self): - # type: () -> int + def __contains__(self, transaction): + # type: (ProposedTransaction) -> bool + return transaction in self._transactions + + def __getitem__(self, index): + # type: (int) -> ProposedTransaction """ - Returns te number of transactions in the bundle. + Returns the transaction at the specified index. """ - return len(self._transactions) + return self._transactions[index] def __iter__(self): - # type: () -> Generator[ProposedTransaction] + # type: () -> Iterator[ProposedTransaction] """ Iterates over transactions in the bundle. """ return iter(self._transactions) - def __getitem__(self, index): - # type: (int) -> ProposedTransaction + def __len__(self): + # type: () -> int """ - Returns the transaction at the specified index. + Returns te number of transactions in the bundle. """ - return self._transactions[index] + return len(self._transactions) @property def balance(self): From 5e9f2410cf227d0429401a3dccdd9fd0825ce331 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 18:38:53 -0500 Subject: [PATCH 187/239] Stubbed out `getBundles`. --- iota/api.py | 16 +++-- iota/commands/extended/get_bundles.py | 39 +++++++++++ test/commands/extended/get_bundles_test.py | 74 ++++++++++++++++++++ test/commands/extended/send_transfer_test.py | 2 + 4 files changed, 127 insertions(+), 4 deletions(-) create mode 100644 iota/commands/extended/get_bundles.py create mode 100644 test/commands/extended/get_bundles_test.py diff --git a/iota/api.py b/iota/api.py index 473b75c..fc46eb6 100644 --- a/iota/api.py +++ b/iota/api.py @@ -472,10 +472,11 @@ def get_new_addresses(self, index=None, count=1): """ return self.getNewAddresses(seed=self.seed, index=index, count=count) - def get_bundle(self, transaction): - # type: (TransactionHash) -> Bundle + def get_bundles(self, transaction): + # type: (TransactionHash) -> List[Bundle] """ - Returns the bundle associated with the specified transaction hash. + Returns the bundle(s) associated with the specified transaction + hash. :param transaction: Transaction hash. Can be any type of transaction (tail or non- @@ -486,10 +487,17 @@ def get_bundle(self, transaction): If there are multiple bundles (e.g., because of a replay), all valid matching bundles will be returned. + Note that this method always returns a list, even if only one + bundle was found. + + :raise: + - :py:class:`iota.adapter.BadApiResponse` if any of the + bundles fails validation. + References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle """ - raise NotImplementedError('Not implemented yet.') + return self.getBundles(transaction=transaction) def get_transfers(self, start=0, end=None, inclusion_states=False): # type: (int, Optional[int], bool) -> List[Bundle] diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py new file mode 100644 index 0000000..914191d --- /dev/null +++ b/iota/commands/extended/get_bundles.py @@ -0,0 +1,39 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from iota import TransactionHash +from iota.commands import FilterCommand, RequestFilter +from iota.filters import Trytes + +__all__ = [ + 'GetBundlesCommand', +] + + +class GetBundlesCommand(FilterCommand): + """ + Executes ``getBundles`` extended API command. + + See :py:meth:`iota.api.Iota.get_bundles` for more info. + """ + command = 'getBundles' + + def get_request_filter(self): + return GetBundlesRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + +class GetBundlesRequestFilter(RequestFilter): + def __init__(self): + super(GetBundlesRequestFilter, self).__init__({ + 'transaction': f.Required | Trytes(result_type=TransactionHash), + }) diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py new file mode 100644 index 0000000..aa98cfa --- /dev/null +++ b/test/commands/extended/get_bundles_test.py @@ -0,0 +1,74 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from filters.test import BaseFilterTestCase +from iota import Iota +from iota.commands.extended.get_bundles import GetBundlesCommand +from test import MockAdapter + + +class GetBundlesRequestFilterTestCase(BaseFilterTestCase): + filter_type = GetBundlesCommand(MockAdapter()).get_request_filter + skip_value_check = True + + def test_pass_happy_path(self): + """ + Request is valid. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_empty(self): + """ + Request is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transaction_wrong_type(self): + """ + ``transaction`` is not a TrytesCompatible value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transaction_not_trytes(self): + """ + ``transaction`` contains invalid characters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + +class GetBundlesCommandTestCase(TestCase): + def setUp(self): + super(GetBundlesCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verifies that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).getBundles, + GetBundlesCommand, + ) diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index 73e349a..3be912a 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -184,3 +184,5 @@ def test_wireup(self): Iota(self.adapter).sendTransfer, SendTransferCommand, ) + + # :todo: Unit tests. From 9e6e4498082f036976f0f493a0f25585a7cf8ffa Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 23 Dec 2016 18:41:41 -0500 Subject: [PATCH 188/239] Alphabetized extended API commands. --- iota/api.py | 150 ++++++++++++++++++++++++++-------------------------- 1 file changed, 75 insertions(+), 75 deletions(-) diff --git a/iota/api.py b/iota/api.py index fc46eb6..6bf608c 100644 --- a/iota/api.py +++ b/iota/api.py @@ -331,6 +331,43 @@ def __init__(self, adapter, seed=None): self.seed = Seed(seed) if seed else Seed.random() + def broadcast_and_store(self, trytes): + # type: (Iterable[TryteString]) -> List[TryteString] + """ + Broadcasts and stores a set of transaction trytes. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore + """ + return self.broadcastAndStore(trytes=trytes) + + def get_bundles(self, transaction): + # type: (TransactionHash) -> List[Bundle] + """ + Returns the bundle(s) associated with the specified transaction + hash. + + :param transaction: + Transaction hash. Can be any type of transaction (tail or non- + tail). + + :return: + List of bundles associated with the transaction. + If there are multiple bundles (e.g., because of a replay), all + valid matching bundles will be returned. + + Note that this method always returns a list, even if only one + bundle was found. + + :raise: + - :py:class:`iota.adapter.BadApiResponse` if any of the + bundles fails validation. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle + """ + return self.getBundles(transaction=transaction) + def get_inputs(self, start=None, end=None, threshold=None): # type: (Optional[int], Optional[int], Optional[int]) -> dict """ @@ -391,44 +428,6 @@ def get_inputs(self, start=None, end=None, threshold=None): threshold = threshold, ) - def prepare_transfers(self, transfers, inputs=None, change_address=None): - # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address]) -> ProposedBundle - """ - Prepares transactions to be broadcast to the Tangle, by generating - the correct bundle, as well as choosing and signing the inputs (for - value transfers). - - :param transfers: Transaction objects to prepare. - - :param inputs: - List of addresses used to fund the transfer. - Not needed for zero-value transfers. - - If not provided, addresses will be selected automatically by - scanning the Tangle for unspent inputs. Note: this could take - awhile to complete. - - :param change_address: - If inputs are provided, any unspent amount will be sent to this - address. - - If not specified, a change address will be generated - automatically. - - :return: - Array containing the trytes of the new bundle. - This value can be provided to e.g., :py:meth:`attach_to_tangle`. - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers - """ - return self.prepareTransfers( - seed = self.seed, - transfers = transfers, - inputs = inputs, - change_address = change_address, - ) - def get_latest_inclusion(self, hashes): # type: (Iterable[TransactionHash]) -> Dict[TransactionHash, bool] """ @@ -472,33 +471,6 @@ def get_new_addresses(self, index=None, count=1): """ return self.getNewAddresses(seed=self.seed, index=index, count=count) - def get_bundles(self, transaction): - # type: (TransactionHash) -> List[Bundle] - """ - Returns the bundle(s) associated with the specified transaction - hash. - - :param transaction: - Transaction hash. Can be any type of transaction (tail or non- - tail). - - :return: - List of bundles associated with the transaction. - If there are multiple bundles (e.g., because of a replay), all - valid matching bundles will be returned. - - Note that this method always returns a list, even if only one - bundle was found. - - :raise: - - :py:class:`iota.adapter.BadApiResponse` if any of the - bundles fails validation. - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle - """ - return self.getBundles(transaction=transaction) - def get_transfers(self, start=0, end=None, inclusion_states=False): # type: (int, Optional[int], bool) -> List[Bundle] """ @@ -535,6 +507,44 @@ def get_transfers(self, start=0, end=None, inclusion_states=False): inclusion_states = inclusion_states, ) + def prepare_transfers(self, transfers, inputs=None, change_address=None): + # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address]) -> ProposedBundle + """ + Prepares transactions to be broadcast to the Tangle, by generating + the correct bundle, as well as choosing and signing the inputs (for + value transfers). + + :param transfers: Transaction objects to prepare. + + :param inputs: + List of addresses used to fund the transfer. + Not needed for zero-value transfers. + + If not provided, addresses will be selected automatically by + scanning the Tangle for unspent inputs. Note: this could take + awhile to complete. + + :param change_address: + If inputs are provided, any unspent amount will be sent to this + address. + + If not specified, a change address will be generated + automatically. + + :return: + Array containing the trytes of the new bundle. + This value can be provided to e.g., :py:meth:`attach_to_tangle`. + + References: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers + """ + return self.prepareTransfers( + seed = self.seed, + transfers = transfers, + inputs = inputs, + change_address = change_address, + ) + def replay_bundle(self, transaction): # type: (TransactionHash) -> Bundle """ @@ -630,13 +640,3 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=18): depth = depth, min_weight_magnitude = min_weight_magnitude, ) - - def broadcast_and_store(self, trytes): - # type: (Iterable[TryteString]) -> List[TryteString] - """ - Broadcasts and stores a set of transaction trytes. - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore - """ - return self.broadcastAndStore(trytes=trytes) From dd6163a4c12d54895bcc5bbedb96fe0c8493f2a5 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 25 Dec 2016 10:20:17 -0500 Subject: [PATCH 189/239] Partially-impl'd `sendTransfers` (note: needs tests & sig val). --- iota/commands/extended/get_bundles.py | 79 ++++++++++++- iota/commands/extended/get_transfers.py | 2 +- iota/transaction.py | 131 +++++++++++++++++---- test/commands/extended/get_bundles_test.py | 2 + test/transaction_test.py | 14 +-- 5 files changed, 195 insertions(+), 33 deletions(-) diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index 914191d..36405cd 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -2,9 +2,14 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import List, Optional + import filters as f -from iota import TransactionHash +from iota import BadApiResponse, Bundle, BundleHash, Transaction, \ + TransactionHash, TryteString from iota.commands import FilterCommand, RequestFilter +from iota.commands.core.get_trytes import GetTrytesCommand +from iota.exceptions import with_context from iota.filters import Trytes __all__ = [ @@ -27,11 +32,79 @@ def get_response_filter(self): pass def _execute(self, request): - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), + transaction_hash = request['transaction'] # type: TransactionHash + + bundle = Bundle(self._traverse_bundle(transaction_hash)) + + try: + bundle.validate() + except ValueError as e: + raise with_context( + exc = BadApiResponse( + 'Bundle failed validation: {error} ' + '(``exc.context`` has more info).'.format( + error = e, + ), + ), + + context = { + 'bundle': bundle, + }, + ) + + return bundle + + def _traverse_bundle(self, trunk_txn_hash, target_bundle_hash=None): + # type: (TransactionHash, Optional[BundleHash]) -> List[Transaction] + """ + Recursively traverse the Tangle, collecting transactions until we + hit a new bundle. + """ + trytes = GetTrytesCommand(self.adapter)(hashes=[trunk_txn_hash])['trytes'] # type: List[TryteString] + + if not trytes: + raise with_context( + exc = BadApiResponse( + 'Bundle transactions not visible (``exc.context`` has more info).', + ), + + context = { + 'trunk_transaction': trunk_txn_hash, + 'target_bundle': target_bundle_hash, + }, + ) + + transaction = Transaction.from_tryte_string(trytes[0]) + + if (not target_bundle_hash) and transaction.current_index: + raise with_context( + exc = BadApiResponse( + '``_traverse_bundle`` started with a non-tail transaction ' + '(``exc.context`` has more info).', + ), + + context = { + 'trunk_transaction': transaction, + 'target_bundle': target_bundle_hash, + }, + ) + + if target_bundle_hash: + if target_bundle_hash != transaction.bundle_hash: + return [] + else: + target_bundle_hash = transaction.bundle_hash + + if transaction.current_index == transaction.last_index == 0: + return [transaction] + + return [transaction] + self._traverse_bundle( + trunk_txn_hash = transaction.trunk_transaction_hash, + target_bundle_hash = target_bundle_hash ) + class GetBundlesRequestFilter(RequestFilter): def __init__(self): super(GetBundlesRequestFilter, self).__init__({ diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index 18adc63..a4e9b71 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -79,7 +79,7 @@ def _execute(self, request): # 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(t.bundle_id) + non_tails.add(t.bundle_hash) if non_tails: for t in self._find_transactions(bundles=non_tails): diff --git a/iota/transaction.py b/iota/transaction.py index 7aa7c26..533a79d 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -65,9 +65,9 @@ def from_tryte_string(cls, trytes): timestamp = int_from_trits(tryte_string[2322:2331].as_trits()), current_index = int_from_trits(tryte_string[2331:2340].as_trits()), last_index = int_from_trits(tryte_string[2340:2349].as_trits()), - bundle_id = BundleHash(tryte_string[2349:2430]), - trunk_transaction_id = TransactionHash(tryte_string[2430:2511]), - branch_transaction_id = TransactionHash(tryte_string[2511:2592]), + bundle_hash = BundleHash(tryte_string[2349:2430]), + trunk_transaction_hash = TransactionHash(tryte_string[2430:2511]), + branch_transaction_hash = TransactionHash(tryte_string[2511:2592]), nonce = Hash(tryte_string[2592:2673]), ) @@ -81,9 +81,9 @@ def __init__( timestamp, current_index, last_index, - bundle_id, - trunk_transaction_id, - branch_transaction_id, + bundle_hash, + trunk_transaction_hash, + branch_transaction_hash, nonce, ): # type: (Optional[TransactionHash], Optional[TryteString], Address, int, Tag, int, Optional[int], Optional[int], Optional[BundleHash], Optional[TransactionHash], Optional[TransactionHash], Optional[Hash]) -> None @@ -93,10 +93,10 @@ def __init__( trits. """ - self.bundle_id = bundle_id + self.bundle_hash = bundle_hash """ - Bundle ID, generated by taking a hash of all the transactions in - the bundle. + Bundle hash, generated by taking a hash of metadata from all the + transactions in the bundle. """ self.address = address @@ -144,8 +144,8 @@ def __init__( The position of the final transaction inside the bundle. """ - self.branch_transaction_id = branch_transaction_id - self.trunk_transaction_id = trunk_transaction_id + self.branch_transaction_hash = branch_transaction_hash + self.trunk_transaction_hash = trunk_transaction_hash self.signature_message_fragment =\ TryteString(signature_message_fragment or b'') @@ -175,6 +175,41 @@ def is_tail(self): """ return self.current_index == 0 + @property + def value_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's value. + """ + return TryteString.from_trits(trits_from_int(self.value, pad=81)) + + @property + def timestamp_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + timestamp. + """ + return TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) + + @property + def current_index_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + ``current_index`` value. + """ + return TryteString.from_trits(trits_from_int(self.current_index, pad=27)) + + @property + def last_index_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + ``last_index`` value. + """ + return TryteString.from_trits(trits_from_int(self.last_index, pad=27)) + def as_tryte_string(self): # type: () -> TryteString """ @@ -182,18 +217,31 @@ def as_tryte_string(self): """ return ( self.signature_message_fragment - + self.address - + TryteString.from_trits(trits_from_int(self.value, pad=81)) + + self.address.address + + self.value_as_trytes + self.tag - + TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) - + TryteString.from_trits(trits_from_int(self.current_index, pad=27)) - + TryteString.from_trits(trits_from_int(self.last_index, pad=27)) - + self.bundle_id - + self.trunk_transaction_id - + self.branch_transaction_id + + self.timestamp_as_trytes + + self.current_index_as_trytes + + self.last_index_as_trytes + + self.bundle_hash + + self.trunk_transaction_hash + + self.branch_transaction_hash + self.nonce ) + def get_signature_validation_trytes(self): + # type: () -> TryteString + """ + Returns the values needed to validate the transaction's + ``signature_message_fragment`` value. + """ + return ( + self.address.address + + self.value_as_trytes + + self.tag + + self.timestamp_as_trytes + ) + class ProposedTransaction(Transaction): """ @@ -230,9 +278,9 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): timestamp = timestamp, current_index = None, last_index = None, - bundle_id = None, - trunk_transaction_id = None, - branch_transaction_id = None, + bundle_hash = None, + trunk_transaction_hash = None, + branch_transaction_hash = None, nonce = None, ) @@ -308,6 +356,45 @@ def __len__(self): # type: () -> int return len(self.transactions) + def validate(self): + # type: () -> None + """ + Checks that the bundle is valid. If any issues are found, an + exception is raised. + + :raise: + - :py:class:`ValueError` if any issues are found. + """ + # balance = 0 + # sponge = Curl() + # + # # Use a counter for the loop so that we can skip ahead as we go. + # i = 0 + # while i < len(self): + # txn = self[i] + # + # if txn.current_index != i: + # raise ValueError( + # 'Transaction #{i} has invalid ``currentIndex`` ' + # '({txn.current_index}).'.format( + # i = i, + # txn = txn, + # ), + # ) + # + # balance += txn.value + # + # sponge.absorb(txn.get_signature_validation_trytes()) + # + # + # if balance: + # raise ValueError( + # 'Bundle has non-zero balance ({balance}).'.format( + # balance = balance, + # ), + # ) + pass + class ProposedBundle(Sequence[ProposedTransaction]): """ diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py index aa98cfa..3f66c92 100644 --- a/test/commands/extended/get_bundles_test.py +++ b/test/commands/extended/get_bundles_test.py @@ -72,3 +72,5 @@ def test_wireup(self): Iota(self.adapter).getBundles, GetBundlesCommand, ) + + # :todo: Unit tests. diff --git a/test/transaction_test.py b/test/transaction_test.py index 5b87910..5d7a9e8 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -274,7 +274,7 @@ def test_from_tryte_string(self): self.assertEqual(transaction.last_index, 1) self.assertEqual( - transaction.bundle_id, + transaction.bundle_hash, BundleHash( b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' @@ -283,7 +283,7 @@ def test_from_tryte_string(self): ) self.assertEqual( - transaction.trunk_transaction_id, + transaction.trunk_transaction_hash, TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' @@ -292,7 +292,7 @@ def test_from_tryte_string(self): ) self.assertEqual( - transaction.branch_transaction_id, + transaction.branch_transaction_hash, TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' @@ -332,7 +332,7 @@ def test_as_tryte_string(self): """ transaction = Transaction( hash_ = - Hash( + TransactionHash( b'QODOAEJHCFUYFTTPRONYSMMSFDNFWFX9UCMESVWA' b'FCVUQYOIJGJMBMGQSFIAFQFMVECYIFXHRGHHEOTMK' ), @@ -387,19 +387,19 @@ def test_as_tryte_string(self): current_index = 1, last_index = 1, - bundle_id = + bundle_hash= BundleHash( b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' ), - trunk_transaction_id = + trunk_transaction_hash= TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' ), - branch_transaction_id = + branch_transaction_hash= TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' From eb59f40f74eb9b99eb5302687372063e41d08659 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 25 Dec 2016 10:40:28 -0500 Subject: [PATCH 190/239] Stubbed out `replayBundle`, refactored magic number. --- iota/api.py | 29 +++++++++++--- iota/commands/__init__.py | 1 + iota/commands/core/attach_to_tangle.py | 10 +++-- iota/commands/extended/replay_bundle.py | 40 ++++++++++++++++++++ iota/commands/extended/send_transfer.py | 10 ++++- test/commands/core/attach_to_tangle_test.py | 17 +++++++-- test/commands/extended/replay_bundle_test.py | 35 +++++++++++++++++ test/transaction_test.py | 2 + 8 files changed, 129 insertions(+), 15 deletions(-) create mode 100644 iota/commands/extended/replay_bundle.py create mode 100644 test/commands/extended/replay_bundle_test.py diff --git a/iota/api.py b/iota/api.py index 6bf608c..f301a85 100644 --- a/iota/api.py +++ b/iota/api.py @@ -7,7 +7,8 @@ from iota import AdapterSpec, Address, Bundle, ProposedBundle, \ ProposedTransaction, Tag, TransactionHash, TryteString, TrytesCompatible from iota.adapter import BaseAdapter, resolve_adapter -from iota.commands import CustomCommand, command_registry +from iota.commands import CustomCommand, DEFAULT_MIN_WEIGHT_MAGNITUDE, \ + command_registry from iota.crypto.types import Seed __all__ = [ @@ -78,7 +79,7 @@ def attach_to_tangle( trunk_transaction, branch_transaction, trytes, - min_weight_magnitude = 18, + min_weight_magnitude = DEFAULT_MIN_WEIGHT_MAGNITUDE, ): # type: (TransactionHash, TransactionHash, Iterable[TryteString], int) -> dict """ @@ -545,8 +546,13 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): change_address = change_address, ) - def replay_bundle(self, transaction): - # type: (TransactionHash) -> Bundle + def replay_bundle( + self, + transaction, + depth, + min_weight_magnitude = DEFAULT_MIN_WEIGHT_MAGNITUDE, + ): + # type: (TransactionHash, int, int) -> Bundle """ Takes a tail transaction hash as input, gets the bundle associated with the transaction and then replays the bundle by attaching it to @@ -555,13 +561,24 @@ def replay_bundle(self, transaction): :param transaction: Transaction hash. Must be a tail. + :param depth: + Depth at which to attach the bundle. + + :param min_weight_magnitude: + Min weight magnitude, used by the node to calibrate Proof of + Work. + :return: The bundle containing the replayed transfer. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#replaytransfer """ - raise NotImplementedError('Not implemented yet.') + return self.replayBundle( + transaction = transaction, + depth = depth, + min_weight_magnitude = min_weight_magnitude, + ) def send_transfer( self, @@ -569,7 +586,7 @@ def send_transfer( transfers, inputs = None, change_address = None, - min_weight_magnitude = 18, + min_weight_magnitude = DEFAULT_MIN_WEIGHT_MAGNITUDE, ): # type: (int, Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], int) -> Bundle """ diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index 18386d2..cb8b2d8 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -21,6 +21,7 @@ 'command_registry', ] +DEFAULT_MIN_WEIGHT_MAGNITUDE = 18 command_registry = {} # type: Dict[Text, CommandMeta] """Registry of commands, indexed by command name.""" diff --git a/iota/commands/core/attach_to_tangle.py b/iota/commands/core/attach_to_tangle.py index 4d4c3e5..83cd993 100644 --- a/iota/commands/core/attach_to_tangle.py +++ b/iota/commands/core/attach_to_tangle.py @@ -3,9 +3,9 @@ unicode_literals import filters as f - from iota import TransactionHash -from iota.commands import FilterCommand, RequestFilter, ResponseFilter +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ + RequestFilter, ResponseFilter from iota.filters import Trytes __all__ = [ @@ -35,7 +35,11 @@ def __init__(self): 'trunk_transaction': f.Required | Trytes(result_type=TransactionHash), 'branch_transaction': f.Required | Trytes(result_type=TransactionHash), - 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), + 'min_weight_magnitude': ( + f.Type(int) + | f.Min(18) + | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE) + ), 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), }, diff --git a/iota/commands/extended/replay_bundle.py b/iota/commands/extended/replay_bundle.py new file mode 100644 index 0000000..b63b7c5 --- /dev/null +++ b/iota/commands/extended/replay_bundle.py @@ -0,0 +1,40 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.commands import FilterCommand, RequestFilter + +__all__ = [ + 'ReplayBundleCommand', +] + + +class ReplayBundleCommand(FilterCommand): + """ + Executes ``replayBundle`` extended API command. + + See :py:meth:`iota.api.Iota.replay_bundle` for more information. + """ + command = 'replayBundle' + + def get_request_filter(self): + return ReplayBundleRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + +class ReplayBundleRequestFilter(RequestFilter): + def __init__(self): + super(ReplayBundleRequestFilter, self).__init__( + { + }, + + allow_missing_keys = { + }, + ) diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index 1c7a227..50a1ac0 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -6,7 +6,8 @@ import filters as f from iota import Address, Bundle, ProposedTransaction -from iota.commands import FilterCommand, RequestFilter +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ + RequestFilter from iota.commands.extended.prepare_transfers import PrepareTransfersCommand from iota.commands.extended.send_trytes import SendTrytesCommand from iota.crypto.types import Seed @@ -68,7 +69,12 @@ def __init__(self): # Optional parameters. 'change_address': Trytes(result_type=Address), 'depth': f.Type(int) | f.Min(1), - 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), + + 'min_weight_magnitude': ( + f.Type(int) + | f.Min(18) + | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE) + ), 'inputs': f.Array | f.FilterRepeater(Trytes(result_type=Address)), diff --git a/test/commands/core/attach_to_tangle_test.py b/test/commands/core/attach_to_tangle_test.py index 1da65a2..c077bb5 100644 --- a/test/commands/core/attach_to_tangle_test.py +++ b/test/commands/core/attach_to_tangle_test.py @@ -7,6 +7,7 @@ import filters as f from filters.test import BaseFilterTestCase from iota import Iota, TransactionHash, TryteString +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.core.attach_to_tangle import AttachToTangleCommand from iota.filters import Trytes from six import binary_type, text_type @@ -60,16 +61,24 @@ def test_pass_min_weight_magnitude_missing(self): TryteString(self.trytes1) ], - # If not provided, this value is set to the minimum (18). + # If not provided, this value is set to the default (18). # 'min_weight_magnitude': 20, } filter_ = self._filter(request) + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'trunk_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), + 'trytes': [TryteString(self.trytes1)], - expected_value = request.copy() - expected_value['min_weight_magnitude'] = 18 - self.assertDictEqual(filter_.cleaned_data, expected_value) + 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, + }, + ) # noinspection SpellCheckingInspection def test_pass_compatible_types(self): diff --git a/test/commands/extended/replay_bundle_test.py b/test/commands/extended/replay_bundle_test.py new file mode 100644 index 0000000..86a0067 --- /dev/null +++ b/test/commands/extended/replay_bundle_test.py @@ -0,0 +1,35 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +from filters.test import BaseFilterTestCase +from iota import Iota +from iota.commands.extended.replay_bundle import ReplayBundleCommand +from test import MockAdapter + + +class ReplayBundleRequestFilterTestCase(BaseFilterTestCase): + filter_type = ReplayBundleCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # :todo: Unit tests. + + +class ReplayBundleCommandTestCase(TestCase): + def setUp(self): + super(ReplayBundleCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + + def test_wireup(self): + """ + Verifies that the command is wired-up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).replayBundle, + ReplayBundleCommand, + ) + + # :todo: Unit tests. diff --git a/test/transaction_test.py b/test/transaction_test.py index 5d7a9e8..46f0091 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -122,6 +122,8 @@ def test_sign_inputs_error_not_finalized(self): # :todo: Implement test. self.skipTest('Not implemented yet.') + # :todo: Validation tests. + # noinspection SpellCheckingInspection class TransactionHashTestCase(TestCase): From 9428a2bae2b0dfad65578bc7fcdffb437dc4322d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 25 Dec 2016 10:55:45 -0500 Subject: [PATCH 191/239] Stubbed out unit tests for `replayBundle` request val'n. --- iota/commands/extended/send_trytes.py | 2 +- .../core/get_transactions_to_approve_test.py | 35 ++++-- test/commands/extended/replay_bundle_test.py | 106 +++++++++++++++++- test/commands/extended/send_trytes_test.py | 18 ++- 4 files changed, 147 insertions(+), 14 deletions(-) diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index ac84029..b9e8e5a 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -51,7 +51,7 @@ class SendTrytesRequestFilter(RequestFilter): def __init__(self): super(SendTrytesRequestFilter, self).__init__( { - 'depth': f.Type(int) | f.Min(1), + 'depth': f.Required | f.Type(int) | f.Min(1), 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), diff --git a/test/commands/core/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py index 133418d..63d5823 100644 --- a/test/commands/core/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -18,7 +18,9 @@ class GetTransactionsToApproveRequestFilterTestCase(BaseFilterTestCase): skip_value_check = True def test_pass_happy_path(self): - """Typical `getTransactionsToApprove` request.""" + """ + Request is valid. + """ request = { 'depth': 100, } @@ -29,7 +31,9 @@ def test_pass_happy_path(self): self.assertDictEqual(filter_.cleaned_data, request) def test_fail_empty(self): - """Request is empty.""" + """ + Request is empty. + """ self.assertFilterErrors( {}, @@ -39,7 +43,9 @@ def test_fail_empty(self): ) def test_fail_unexpected_parameters(self): - """Request contains unexpected parameters.""" + """ + Request contains unexpected parameters. + """ self.assertFilterErrors( { 'depth': 100, @@ -53,8 +59,17 @@ def test_fail_unexpected_parameters(self): }, ) + def test_fail_depth_null(self): + """ + ``depth`` is null. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + def test_fail_depth_float(self): - """`depth` is a float.""" + """ + ``depth`` is a float. + """ self.assertFilterErrors( { 'depth': 100.0, @@ -66,7 +81,9 @@ def test_fail_depth_float(self): ) def test_fail_depth_string(self): - """`depth` is a string.""" + """ + ``depth`` is a string. + """ self.assertFilterErrors( { 'depth': '100', @@ -78,7 +95,9 @@ def test_fail_depth_string(self): ) def test_fail_depth_too_small(self): - """`depth` is less than 1.""" + """ + ``depth`` is less than 1. + """ self.assertFilterErrors( { 'depth': 0, @@ -97,7 +116,9 @@ class GetTransactionsToApproveResponseFilterTestCase(BaseFilterTestCase): # noinspection SpellCheckingInspection def test_pass_happy_path(self): - """Typical `getTransactionsToApprove` response.""" + """ + Typical ``getTransactionsToApprove`` response. + """ response = { 'trunkTransaction': 'TKGDZ9GEI9CPNQGHEATIISAKYPPPSXVCXBSR9EIW' diff --git a/test/commands/extended/replay_bundle_test.py b/test/commands/extended/replay_bundle_test.py index 86a0067..071d63a 100644 --- a/test/commands/extended/replay_bundle_test.py +++ b/test/commands/extended/replay_bundle_test.py @@ -14,7 +14,111 @@ class ReplayBundleRequestFilterTestCase(BaseFilterTestCase): filter_type = ReplayBundleCommand(MockAdapter()).get_request_filter skip_value_check = True - # :todo: Unit tests. + def test_pass_happy_path(self): + """ + Request is valid. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_optional_parameters_excluded(self): + """ + Request omits optional parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_empty(self): + """ + Request is empty. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transaction_null(self): + """ + ``transaction`` is null. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transaction_wrong_type(self): + """ + ``transaction`` is not a TrytesCompatible value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_transaction_not_trytes(self): + """ + ``transaction`` contains invalid characters. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_null(self): + """ + ``depth`` is null. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_string(self): + """ + ``depth`` is a string. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_float(self): + """ + ``depth`` is a float. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_depth_too_small(self): + """ + ``depth`` is < 1. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_min_weight_magnitude_string(self): + """ + ``min_weight_magnitude`` is a string. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_min_weight_magnitude_float(self): + """ + ``min_weight_magnitude`` is a float. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_min_weight_magnitude_too_small(self): + """ + ``min_weight_magnitude`` is < 18. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') class ReplayBundleCommandTestCase(TestCase): diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index 6c9522f..96d45be 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -4,12 +4,20 @@ from unittest import TestCase +from filters.test import BaseFilterTestCase from iota import BadApiResponse, Iota, TransactionHash, TryteString from iota.commands.extended.send_trytes import SendTrytesCommand from six import text_type from test import MockAdapter +class SendTrytesRequestFilterTestCase(BaseFilterTestCase): + filter_type = SendTrytesCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # :todo: Unit tests. + + class SendTrytesCommandTestCase(TestCase): # noinspection SpellCheckingInspection def setUp(self): @@ -44,7 +52,7 @@ def test_wireup(self): def test_happy_path(self): """ - Successful invocation of `sendTrytes`. + Successful invocation of ``sendTrytes``. """ self.adapter.seed_response('getTransactionsToApprove', { 'trunkTransaction': text_type(self.transaction1, 'ascii'), @@ -123,7 +131,7 @@ def test_happy_path(self): def test_get_transactions_to_approve_fails(self): """ - The `getTransactionsToApprove` call fails. + The ``getTransactionsToApprove`` call fails. """ self.adapter.seed_response('getTransactionsToApprove', { 'error': "I'm a teapot.", @@ -155,7 +163,7 @@ def test_get_transactions_to_approve_fails(self): def test_attach_to_tangle_fails(self): """ - The `attachToTangle` call fails. + The ``attachToTangle`` call fails. """ self.adapter.seed_response('getTransactionsToApprove', { 'trunkTransaction': text_type(self.transaction1, 'ascii'), @@ -205,7 +213,7 @@ def test_attach_to_tangle_fails(self): def test_broadcast_transactions_fails(self): """ - The `broadcastTransactions` call fails. + The ``broadcastTransactions`` call fails. """ self.adapter.seed_response('getTransactionsToApprove', { 'trunkTransaction': text_type(self.transaction1, 'ascii'), @@ -271,7 +279,7 @@ def test_broadcast_transactions_fails(self): def test_store_transactions_fails(self): """ - The `storeTransactions` call fails. + The ``storeTransactions`` call fails. """ self.adapter.seed_response('getTransactionsToApprove', { 'trunkTransaction': text_type(self.transaction1, 'ascii'), From 8efd319b618421f5c14bffd75d274cc577c6d3d2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 25 Dec 2016 11:01:47 -0500 Subject: [PATCH 192/239] Impl'd `replayBundle` (note: needs tests). --- iota/commands/extended/replay_bundle.py | 34 ++++++++++++++++++++++--- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/iota/commands/extended/replay_bundle.py b/iota/commands/extended/replay_bundle.py index b63b7c5..70e0125 100644 --- a/iota/commands/extended/replay_bundle.py +++ b/iota/commands/extended/replay_bundle.py @@ -2,7 +2,16 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from iota.commands import FilterCommand, RequestFilter +from typing import List + +import filters as f +from iota import Bundle +from iota import TransactionHash +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ + RequestFilter +from iota.commands.extended.get_bundles import GetBundlesCommand +from iota.commands.extended.send_trytes import SendTrytesCommand +from iota.filters import Trytes __all__ = [ 'ReplayBundleCommand', @@ -24,17 +33,36 @@ def get_response_filter(self): pass def _execute(self, request): - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), + depth = request['depth'] # type: int + min_weight_magnitude = request['min_weight_magnitude'] # type: int + transaction = request['transaction'] # type: TransactionHash + + bundles = GetBundlesCommand(self.adapter)(transaction=transaction) # type: List[Bundle] + + return SendTrytesCommand(self.adapter)( + depth = depth, + min_weight_magnitude = min_weight_magnitude, + + trytes = list(reversed(b.as_tryte_string() for b in bundles)), ) + class ReplayBundleRequestFilter(RequestFilter): def __init__(self): super(ReplayBundleRequestFilter, self).__init__( { + 'depth': f.Required | f.Type(int) | f.Min(1), + 'transaction': f.Required | Trytes(result_type=TransactionHash), + + 'min_weight_magnitude': ( + f.Type(int) + | f.Min(18) + | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE) + ), }, allow_missing_keys = { + 'min_weight_magnitude', }, ) From b9cbd328fd7299b332ee02af5e7710b934b71e02 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 25 Dec 2016 11:16:36 -0500 Subject: [PATCH 193/239] Finished impl'ing `getTransfers` (note: needs tests). --- iota/commands/extended/get_transfers.py | 40 ++++++++++++++++-------- iota/transaction.py | 41 +++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index a4e9b71..681bc50 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -5,10 +5,11 @@ from typing import List, Optional import filters as f -from iota import Transaction +from iota import Bundle, Transaction from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.commands.core.get_trytes import GetTrytesCommand +from iota.commands.extended.get_bundles import GetBundlesCommand from iota.commands.extended.get_latest_inclusion import \ GetLatestInclusionCommand from iota.crypto.addresses import AddressGenerator @@ -72,19 +73,19 @@ def _execute(self, request): transactions = self._find_transactions(hashes=hashes) - for t in transactions: - if t.is_tail: - tails.add(t.hash) + for txn in transactions: + if txn.is_tail: + tails.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(t.bundle_hash) + non_tails.add(txn.bundle_hash) if non_tails: - for t in self._find_transactions(bundles=non_tails): - if t.is_tail: - tails.add(t.hash) + for txn in self._find_transactions(bundles=non_tails): + if txn.is_tail: + tails.add(txn.hash) # Attach inclusion states, if requested. if inclusion_states: @@ -92,11 +93,26 @@ def _execute(self, request): hashes = tails, ) - for t in transactions: - t.is_confirmed = gli_response['states'].get(t.hash) + for txn in transactions: + txn.is_confirmed = gli_response['states'].get(txn.hash) - # :todo: Invoke getBundle. - # :todo: Sort bundles by timestamp and return. + all_bundles = [] # type: List[Bundle] + + # Find the bundles for each transaction. + for txn in transactions: + txn_bundles = GetBundlesCommand(self.adapter)(transactions=txn.hash) # type: List[Bundle] + + if inclusion_states: + for bundle in txn_bundles: + bundle.is_confirmed = txn.is_confirmed + + all_bundles.extend(txn_bundles) + + # Sort bundles by tail transaction timestamp. + return list(sorted( + all_bundles, + key = lambda bundle_: bundle_.tail_transaction.timestamp, + )) def _find_transactions(self, **kwargs): diff --git a/iota/transaction.py b/iota/transaction.py index 533a79d..03b42f2 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -340,6 +340,15 @@ def __init__(self, transactions=None): self.transactions = transactions or [] # type: List[Transaction] + self._is_confirmed = None # type: Optional[bool] + """ + Whether this bundle has been confirmed by neighbor nodes. + Must be set manually. + + References: + - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` + """ + def __contains__(self, transaction): # type: (Transaction) -> bool return transaction in self.transactions @@ -356,6 +365,38 @@ def __len__(self): # type: () -> int return len(self.transactions) + @property + def is_confirmed(self): + # type: () -> Optional[bool] + """ + Returns whether this bundle has been confirmed by neighbor nodes. + + This attribute must be set manually. + + References: + - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` + """ + return self._is_confirmed + + @is_confirmed.setter + def is_confirmed(self, new_is_confirmed): + # type: (bool) -> None + """ + Sets the ``is_confirmed`` for the bundle. + """ + self._is_confirmed = new_is_confirmed + + for txn in self: + txn.is_confirmed = new_is_confirmed + + @property + def tail_transaction(self): + # type: () -> Transaction + """ + Returns the tail transaction of the bundle. + """ + return self[0] + def validate(self): # type: () -> None """ From de8afdb0cd54921366e5ede386d6932e8c490ee4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 09:52:26 -0500 Subject: [PATCH 194/239] Made `depth` req'd for `getTransactionsToApprove`. --- iota/commands/core/get_transactions_to_approve.py | 2 +- .../commands/core/get_transactions_to_approve_test.py | 11 +++++++++-- 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/iota/commands/core/get_transactions_to_approve.py b/iota/commands/core/get_transactions_to_approve.py index 11b6531..66d4ffc 100644 --- a/iota/commands/core/get_transactions_to_approve.py +++ b/iota/commands/core/get_transactions_to_approve.py @@ -31,7 +31,7 @@ def get_response_filter(self): class GetTransactionsToApproveRequestFilter(RequestFilter): def __init__(self): super(GetTransactionsToApproveRequestFilter, self).__init__({ - 'depth': f.Type(int) | f.Min(1), + 'depth': f.Required | f.Type(int) | f.Min(1), }) diff --git a/test/commands/core/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py index 63d5823..9c0bdd9 100644 --- a/test/commands/core/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -63,8 +63,15 @@ def test_fail_depth_null(self): """ ``depth`` is null. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': None, + }, + + { + 'depth': [f.Required.CODE_EMPTY], + }, + ) def test_fail_depth_float(self): """ From 0e098bbc09dfbb89b81fd4f905b88ed0291c2cc3 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 10:06:16 -0500 Subject: [PATCH 195/239] Impl'd tests for `getBundles` request. --- test/commands/extended/get_bundles_test.py | 84 ++++++++++++++++++---- 1 file changed, 71 insertions(+), 13 deletions(-) diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py index 3f66c92..45ec6cc 100644 --- a/test/commands/extended/get_bundles_test.py +++ b/test/commands/extended/get_bundles_test.py @@ -4,9 +4,12 @@ from unittest import TestCase +import filters as f from filters.test import BaseFilterTestCase -from iota import Iota +from iota import Iota, TransactionHash from iota.commands.extended.get_bundles import GetBundlesCommand +from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter @@ -14,48 +17,103 @@ class GetBundlesRequestFilterTestCase(BaseFilterTestCase): filter_type = GetBundlesCommand(MockAdapter()).get_request_filter skip_value_check = True + def setUp(self): + super(GetBundlesRequestFilterTestCase, self).setUp() + + # noinspection SpellCheckingInspection + self.transaction = ( + b'ORLSCIMM9ZONOUSPYYWLOEMXQZLYEHCBEDQSHZOG' + b'OPZCZCDZYTDPGEEUXWUZ9FQYCT9OGS9PICOOX9999' + ) + def test_pass_happy_path(self): """ Request is valid. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'transaction': TransactionHash(self.transaction) + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ Request contains values that can be converted to the expected types. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + # Any TrytesCompatible value will work here. + 'transaction': binary_type(self.transaction), + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'transaction': TransactionHash(self.transaction), + }, + ) def test_fail_empty(self): """ Request is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + {}, + + { + 'transaction': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) def test_fail_unexpected_parameters(self): """ Request contains unexpected parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transaction': TransactionHash(self.transaction), + + # SAY "WHAT" AGAIN! + 'what': 'augh!', + }, + + { + 'what': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_transaction_wrong_type(self): """ ``transaction`` is not a TrytesCompatible value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transaction': text_type(self.transaction, 'ascii'), + }, + + { + 'transaction': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_transaction_not_trytes(self): """ ``transaction`` contains invalid characters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transaction': b'not valid; must contain only uppercase and "9"', + }, + + { + 'transaction': [Trytes.CODE_NOT_TRYTES], + }, + ) class GetBundlesCommandTestCase(TestCase): From 7b43dfb1fa0ae79ae62075dc5be46160d0d94a74 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 10:19:27 -0500 Subject: [PATCH 196/239] Impl'd tests for `getLatestInclusion` request. --- .../extended/get_latest_inclusion_test.py | 138 +++++++++++++++--- 1 file changed, 121 insertions(+), 17 deletions(-) diff --git a/test/commands/extended/get_latest_inclusion_test.py b/test/commands/extended/get_latest_inclusion_test.py index f246d5a..3b550c0 100644 --- a/test/commands/extended/get_latest_inclusion_test.py +++ b/test/commands/extended/get_latest_inclusion_test.py @@ -4,10 +4,13 @@ from unittest import TestCase +import filters as f from filters.test import BaseFilterTestCase -from iota import Iota +from iota import Iota, TransactionHash, TryteString from iota.commands.extended.get_latest_inclusion import \ GetLatestInclusionCommand +from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter @@ -15,62 +18,163 @@ class GetLatestInclusionRequestFilterTestCase(BaseFilterTestCase): filter_type = GetLatestInclusionCommand(MockAdapter()).get_request_filter skip_value_check = True + # noinspection SpellCheckingInspection + def setUp(self): + super(GetLatestInclusionRequestFilterTestCase, self).setUp() + + self.hash1 = ( + b'UBMJSEJDJLPDDJ99PISPI9VZSWBWBPZWVVFED9ED' + b'XSU9BHQHKMBMVURSZOSBIXJ9MBEOHVDPV9CWV9ECF' + ) + + self.hash2 = ( + b'WGXG9AGGIVSE9NUEEVVFNJARM9ZWDDATZKPBBXFJ' + b'HFPGFPTQPHBCVIEYQWENDK9NMREIIBIWLZHRWRIPU' + ) + def test_pass_happy_path(self): """ Request is valid. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'hashes': [TransactionHash(self.hash1), TransactionHash(self.hash2)], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ Request contains values that can be converted to the expected types. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'hashes': [ + # Any TrytesCompatible value can be used here. + binary_type(self.hash1), + bytearray(self.hash2), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'hashes': [ + TransactionHash(self.hash1), + TransactionHash(self.hash2), + ], + }, + ) def test_fail_empty(self): """ Request is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + {}, + + { + 'hashes': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) def test_fail_unexpected_parameters(self): """ Request contains unexpected parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': [TransactionHash(self.hash1)], + + # Uh, before we dock, I think we ought to discuss the bonus + # situation. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_hashes_null(self): """ ``hashes`` is null. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': None, + }, + + { + 'hashes': [f.Required.CODE_EMPTY], + }, + ) def test_fail_hashes_wrong_type(self): """ ``hashes`` is not an array. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # It's gotta be an array, even if there's only one hash. + 'hashes': TransactionHash(self.hash1), + }, + + { + 'hashes': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_hashes_empty(self): """ ``hashes`` is an array, but it is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': [], + }, + + { + 'hashes': [f.Required.CODE_EMPTY], + }, + ) def test_fail_hashes_contents_invalid(self): """ ``hashes`` is a non-empty array, but it contains invalid values. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'hashes': [ + b'', + text_type(self.hash1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.hash1), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'hashes.0': [f.Required.CODE_EMPTY], + 'hashes.1': [f.Type.CODE_WRONG_TYPE], + 'hashes.2': [f.Type.CODE_WRONG_TYPE], + 'hashes.3': [f.Required.CODE_EMPTY], + 'hashes.4': [Trytes.CODE_NOT_TRYTES], + 'hashes.6': [f.Type.CODE_WRONG_TYPE], + 'hashes.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) class GetLatestInclusionCommandTestCase(TestCase): From 1a786ce74802028154ba2f8fab4c7552850805cd Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 11:02:41 -0500 Subject: [PATCH 197/239] Impl'd req valid'n for `prepareTransfers`. --- iota/commands/extended/prepare_transfers.py | 36 +- .../extended/prepare_transfers_test.py | 338 ++++++++++++++++-- 2 files changed, 321 insertions(+), 53 deletions(-) diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfers.py index 44d0c13..ca77360 100644 --- a/iota/commands/extended/prepare_transfers.py +++ b/iota/commands/extended/prepare_transfers.py @@ -42,12 +42,23 @@ def _execute(self, request): # Optional parameters. change_address = request.get('change_address') # type: Optional[Address] - proposed_inputs = request.get('inputs') or [] # type: List[Address] + proposed_inputs = request.get('inputs') # type: Optional[List[Address]] want_to_spend = bundle.balance if want_to_spend > 0: # We are spending inputs, so we need to gather and sign them. - if proposed_inputs: + if proposed_inputs is None: + # No inputs provided. Scan addresses for unspent inputs. + gi_response = GetInputsCommand(self.adapter)( + seed = seed, + threshold = want_to_spend, + ) + + confirmed_inputs = [ + input_['address'] + for input_ in gi_response['inputs'] + ] + else: # Inputs provided. Check to make sure we have sufficient # balance. available_to_spend = 0 @@ -85,17 +96,6 @@ def _execute(self, request): 'want_to_spend': want_to_spend, }, ) - else: - # No inputs provided. Scan addresses for unspent inputs. - gi_response = GetInputsCommand(self.adapter)( - seed = seed, - threshold = want_to_spend, - ) - - confirmed_inputs = [ - input_['address'] - for input_ in gi_response['inputs'] - ] bundle.add_inputs(confirmed_inputs) @@ -120,14 +120,18 @@ def __init__(self): # Required parameters. 'seed': f.Required | Trytes(result_type=Seed), - 'transfers': - f.Required | f.Array | f.FilterRepeater(f.Type(ProposedTransaction)), + 'transfers': ( + f.Required + | f.Array + | f.FilterRepeater(f.Required | f.Type(ProposedTransaction)) + ), # Optional parameters. 'change_address': Trytes(result_type=Address), + # Note that ``inputs`` is allowed to be an empty array. 'inputs': - f.Array | f.FilterRepeater(Trytes(result_type=Address)), + f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Address)), }, allow_missing_keys = { diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index ffe372d..fc459e5 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -4,9 +4,13 @@ from unittest import TestCase +import filters as f from filters.test import BaseFilterTestCase -from iota import Iota +from iota import Address, Iota, ProposedTransaction, TryteString from iota.commands.extended.prepare_transfers import PrepareTransfersCommand +from iota.crypto.types import Seed +from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter @@ -14,118 +18,378 @@ class PrepareTransfersRequestFilterTestCase(BaseFilterTestCase): filter_type = PrepareTransfersCommand(MockAdapter()).get_request_filter skip_value_check = True + # noinspection SpellCheckingInspection + def setUp(self): + super(PrepareTransfersRequestFilterTestCase, self).setUp() + + # Define some tryte sequences that we can reuse between tests. + self.trytes1 = ( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999JBW' + b'GEC99GBXFFBCHAEJHLC9DX9EEPAI9ICVCKBX9FFII' + ) + + self.trytes2 = ( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999THZ' + b'BODYHZM99IR9KOXLZXVUOJM9LQKCQJBWMTY999999' + ) + + self.trytes3 = ( + b'TESTVALUETHREE9DONTUSEINPRODUCTIONG99999' + b'GTQ9CSNUFPYW9MBQ9LFQJSORCF9LGTY9BWQFY9999' + ) + + self.trytes4 = ( + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999ZQ' + b'HOGCBZCOTZVZRFBEHQKHENBIZWDTUQXTOVWEXRIK9' + ) + + self.transfer1 =\ + ProposedTransaction( + address = + Address( + b'TESTVALUEFIVE9DONTUSEINPRODUCTION99999MG' + b'AAAHJDZ9BBG9U9R9XEOHCBVCLCWCCCCBQCQGG9WHK' + ), + + value = 42, + ) + + self.transfer2 =\ + ProposedTransaction( + address = + Address( + b'TESTVALUESIX9DONTUSEINPRODUCTION99999GGT' + b'FODSHHELBDERDCDRBCINDCGQEI9NAWDJBC9TGPFME' + ), + + value = 86, + ) + def test_pass_happy_path(self): """ Request is valid. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'change_address': Address(self.trytes1), + 'seed': Seed(self.trytes2), + 'transfers': [self.transfer1, self.transfer2], + + 'inputs': [ + Address(self.trytes3), + Address(self.trytes4), + ], + + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ Request contains values that can be converted to the expected types. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + # Any TrytesCompatible value works here. + 'change_address': binary_type(self.trytes1), + 'seed': bytearray(self.trytes2), + + 'inputs': [ + binary_type(self.trytes3), + bytearray(self.trytes4), + ], + + # These still have to have the correct type, however. + 'transfers': [self.transfer1, self.transfer2], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'change_address': Address(self.trytes1), + 'seed': Seed(self.trytes2), + 'transfers': [self.transfer1, self.transfer2], + + 'inputs': [ + Address(self.trytes3), + Address(self.trytes4), + ], + }, + ) def test_pass_optional_parameters_omitted(self): """ Request omits optional parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + + # These parameters are set to their default values. + 'change_address': None, + 'inputs': None, + }, + ) def test_fail_empty(self): """ Request is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + {}, + + { + 'seed': [f.FilterMapper.CODE_MISSING_KEY], + 'transfers': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) def test_fail_unexpected_parameters(self): """ Request contains unexpected parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'seed': Seed(self.trytes1), + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + + # You guys give up? Or are you thirsty for more? + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_seed_null(self): """ ``seed`` is null. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'seed': None, + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + }, + + { + 'seed': [f.Required.CODE_EMPTY], + }, + ) def test_fail_seed_wrong_type(self): """ ``seed`` is not a TrytesCompatible value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'seed': text_type(self.trytes1, 'ascii'), + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + }, + + { + 'seed': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_seed_not_trytes(self): """ ``seed`` contains invalid characters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'seed': b'not valid; must contain only uppercase and "9"', + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + }, + + { + 'seed': [Trytes.CODE_NOT_TRYTES], + }, + ) def test_fail_transfers_wrong_type(self): """ ``transfers`` is not an array. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # It's gotta be an array, even if there's only one transaction. + 'transfers': + ProposedTransaction(address=Address(self.trytes2), value=42), + + 'seed': Seed(self.trytes1), + }, + + { + 'transfers': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_transfers_empty(self): """ ``transfers`` is an array, but it is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transfers': [], + + 'seed': Seed(self.trytes1), + }, + + { + 'transfers': [f.Required.CODE_EMPTY], + }, + ) def test_fail_transfers_contents_invalid(self): """ ``transfers`` is a non-empty array, but it contains invalid values. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transfers': [ + None, + + # This value is valid; just adding it to make sure the filter + # doesn't cheat! + ProposedTransaction(address=Address(self.trytes2), value=42), + + {'address': Address(self.trytes2), 'value': 42}, + ], + + 'seed': Seed(self.trytes1), + }, + + { + 'transfers.0': [f.Required.CODE_EMPTY], + 'transfers.2': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_change_address_wrong_type(self): """ ``change_address`` is not a TrytesCompatible value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'change_address': text_type(self.trytes3, 'ascii'), + + 'seed': Seed(self.trytes1), + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + }, + + { + 'change_address': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_change_address_not_trytes(self): """ ``change_address`` contains invalid characters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'change_address': b'not valid; must contain only uppercase and "9"', + + 'seed': Seed(self.trytes1), + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + }, + + { + 'change_address': [Trytes.CODE_NOT_TRYTES], + }, + ) def test_fail_inputs_wrong_type(self): """ ``inputs`` is not an array. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Must be an array, even if there's only one input. + 'inputs': Address(self.trytes3), - def test_fail_inputs_empty(self): - """ - ``inputs`` is an array, but it is empty. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + 'seed': Seed(self.trytes1), + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + }, + + { + 'inputs': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_inputs_contents_invalid(self): """ ``inputs`` is a non-empty array, but it contains invalid values. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'inputs': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes1), + + 2130706433, + b'9' * 82, + ], + + 'seed': Seed(self.trytes1), + + 'transfers': [ + ProposedTransaction(address=Address(self.trytes2), value=42), + ], + }, + + { + 'inputs.0': [f.Required.CODE_EMPTY], + 'inputs.1': [f.Type.CODE_WRONG_TYPE], + 'inputs.2': [f.Type.CODE_WRONG_TYPE], + 'inputs.3': [f.Required.CODE_EMPTY], + 'inputs.4': [Trytes.CODE_NOT_TRYTES], + 'inputs.6': [f.Type.CODE_WRONG_TYPE], + 'inputs.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) class PrepareTransfersCommandTestCase(TestCase): From c3480fa55d9b0fd91bcd235cb31ccb7717e6fd66 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 11:19:39 -0500 Subject: [PATCH 198/239] Impl'd req valid'n for `replayBundle`. --- test/commands/extended/replay_bundle_test.py | 227 ++++++++++++++++--- 1 file changed, 196 insertions(+), 31 deletions(-) diff --git a/test/commands/extended/replay_bundle_test.py b/test/commands/extended/replay_bundle_test.py index 071d63a..f6e3cee 100644 --- a/test/commands/extended/replay_bundle_test.py +++ b/test/commands/extended/replay_bundle_test.py @@ -4,9 +4,13 @@ from unittest import TestCase +import filters as f from filters.test import BaseFilterTestCase -from iota import Iota +from iota import Iota, TransactionHash +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.extended.replay_bundle import ReplayBundleCommand +from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter @@ -14,111 +18,272 @@ class ReplayBundleRequestFilterTestCase(BaseFilterTestCase): filter_type = ReplayBundleCommand(MockAdapter()).get_request_filter skip_value_check = True + # noinspection SpellCheckingInspection + def setUp(self): + super(ReplayBundleRequestFilterTestCase, self).setUp() + + self.trytes1 = ( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999DAU' + b'9WFSFWBSFT9QATCXFIIKDVFLHIIJGGFCDYENBEDCF' + ) + def test_pass_happy_path(self): """ Request is valid. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'depth': 100, + 'min_weight_magnitude': 18, + 'transaction': TransactionHash(self.trytes1), + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ Request contains values that can be converted to the expected types. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + # This can be any TrytesCompatible value. + 'transaction': binary_type(self.trytes1), + + # These values must still be ints, however. + 'depth': 100, + 'min_weight_magnitude': 18, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'depth': 100, + 'min_weight_magnitude': 18, + 'transaction': TransactionHash(self.trytes1), + }, + ) def test_pass_optional_parameters_excluded(self): """ Request omits optional parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'depth': 100, + 'transaction': TransactionHash(self.trytes1), + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'depth': 100, + 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, + 'transaction': TransactionHash(self.trytes1), + }, + ) def test_fail_empty(self): """ Request is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + {}, + + { + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + 'transaction': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) def test_fail_unexpected_parameters(self): """ Request contains unexpected parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': 100, + 'transaction': TransactionHash(self.trytes1), + + # That's a real nasty habit you got there. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_transaction_null(self): """ ``transaction`` is null. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transaction': None, + + 'depth': 100, + }, + + { + 'transaction': [f.Required.CODE_EMPTY], + }, + ) def test_fail_transaction_wrong_type(self): """ ``transaction`` is not a TrytesCompatible value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transaction': text_type(self.trytes1, 'ascii'), + + 'depth': 100, + }, + + { + 'transaction': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_transaction_not_trytes(self): """ ``transaction`` contains invalid characters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transaction': b'not valid; must contain only uppercase and "9"', + + 'depth': 100, + }, + + { + 'transaction': [Trytes.CODE_NOT_TRYTES], + }, + ) def test_fail_depth_null(self): """ ``depth`` is null. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': None, + + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'depth': [f.Required.CODE_EMPTY], + }, + ) def test_fail_depth_string(self): """ ``depth`` is a string. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Too ambiguous; it's gotta be an int. + 'depth': '4', + + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_depth_float(self): """ ``depth`` is a float. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Even with an empty fpart, float value is not valid. + 'depth': 8.0, + + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_depth_too_small(self): """ ``depth`` is < 1. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': 0, + + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'depth': [f.Min.CODE_TOO_SMALL], + }, + ) def test_fail_min_weight_magnitude_string(self): """ ``min_weight_magnitude`` is a string. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # It's gotta be an int! + 'min_weight_magnitude': '18', + + 'depth': 100, + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_min_weight_magnitude_float(self): """ ``min_weight_magnitude`` is a float. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Even with an empty fpart, float values are not valid. + 'min_weight_magnitude': 18.0, + + 'depth': 100, + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_min_weight_magnitude_too_small(self): """ ``min_weight_magnitude`` is < 18. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'min_weight_magnitude': 17, + + 'depth': 100, + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'min_weight_magnitude': [f.Min.CODE_TOO_SMALL], + }, + ) class ReplayBundleCommandTestCase(TestCase): From 7407839ee62ab7db633161a488a39a42b61ab557 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 11:40:25 -0500 Subject: [PATCH 199/239] Impl'd req valid'n for `sendTrytes`. --- iota/commands/extended/send_trytes.py | 26 +- test/commands/extended/send_trytes_test.py | 315 ++++++++++++++++++++- 2 files changed, 330 insertions(+), 11 deletions(-) diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index b9e8e5a..85a7ac0 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -2,8 +2,12 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import List + import filters as f -from iota.commands import FilterCommand, RequestFilter +from iota import TryteString +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ + RequestFilter from iota.commands.core.attach_to_tangle import AttachToTangleCommand from iota.commands.core.get_transactions_to_approve import \ GetTransactionsToApproveCommand @@ -30,18 +34,20 @@ def get_response_filter(self): pass def _execute(self, request): + depth = request['depth'] # type: int + min_weight_magnitude = request['min_weight_magnitude'] # type: int + trytes = request['trytes'] # type: List[TryteString] + # Call ``getTransactionsToApprove`` to locate trunk and branch # transactions so that we can attach the bundle to the Tangle. - gta_response = GetTransactionsToApproveCommand(self.adapter)( - depth = request['depth'], - ) + gta_response = GetTransactionsToApproveCommand(self.adapter)(depth=depth) AttachToTangleCommand(self.adapter)( branch_transaction = gta_response.get('branchTransaction'), trunk_transaction = gta_response.get('trunkTransaction'), - min_weight_magnitude = request['min_weight_magnitude'], - trytes = request['trytes'], + min_weight_magnitude = min_weight_magnitude, + trytes = trytes, ) return BroadcastAndStoreCommand(self.adapter)(trytes=request['trytes']) @@ -51,11 +57,13 @@ class SendTrytesRequestFilter(RequestFilter): def __init__(self): super(SendTrytesRequestFilter, self).__init__( { + # Required parameters. 'depth': f.Required | f.Type(int) | f.Min(1), - - 'min_weight_magnitude': f.Type(int) | f.Min(18) | f.Optional(18), - 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + + # Optional parameters. + 'min_weight_magnitude': + f.Type(int) | f.Min(18) | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE), }, allow_missing_keys = { diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index 96d45be..2d8e946 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -4,10 +4,13 @@ from unittest import TestCase +import filters as f from filters.test import BaseFilterTestCase from iota import BadApiResponse, Iota, TransactionHash, TryteString +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.extended.send_trytes import SendTrytesCommand -from six import text_type +from iota.filters import Trytes +from six import text_type, binary_type from test import MockAdapter @@ -15,7 +18,315 @@ class SendTrytesRequestFilterTestCase(BaseFilterTestCase): filter_type = SendTrytesCommand(MockAdapter()).get_request_filter skip_value_check = True - # :todo: Unit tests. + # noinspection SpellCheckingInspection + def setUp(self): + super(SendTrytesRequestFilterTestCase, self).setUp() + + # These values would normally be a lot longer (2187 trytes, to be + # exact), but for purposes of this test, we just need a non-empty + # value. + self.trytes1 = b'TRYTEVALUEHERE' + self.trytes2 = b'HELLOIOTA' + + def test_pass_happy_path(self): + """ + Request is valid. + """ + request = { + 'depth': 100, + 'min_weight_magnitude': 18, + + 'trytes': + [TryteString(self.trytes1), TryteString(self.trytes2)], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + # This can accept any TrytesCompatible values. + 'trytes': [ + binary_type(self.trytes1), + bytearray(self.trytes2), + ], + + # These still have to be ints, however. + 'depth': 100, + 'min_weight_magnitude': 18, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'depth': 100, + 'min_weight_magnitude': 18, + + 'trytes': + [TryteString(self.trytes1), TryteString(self.trytes2)], + }, + ) + + def test_pass_optional_parameters_omitted(self): + """ + Request omits optional parameters. + """ + filter_ = self._filter({ + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }) + + self.assertFilterPasses(filter_ ) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'depth': 100, + 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, + 'trytes': [TryteString(self.trytes1)], + }, + ) + + def test_fail_request_empty(self): + """ + Request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_request_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + + # Oh, bother. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_depth_null(self): + """ + ``depth`` is null. + """ + self.assertFilterErrors( + { + 'depth': None, + + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'depth': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_depth_string(self): + """ + ``depth`` is a string. + """ + self.assertFilterErrors( + { + # Too ambiguous; it's gotta be an int. + 'depth': '4', + + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_depth_float(self): + """ + ``depth`` is a float. + """ + self.assertFilterErrors( + { + # Even with an empty fpart, float value is not valid. + 'depth': 8.0, + + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_depth_too_small(self): + """ + ``depth`` is < 1. + """ + self.assertFilterErrors( + { + 'depth': 0, + + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'depth': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_min_weight_magnitude_string(self): + """ + ``min_weight_magnitude`` is a string. + """ + self.assertFilterErrors( + { + # It's gotta be an int! + 'min_weight_magnitude': '18', + + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_min_weight_magnitude_float(self): + """ + ``min_weight_magnitude`` is a float. + """ + self.assertFilterErrors( + { + # Even with an empty fpart, float values are not valid. + 'min_weight_magnitude': 18.0, + + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_min_weight_magnitude_too_small(self): + """ + ``min_weight_magnitude`` is < 18. + """ + self.assertFilterErrors( + { + 'min_weight_magnitude': 17, + + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'min_weight_magnitude': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_trytes_null(self): + """ + ``trytes`` is null. + """ + self.assertFilterErrors( + { + 'trytes': None, + + 'depth': 100, + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_trytes_wrong_type(self): + """ + ``trytes`` is not an array. + """ + self.assertFilterErrors( + { + # Must be an array, even if there's only one TryteString to + # send. + 'trytes': TryteString(self.trytes1), + + 'depth': 100, + }, + + { + 'trytes': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_trytes_empty(self): + """ + ``trytes`` is an array, but it is empty. + """ + self.assertFilterErrors( + { + 'trytes': [], + + 'depth': 100, + }, + + { + 'trytes': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_trytes_contents_invalid(self): + """ + ``trytes`` is a non-empty array, but it contains invalid values. + """ + self.assertFilterErrors( + { + 'trytes': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes1), + + 2130706433, + ], + + 'depth': 100, + }, + + { + 'trytes.0': [f.Required.CODE_EMPTY], + 'trytes.1': [f.Type.CODE_WRONG_TYPE], + 'trytes.2': [f.Type.CODE_WRONG_TYPE], + 'trytes.3': [f.Required.CODE_EMPTY], + 'trytes.4': [Trytes.CODE_NOT_TRYTES], + 'trytes.6': [f.Type.CODE_WRONG_TYPE], + }, + ) class SendTrytesCommandTestCase(TestCase): From 6bb8b3100fcc46768ec8b5c781698c001d9d43be Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 12:02:34 -0500 Subject: [PATCH 200/239] Impl'd req valid'n for `sendTransfer`. --- iota/commands/extended/send_transfer.py | 16 +- test/commands/extended/send_transfer_test.py | 450 +++++++++++++++++-- 2 files changed, 410 insertions(+), 56 deletions(-) diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index 50a1ac0..5b8abd7 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -61,14 +61,17 @@ def __init__(self): super(SendTransferRequestFilter, self).__init__( { # Required parameters. - 'seed': f.Required | Trytes(result_type=Seed), + 'depth': f.Required | f.Type(int) | f.Min(1), + 'seed': f.Required | Trytes(result_type=Seed), - 'transfers': - f.Required | f.Array | f.FilterRepeater(f.Type(ProposedTransaction)), + 'transfers': ( + f.Required + | f.Array + | f.FilterRepeater(f.Required | f.Type(ProposedTransaction)) + ), # Optional parameters. 'change_address': Trytes(result_type=Address), - 'depth': f.Type(int) | f.Min(1), 'min_weight_magnitude': ( f.Type(int) @@ -76,14 +79,13 @@ def __init__(self): | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE) ), + # Note that ``inputs`` is allowed to be an empty array. 'inputs': - f.Array | f.FilterRepeater(Trytes(result_type=Address)), - + f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Address)), }, allow_missing_keys = { 'change_address', - 'depth', 'inputs', 'min_weight_magnitude', }, diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index 3be912a..d81c521 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -4,9 +4,14 @@ from unittest import TestCase +import filters as f from filters.test import BaseFilterTestCase -from iota import Iota +from iota import Address, Iota, ProposedTransaction, TryteString +from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.extended.send_transfer import SendTransferCommand +from iota.crypto.types import Seed +from iota.filters import Trytes +from six import binary_type, text_type from test import MockAdapter @@ -14,160 +19,507 @@ class SendTransferRequestFilterTestCase(BaseFilterTestCase): filter_type = SendTransferCommand(MockAdapter()).get_request_filter skip_value_check = True + # noinspection SpellCheckingInspection + def setUp(self): + super(SendTransferRequestFilterTestCase, self).setUp() + + # Define some tryte sequences that we can reuse between tests. + self.trytes1 = ( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999JBW' + b'GEC99GBXFFBCHAEJHLC9DX9EEPAI9ICVCKBX9FFII' + ) + + self.trytes2 = ( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999THZ' + b'BODYHZM99IR9KOXLZXVUOJM9LQKCQJBWMTY999999' + ) + + self.trytes3 = ( + b'TESTVALUETHREE9DONTUSEINPRODUCTIONG99999' + b'GTQ9CSNUFPYW9MBQ9LFQJSORCF9LGTY9BWQFY9999' + ) + + self.trytes4 = ( + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999ZQ' + b'HOGCBZCOTZVZRFBEHQKHENBIZWDTUQXTOVWEXRIK9' + ) + + self.transfer1 =\ + ProposedTransaction( + address = + Address( + b'TESTVALUEFIVE9DONTUSEINPRODUCTION99999MG' + b'AAAHJDZ9BBG9U9R9XEOHCBVCLCWCCCCBQCQGG9WHK' + ), + + value = 42, + ) + + self.transfer2 =\ + ProposedTransaction( + address = + Address( + b'TESTVALUESIX9DONTUSEINPRODUCTION99999GGT' + b'FODSHHELBDERDCDRBCINDCGQEI9NAWDJBC9TGPFME' + ), + + value = 86, + ) + def test_pass_happy_path(self): """ Request is valid. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + request = { + 'change_address': Address(self.trytes1), + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes2), + + 'inputs': [ + Address(self.trytes3), + Address(self.trytes4), + ], + + 'transfers': [ + self.transfer1, + self.transfer2 + ], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) def test_pass_compatible_types(self): """ Request contains values that can be converted to the expected types. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + # Any TrytesCompatible values will work here. + 'change_address': binary_type(self.trytes1), + 'seed': bytearray(self.trytes2), + + 'inputs': [ + binary_type(self.trytes3), + bytearray(self.trytes4), + ], + + # These values must have the correct type, however. + 'transfers': [ + self.transfer1, + self.transfer2 + ], + + 'depth': 100, + 'min_weight_magnitude': 18, + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'change_address': Address(self.trytes1), + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes2), + + 'inputs': [ + Address(self.trytes3), + Address(self.trytes4), + ], + + 'transfers': [ + self.transfer1, + self.transfer2 + ], + } + ) def test_pass_optional_parameters_omitted(self): """ Request omits optional parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + filter_ = self._filter({ + 'depth': 100, + 'seed': Seed(self.trytes2), + + 'transfers': [ + self.transfer1, + self.transfer2 + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + 'change_address': None, + 'depth': 100, + 'inputs': None, + 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, + 'seed': Seed(self.trytes2), + + 'transfers': [ + self.transfer1, + self.transfer2 + ], + } + ) def test_fail_empty(self): """ Request is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + {}, + + { + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + 'seed': [f.FilterMapper.CODE_MISSING_KEY], + 'transfers': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) def test_fail_unexpected_parameters(self): """ Request contains unexpected parameters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': 100, + 'seed': Seed(self.trytes2), + + 'transfers': [ + self.transfer1, + self.transfer2 + ], + + # Maybe he's not that smart; maybe he's like a worker bee who + # only knows how to push buttons or something. + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) def test_fail_seed_null(self): """ ``seed`` is null. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'seed': None, + + 'depth': 100, + 'transfers': [self.transfer1], + }, + + { + 'seed': [f.Required.CODE_EMPTY], + }, + ) def test_fail_seed_wrong_type(self): """ ``seed`` is not a TrytesCompatible value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'seed': text_type(self.trytes1, 'ascii'), + + 'depth': 100, + 'transfers': [self.transfer1], + }, + + { + 'seed': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_seed_not_trytes(self): """ ``seed`` contains invalid characters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'seed': b'not valid; must contain only uppercase and "9"', + + 'depth': 100, + 'transfers': [self.transfer1], + }, + + { + 'seed': [Trytes.CODE_NOT_TRYTES], + }, + ) def test_fail_transfers_wrong_type(self): """ ``transfers`` is not an array. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transfers': self.transfer1, + + 'depth': 100, + 'seed': Seed(self.trytes1), + }, + + { + 'transfers': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_transfers_empty(self): """ ``transfers`` is an array, but it is empty. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transfers': [], + + 'depth': 100, + 'seed': Seed(self.trytes1), + }, + + { + 'transfers': [f.Required.CODE_EMPTY], + }, + ) def test_fail_transfers_contents_invalid(self): """ ``transfers`` is a non-empty array, but it contains invalid values. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'transfers': [ + None, + + # This value is valid; just adding it to make sure the filter + # doesn't cheat! + ProposedTransaction(address=Address(self.trytes2), value=42), + + {'address': Address(self.trytes2), 'value': 42}, + ], + + 'depth': 100, + 'seed': Seed(self.trytes1), + }, + + { + 'transfers.0': [f.Required.CODE_EMPTY], + 'transfers.2': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_change_address_wrong_type(self): """ ``change_address`` is not a TrytesCompatible value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'change_address': text_type(self.trytes3, 'ascii'), + + 'depth': 100, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'change_address': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_change_address_not_trytes(self): """ ``change_address`` contains invalid characters. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'change_address': b'not valid; must contain only uppercase and "9"', + + 'depth': 100, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'change_address': [Trytes.CODE_NOT_TRYTES], + }, + ) def test_fail_inputs_wrong_type(self): """ ``inputs`` is not an array. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Must be an array, even if there's only one input. + 'inputs': Address(self.trytes4), - def test_fail_inputs_empty(self): - """ - ``inputs`` is an array, but it is empty. - """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + 'depth': 100, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'inputs': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_inputs_contents_invalid(self): """ ``inputs`` is a non-empty array, but it contains invalid values. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'inputs': [ + b'', + text_type(self.trytes1, 'ascii'), + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.trytes4), + + 2130706433, + b'9' * 82, + ], + + 'depth': 100, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'inputs.0': [f.Required.CODE_EMPTY], + 'inputs.1': [f.Type.CODE_WRONG_TYPE], + 'inputs.2': [f.Type.CODE_WRONG_TYPE], + 'inputs.3': [f.Required.CODE_EMPTY], + 'inputs.4': [Trytes.CODE_NOT_TRYTES], + 'inputs.6': [f.Type.CODE_WRONG_TYPE], + 'inputs.7': [Trytes.CODE_WRONG_FORMAT], + }, + ) def test_fail_depth_string(self): """ ``depth`` is a string. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Too ambiguous; it must be an int. + 'depth': '2', + + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_depth_float(self): """ ``depth`` is a float. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Even with an empty fpart, floats are invalid. + 'depth': 100.0, + + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'depth': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_depth_too_small(self): """ ``depth`` is < 1. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'depth': 0, + + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'depth': [f.Min.CODE_TOO_SMALL], + }, + ) def test_fail_min_weight_magnitude_string(self): """ ``min_weight_magnitude`` is a string. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Nope; it's gotta be an int. + 'min_weight_magnitude': '18', + + 'depth': 100, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_min_weight_magnitude_float(self): """ ``min_weight_magnitude`` is a float. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + # Even with an empty fpart, floats are invalid. + 'min_weight_magnitude': 18.0, + + 'depth': 100, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'min_weight_magnitude': [f.Type.CODE_WRONG_TYPE], + }, + ) def test_fail_min_weight_magnitude_too_small(self): """ ``min_weight_magnitude`` is < 18. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.assertFilterErrors( + { + 'min_weight_magnitude': 17, + + 'depth': 100, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'min_weight_magnitude': [f.Min.CODE_TOO_SMALL], + }, + ) class SendTransferCommandTestCase(TestCase): From 373a188ee7641efbbc1f9b04b961c58e48c64ded Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 26 Dec 2016 12:21:00 -0500 Subject: [PATCH 201/239] Fixed incorrect type hint in hello world script. --- examples/hello_world.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/hello_world.py b/examples/hello_world.py index 7d3c103..c4f8daa 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -1,7 +1,7 @@ # coding=utf-8 """ Simple "Hello, world!" example that sends a `getNodeInfo` command to - your friendly neighborhood node. +your friendly neighborhood node. """ from __future__ import absolute_import, division, print_function, \ @@ -19,7 +19,7 @@ def main(uri): - # type: (Text, int) -> None + # type: (Text) -> None api = StrictIota(uri) try: From 2d7fadcb6ae55498c339a817052afd470009cc71 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 27 Dec 2016 12:28:59 -0500 Subject: [PATCH 202/239] Made repl script a bit more user-friendly. --- examples/shell.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/examples/shell.py b/examples/shell.py index 08f5bb6..819df7a 100644 --- a/examples/shell.py +++ b/examples/shell.py @@ -10,7 +10,9 @@ from six import text_type as text -from iota import Iota, __version__ +# Import all common IOTA symbols into module scope, so that the user +# doesn't have to import anything themselves. +from iota import * def main(uri): @@ -24,6 +26,7 @@ def main(uri): ) ) + try: # noinspection PyUnresolvedReferences import IPython @@ -35,6 +38,8 @@ def main(uri): if __name__ == '__main__': + from iota import __version__ + parser = ArgumentParser( description = __doc__, epilog = 'PyOTA v{version}'.format(version=__version__), From 6b737cfff5b04c0a563fc7f6a14b18d9418b9755 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 27 Dec 2016 12:35:39 -0500 Subject: [PATCH 203/239] Improved documentation for `_traverse_bundle`. --- iota/commands/extended/get_bundles.py | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index 36405cd..b1115b9 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -54,13 +54,16 @@ def _execute(self, request): return bundle - def _traverse_bundle(self, trunk_txn_hash, target_bundle_hash=None): + def _traverse_bundle(self, txn_hash, target_bundle_hash=None): # type: (TransactionHash, Optional[BundleHash]) -> List[Transaction] """ Recursively traverse the Tangle, collecting transactions until we hit a new bundle. + + This method is (usually) faster than ``findTransactions``, and it + ensures we don't collect transactions from replayed bundles. """ - trytes = GetTrytesCommand(self.adapter)(hashes=[trunk_txn_hash])['trytes'] # type: List[TryteString] + trytes = GetTrytesCommand(self.adapter)(hashes=[txn_hash])['trytes'] # type: List[TryteString] if not trytes: raise with_context( @@ -69,8 +72,8 @@ def _traverse_bundle(self, trunk_txn_hash, target_bundle_hash=None): ), context = { - 'trunk_transaction': trunk_txn_hash, - 'target_bundle': target_bundle_hash, + 'transaction_hash': txn_hash, + 'target_bundle_hash': target_bundle_hash, }, ) @@ -84,22 +87,26 @@ def _traverse_bundle(self, trunk_txn_hash, target_bundle_hash=None): ), context = { - 'trunk_transaction': transaction, - 'target_bundle': target_bundle_hash, + 'transaction_object': transaction, + 'target_bundle_hash': target_bundle_hash, }, ) if target_bundle_hash: if target_bundle_hash != transaction.bundle_hash: + # We've hit a different bundle; we can stop now. return [] else: target_bundle_hash = transaction.bundle_hash if transaction.current_index == transaction.last_index == 0: + # Bundle only has one transaction. return [transaction] + # Recursively follow the trunk transaction, to fetch the next + # transaction in the bundle. return [transaction] + self._traverse_bundle( - trunk_txn_hash = transaction.trunk_transaction_hash, + txn_hash = transaction.trunk_transaction_hash, target_bundle_hash = target_bundle_hash ) From 803b9f53e9b66b2c3bb5ccf05e0e75355deb8b95 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 27 Dec 2016 13:21:12 -0500 Subject: [PATCH 204/239] Planned out unit tests for `prepareTransfers`. --- iota/api.py | 2 +- .../extended/prepare_transfers_test.py | 42 ++++++++++++++++++- 2 files changed, 42 insertions(+), 2 deletions(-) diff --git a/iota/api.py b/iota/api.py index f301a85..d5db4f9 100644 --- a/iota/api.py +++ b/iota/api.py @@ -519,7 +519,7 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): :param inputs: List of addresses used to fund the transfer. - Not needed for zero-value transfers. + Ignored for zero-value transfers. If not provided, addresses will be selected automatically by scanning the Tangle for unspent inputs. Note: this could take diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index fc459e5..fa1ec8b 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -407,4 +407,44 @@ def test_wireup(self): PrepareTransfersCommand, ) - # :todo: Unit tests. + def test_pass_inputs_not_needed(self): + """ + Preparing a bundle that does not transfer any IOTAs. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_inputs_explicit(self): + """ + Preparing a bundle with specified inputs. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_explicit_insufficient(self): + """ + Specified inputs are not sufficient to cover spend amount. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_inputs_implicit(self): + """ + Preparing a bundle that finds inputs to use automatically. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_inputs_implicit_insufficient(self): + """ + Account's total balance is not enough to cover spend amount. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_change_address_auto_generated(self): + """ + Preparing a bundle with an auto-generated change address. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') From c803f1f663cceb8e857fa1dd449c411e2c072029 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 10:28:24 -0500 Subject: [PATCH 205/239] Fixed issues preparing zero-IOTA transfers. --- iota/api.py | 11 +- iota/commands/extended/prepare_transfers.py | 6 +- iota/transaction.py | 51 +++++-- .../extended/prepare_transfers_test.py | 135 +++++++++++++++++- 4 files changed, 185 insertions(+), 18 deletions(-) diff --git a/iota/api.py b/iota/api.py index d5db4f9..8235285 100644 --- a/iota/api.py +++ b/iota/api.py @@ -509,7 +509,7 @@ def get_transfers(self, start=0, end=None, inclusion_states=False): ) def prepare_transfers(self, transfers, inputs=None, change_address=None): - # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address]) -> ProposedBundle + # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address]) -> dict """ Prepares transactions to be broadcast to the Tangle, by generating the correct bundle, as well as choosing and signing the inputs (for @@ -533,8 +533,13 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): automatically. :return: - Array containing the trytes of the new bundle. - This value can be provided to e.g., :py:meth:`attach_to_tangle`. + Dict containing the following values:: + + { + 'trytes': List[TryteString] + Raw trytes for the transactions in the bundle, ready to + be provided to :py:meth:`send_trytes`. + } References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfers.py index ca77360..093296b 100644 --- a/iota/commands/extended/prepare_transfers.py +++ b/iota/commands/extended/prepare_transfers.py @@ -109,8 +109,12 @@ def _execute(self, request): if confirmed_inputs: bundle.sign_inputs(KeyGenerator(seed)) + else: + bundle.finalize() - return bundle.as_tryte_strings() + return { + 'trytes': bundle.as_tryte_strings(), + } class PrepareTransfersRequestFilter(RequestFilter): diff --git a/iota/transaction.py b/iota/transaction.py index 03b42f2..cc85ebf 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -147,8 +147,7 @@ def __init__( self.branch_transaction_hash = branch_transaction_hash self.trunk_transaction_hash = trunk_transaction_hash - self.signature_message_fragment =\ - TryteString(signature_message_fragment or b'') + self.signature_message_fragment = signature_message_fragment """ Cryptographic signature used to verify the transaction. @@ -270,18 +269,23 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): timestamp = unix_timestamp(datetime.utcnow().timetuple()) super(ProposedTransaction, self).__init__( - hash_ = None, - signature_message_fragment = None, address = address, - value = value, tag = Tag(tag or b''), timestamp = timestamp, + value = value, + + # These values will be populated when the bundle is finalized. + bundle_hash = None, current_index = None, + hash_ = None, last_index = None, - bundle_hash = None, - trunk_transaction_hash = None, - branch_transaction_hash = None, - nonce = None, + signature_message_fragment = TryteString(b'', pad=2187), + + # These values start out empty; they will be populated when the + # node does PoW. + branch_transaction_hash = TransactionHash(b''), + nonce = Hash(b''), + trunk_transaction_hash = TransactionHash(b''), ) self.message = TryteString(message or b'', pad=self.MESSAGE_LEN) @@ -318,6 +322,28 @@ def last_index_trits(self): """ return trits_from_int(self.last_index, pad=27) + def as_tryte_string(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction. + """ + if not self.bundle_hash: + raise with_context( + exc = RuntimeError( + 'Cannot get TryteString representation of {cls} instance ' + 'without a bundle hash; call ``bundle.finalize()`` first ' + '(``exc.context`` has more info).'.format( + cls = type(self).__name__, + ), + ), + + context = { + 'transaction': self, + }, + ) + + return super(ProposedTransaction, self).as_tryte_string() + class Bundle(Sequence[Transaction]): """ @@ -500,9 +526,12 @@ def as_tryte_strings(self): # type: () -> List[TryteString] """ Returns the bundle as a list of TryteStrings, suitable as inputs - for ``attachToTangle``. + for :py:meth:`iota.api.Iota.send_trytes`. """ - return [t.as_tryte_string() for t in self] + # Return the transaction trytes in reverse order, so that the tail + # transaction is last. This will allow the node to link the + # transactions properly when it performs PoW. + return [t.as_tryte_string() for t in reversed(self)] def add_transaction(self, transaction): # type: (ProposedTransaction) -> None diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index fa1ec8b..6798b52 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -6,7 +6,7 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, Iota, ProposedTransaction, TryteString +from iota import Address, Iota, ProposedTransaction, Tag, TryteString from iota.commands.extended.prepare_transfers import PrepareTransfersCommand from iota.crypto.types import Seed from iota.filters import Trytes @@ -392,11 +392,13 @@ def test_fail_inputs_contents_invalid(self): ) +# noinspection SpellCheckingInspection class PrepareTransfersCommandTestCase(TestCase): def setUp(self): super(PrepareTransfersCommandTestCase, self).setUp() self.adapter = MockAdapter() + self.command = PrepareTransfersCommand(self.adapter) def test_wireup(self): """ @@ -411,8 +413,135 @@ def test_pass_inputs_not_needed(self): """ Preparing a bundle that does not transfer any IOTAs. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + response = self.command( + seed = Seed.random(), + + transfers = [ + ProposedTransaction( + value = 0, + address = Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999KJUPKN' + b'RMTHKVJYWNBKBGCKOQWBTKBOBJIZZYQITTFJZKLOI' + ), + tag = Tag(b'PYOTA9UNIT9TESTS9'), + + # Normally, it's not necessary to specify the timestamp for a + # transaction, but for unit tests, it's kind of important (: + timestamp = 1482938294, + ), + + ProposedTransaction( + value = 0, + address = Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999YMSWGX' + b'VNDMLXPT9HMVAOWUUZMLSJZFWGKDVGXPSQAWAEBJN' + ), + + timestamp = 1482938294, + ), + ], + ) + + self.assertDictEqual( + response, + + { + # Transactions that don't require signatures aren't too + # interesting. Things will get more exciting in subsequent + # tests. + 'trytes': [ + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVA' + b'OWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99A99999999A999999999EBBXLEONGGJMRUPZAO' + b'HRAIOIEXDSZGQCXRWQMZNDUEQYYKDOSPHOI9KXZTCSBEUBW9WBHILISLYOZWIG99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUE9DONTUSEINPRODUCTION99999KJUPKNRMTHKVJYWNBKB' + b'GCKOQWBTKBOBJIZZYQITTFJZKLOI999999999999999999999999999PYOTA9UNI' + b'T9TESTS99999999999NYBKIVD99999999999A999999999EBBXLEONGGJMRUPZAO' + b'HRAIOIEXDSZGQCXRWQMZNDUEQYYKDOSPHOI9KXZTCSBEUBW9WBHILISLYOZWIG99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) def test_pass_inputs_explicit(self): """ From 81596dcc53eb1b485be587ff02278b5f7b85adaf Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 11:47:12 -0500 Subject: [PATCH 206/239] Implemented `GeneratedAddress` filter. --- iota/filters.py | 45 ++++++++++++++++++++++++++++++++++++++++---- test/filters_test.py | 45 ++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 84 insertions(+), 6 deletions(-) diff --git a/iota/filters.py b/iota/filters.py index 6ac2933..9b07383 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -7,12 +7,37 @@ import filters as f from six import binary_type, text_type -from iota import TryteString +from iota import Address, TryteString, TrytesCompatible from iota.adapter import resolve_adapter, InvalidUri +class GeneratedAddress(f.BaseFilter): + """ + Validates an incoming value as a generated :py:class:`Address` (must + have ``key_index`` set). + """ + CODE_NO_KEY_INDEX = 'no_key_index' + + templates = { + CODE_NO_KEY_INDEX: 'Address must have ``key_index`` attribute set.', + } + + def _apply(self, value): + value = self._filter(value, f.Type(Address)) # type: Address + + if self._has_errors: + return None + + if value.key_index is None: + return self._invalid_value(value, self.CODE_NO_KEY_INDEX) + + return value + + class NodeUri(f.BaseFilter): - """Validates a string as a node URI.""" + """ + Validates a string as a node URI. + """ CODE_NOT_NODE_URI = 'not_node_uri' templates = { @@ -34,7 +59,9 @@ def _apply(self, value): class Trytes(f.BaseFilter): - """Validates a sequence as a sequence of trytes.""" + """ + Validates a sequence as a sequence of trytes. + """ CODE_NOT_TRYTES = 'not_trytes' CODE_WRONG_FORMAT = 'wrong_format' @@ -69,11 +96,19 @@ def __init__(self, result_type=TryteString): self.result_type = result_type def _apply(self, value): - value = self._filter(value, f.Type((binary_type, bytearray, TryteString))) # type: Union[binary_type, bytearray, TryteString] + # noinspection PyTypeChecker + value = self._filter(value, f.Type((binary_type, bytearray, TryteString))) # type: TrytesCompatible if self._has_errors: return None + # If the incoming value already has the correct type, then we're + # done. + if isinstance(value, self.result_type): + return value + + # First convert to a generic TryteString, to make sure that the + # sequence doesn't contain any invalid characters. try: value = TryteString(value) except ValueError: @@ -82,6 +117,8 @@ def _apply(self, value): if self.result_type is TryteString: return value + # Now coerce to the expected type and verify that there are no + # type-specific errors. try: return self.result_type(value) except ValueError: diff --git a/test/filters_test.py b/test/filters_test.py index 5b2ddad..e96cb14 100644 --- a/test/filters_test.py +++ b/test/filters_test.py @@ -5,8 +5,49 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import TryteString, TransactionHash -from iota.filters import NodeUri, Trytes +from iota import Address, TryteString, TransactionHash +from iota.filters import GeneratedAddress, NodeUri, Trytes + + +class GeneratedAddressTestCase(BaseFilterTestCase): + filter_type = GeneratedAddress + + def test_pass_none(self): + """ + ``None`` always passes this filter. + + Use ``Required | GeneratedAddress`` to reject null values. + """ + self.assertFilterPasses(None) + + def test_pass_key_index_set(self): + """ + Incoming value has correct type, and ``key_index`` is set. + """ + self.assertFilterPasses(Address(b'', key_index=42)) + + def test_fail_key_index_null(self): + """ + Incoming value does not have ``key_index`` set. + """ + self.assertFilterErrors( + Address(b''), + [GeneratedAddress.CODE_NO_KEY_INDEX], + ) + + def test_fail_wrong_type(self): + """ + Incoming value is not an :py:class:`Address` instance. + """ + # noinspection SpellCheckingInspection + self.assertFilterErrors( + # The only way to ensure ``key_index`` is set is to require that + # the incoming value is an :py:class:`Address` instance. + b'TESTVALUE9DONTUSEINPRODUCTION99999WJ9PCA' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + + [f.Type.CODE_WRONG_TYPE], + ) class NodeUriTestCase(BaseFilterTestCase): From 627e18dd709afab90732c14e10acdf3d04cd04a2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 14:26:51 -0500 Subject: [PATCH 207/239] Fixed bugs when adding inputs to transactions. --- iota/commands/extended/prepare_transfers.py | 6 +- iota/crypto/signing.py | 14 + iota/json.py | 28 +- iota/transaction.py | 196 +++--- iota/types.py | 34 +- .../extended/prepare_transfers_test.py | 570 +++++++++++++++++- 6 files changed, 731 insertions(+), 117 deletions(-) diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfers.py index 093296b..a47707e 100644 --- a/iota/commands/extended/prepare_transfers.py +++ b/iota/commands/extended/prepare_transfers.py @@ -14,7 +14,7 @@ from iota.crypto.signing import KeyGenerator from iota.crypto.types import Seed from iota.exceptions import with_context -from iota.filters import Trytes +from iota.filters import GeneratedAddress, Trytes __all__ = [ 'PrepareTransfersCommand', @@ -99,7 +99,7 @@ def _execute(self, request): bundle.add_inputs(confirmed_inputs) - if bundle.balance: + if bundle.balance < 0: if not change_address: change_address = GetNewAddressesCommand(self.adapter)(seed=seed)[0] @@ -135,7 +135,7 @@ def __init__(self): # Note that ``inputs`` is allowed to be an empty array. 'inputs': - f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Address)), + f.Array | f.FilterRepeater(f.Required | GeneratedAddress), }, allow_missing_keys = { diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 2d6f8f2..8c8d53f 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -220,6 +220,10 @@ class SignatureFragmentGenerator(object): Each instance can generate 1 signature per block (2187 trytes) in the private key. + + Note: This class behaves more like a coroutine than an iterator; you + must invoke the instance's :py:meth:`send` method to generate a new + value. """ def __init__(self, private_key): # type: (PrivateKey) -> None @@ -228,6 +232,16 @@ def __init__(self, private_key): self._key_chunks = private_key.iter_chunks(PrivateKey.BLOCK_LEN) self._sponge = Curl() + def __len__(self): + # type: () -> int + """ + Returns the number of fragments this generator can create. + + Note: This method always returns the same result, no matter how + many iterations have been completed. + """ + return len(self._key_chunks) + def send(self, source_trytes): # type: (TryteString) -> TryteString """ diff --git a/iota/json.py b/iota/json.py index a1f5f7d..bea4ec0 100644 --- a/iota/json.py +++ b/iota/json.py @@ -2,14 +2,36 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals - +from abc import ABCMeta, abstractmethod as abstract_method from json.encoder import JSONEncoder as BaseJsonEncoder +from six import with_metaclass + + +class JsonSerializable(with_metaclass(ABCMeta)): + """ + Interface for classes that can be safely converted to JSON. + """ + @abstract_method + def as_json_compatible(self): + """ + Returns a JSON-compatible representation of the object. + + References: + - :py:class:`iota.json.JsonEncoder`. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + # noinspection PyClassHasNoInit class JsonEncoder(BaseJsonEncoder): - """JSON encoder with support for custom types.""" + """ + JSON encoder with support for :py:class:`JsonSerializable`. + """ def default(self, o): - if hasattr(o, 'as_json_compatible'): + if isinstance(o, JsonSerializable): return o.as_json_compatible() + return super(JsonEncoder, self).default(o) diff --git a/iota/transaction.py b/iota/transaction.py index cc85ebf..15b7cfe 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -13,6 +13,7 @@ from iota.crypto.addresses import AddressGenerator from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator from iota.exceptions import with_context +from iota.json import JsonSerializable __all__ = [ 'Bundle', @@ -23,6 +24,21 @@ 'TransactionHash', ] +def get_current_timestamp(): + # type: () -> int + """ + Returns the current timestamp, used to set ``timestamp`` for new + :py:class:`ProposedTransaction` objects. + + Split out into a separate function so that it can be mocked during + unit tests. + """ + # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, but + # for compatibility with Python 2, we have to do it the old-fashioned + # way. + # :see: http://stackoverflow.com/q/2775864/ + return unix_timestamp(datetime.utcnow().timetuple()) + class BundleHash(Hash): """ @@ -38,7 +54,7 @@ class TransactionHash(Hash): pass -class Transaction(object): +class Transaction(JsonSerializable): """ A transaction that has been attached to the Tangle. """ @@ -180,6 +196,7 @@ def value_as_trytes(self): """ Returns a TryteString representation of the transaction's value. """ + # Note that we are padding to 81 _trits_. return TryteString.from_trits(trits_from_int(self.value, pad=81)) @property @@ -189,6 +206,7 @@ def timestamp_as_trytes(self): Returns a TryteString representation of the transaction's timestamp. """ + # Note that we are padding to 27 _trits_. return TryteString.from_trits(trits_from_int(self.timestamp, pad=27)) @property @@ -198,6 +216,7 @@ def current_index_as_trytes(self): Returns a TryteString representation of the transaction's ``current_index`` value. """ + # Note that we are padding to 27 _trits_. return TryteString.from_trits(trits_from_int(self.current_index, pad=27)) @property @@ -207,8 +226,32 @@ def last_index_as_trytes(self): Returns a TryteString representation of the transaction's ``last_index`` value. """ + # Note that we are padding to 27 _trits_. return TryteString.from_trits(trits_from_int(self.last_index, pad=27)) + def as_json_compatible(self): + # type: () -> dict + """ + Returns a JSON-compatible representation of the object. + + References: + - :py:class:`iota.json.JsonEncoder`. + """ + return { + 'hash': self.hash, + 'signature_message_fragment': self.signature_message_fragment, + 'address': self.address, + 'value': self.value, + 'tag': self.tag, + 'timestamp': self.timestamp, + 'current_index': self.current_index, + 'last_index': self.last_index, + 'bundle_hash': self.bundle_hash, + 'trunk_transaction_hash': self.trunk_transaction_hash, + 'branch_transaction_hash': self.branch_transaction_hash, + 'nonce': self.nonce, + } + def as_tryte_string(self): # type: () -> TryteString """ @@ -239,6 +282,8 @@ def get_signature_validation_trytes(self): + self.value_as_trytes + self.tag + self.timestamp_as_trytes + + self.current_index_as_trytes + + self.last_index_as_trytes ) @@ -262,11 +307,7 @@ class ProposedTransaction(Transaction): def __init__(self, address, value, tag=None, message=None, timestamp=None): # type: (Address, int, Optional[Tag], Optional[TrytesCompatible], Optional[int]) -> None if not timestamp: - # Python 3.3 introduced a :py:meth:`datetime.timestamp` method, - # but for compatibility with Python 2, we have to do this the - # old-fashioned way. - # :see: http://stackoverflow.com/q/2775864/ - timestamp = unix_timestamp(datetime.utcnow().timetuple()) + timestamp = get_current_timestamp() super(ProposedTransaction, self).__init__( address = address, @@ -279,7 +320,7 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): current_index = None, hash_ = None, last_index = None, - signature_message_fragment = TryteString(b'', pad=2187), + signature_message_fragment = TryteString(b'', pad=self.MESSAGE_LEN), # These values start out empty; they will be populated when the # node does PoW. @@ -290,38 +331,6 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): self.message = TryteString(message or b'', pad=self.MESSAGE_LEN) - @property - def timestamp_trits(self): - # type: () -> List[int] - """ - Returns the ``timestamp`` attribute expressed as trits. - """ - return trits_from_int(self.timestamp, pad=27) - - @property - def value_trits(self): - # type: () -> List[int] - """ - Returns the ``value`` attribute expressed as trits. - """ - return trits_from_int(self.value, pad=81) - - @property - def current_index_trits(self): - # type: () -> List[int] - """ - Returns the ``current_index`` attribute expressed as trits. - """ - return trits_from_int(self.current_index, pad=27) - - @property - def last_index_trits(self): - # type: () -> List[int] - """ - Returns the ``last_index`` attribute expressed as trits. - """ - return trits_from_int(self.last_index, pad=27) - def as_tryte_string(self): # type: () -> TryteString """ @@ -345,12 +354,19 @@ def as_tryte_string(self): return super(ProposedTransaction, self).as_tryte_string() -class Bundle(Sequence[Transaction]): +class Bundle(JsonSerializable, Sequence[Transaction]): """ - A collection of transactions, treated as an atomic unit on the - Tangle. + A collection of transactions, treated as an atomic unit when + attached to the Tangle. + + Note: unlike a block in a blockchain, bundles are not first-class + citizens in IOTA; only transactions get stored in the Tangle. + + Instead, Bundles must be inferred by following linked transactions + with the same bundle hash. - Conceptually, a bundle is similar to a block in a blockchain. + References: + - :py:class:`iota.commands.extended.get_bundles.GetBundlesCommand` """ @classmethod def from_tryte_strings(cls, trytes): @@ -423,6 +439,16 @@ def tail_transaction(self): """ return self[0] + def as_json_compatible(self): + # type: () -> List[dict] + """ + Returns a JSON-compatible representation of the object. + + References: + - :py:class:`iota.json.JsonEncoder`. + """ + return [txn.as_json_compatible() for txn in self] + def validate(self): # type: () -> None """ @@ -463,7 +489,7 @@ def validate(self): pass -class ProposedBundle(Sequence[ProposedTransaction]): +class ProposedBundle(JsonSerializable, Sequence[ProposedTransaction]): """ A collection of proposed transactions, to be treated as an atomic unit when attached to the Tangle. @@ -522,6 +548,16 @@ def balance(self): """ return sum(t.value for t in self._transactions) + def as_json_compatible(self): + # type: () -> List[dict] + """ + Returns a JSON-compatible representation of the object. + + References: + - :py:class:`iota.json.JsonEncoder`. + """ + return [txn.as_json_compatible() for txn in self] + def as_tryte_strings(self): # type: () -> List[TryteString] """ @@ -544,9 +580,6 @@ def add_transaction(self, transaction): if self.hash: raise RuntimeError('Bundle is already finalized.') - if transaction.value < 0: - raise ValueError('Use ``add_inputs`` to add inputs to the bundle.') - self._transactions.append(ProposedTransaction( address = transaction.address, value = transaction.value, @@ -622,17 +655,23 @@ def add_inputs(self, inputs): # Add the input as a transaction. self.add_transaction(ProposedTransaction( address = addy, - value = -addy.balance, tag = self.tag, + + # Spend the entire address balance; if necessary, we will add a + # change transaction to the bundle. + value = -addy.balance, )) - # Signatures require multiple transactions to store, due to + # Signatures require additional transactions to store, due to # transaction length limit. - for _ in range(AddressGenerator.DIGEST_ITERATIONS): + # Subtract 1 to account for the transaction we just added. + for _ in range(AddressGenerator.DIGEST_ITERATIONS - 1): self.add_transaction(ProposedTransaction( address = addy, - value = 0, tag = self.tag, + + # Note zero value; this is a meta transaction. + value = 0, )) def send_unspent_inputs_to(self, address): @@ -665,6 +704,7 @@ def finalize(self): if self.hash: raise RuntimeError('Bundle is already finalized.') + # Quick validation. balance = self.balance if balance > 0: raise ValueError( @@ -680,29 +720,23 @@ def finalize(self): ), ) + # Generate bundle hash. sponge = Curl() last_index = len(self) - 1 - for (i, t) in enumerate(self): # type: Tuple[int, ProposedTransaction] - t.current_index = i - t.last_index = last_index - - sponge.absorb( - # Ensure address checksum is not included in the result. - t.address.address.as_trits() - + t.value_trits - + t.tag.as_trits() - + t.timestamp_trits - + t.current_index_trits - + t.last_index_trits - ) + for (i, txn) in enumerate(self): # type: Tuple[int, ProposedTransaction] + txn.current_index = i + txn.last_index = last_index + + sponge.absorb(txn.get_signature_validation_trytes().as_trits()) bundle_hash = [0] * HASH_LENGTH # type: MutableSequence[int] sponge.squeeze(bundle_hash) self.hash = Hash.from_trits(bundle_hash) - for t in self: - t.bundle_hash = self.hash + # Copy bundle hash to individual transactions. + for txn in self: + txn.bundle_hash = self.hash def sign_inputs(self, key_generator): # type: (KeyGenerator) -> None @@ -734,12 +768,8 @@ def sign_inputs(self, key_generator): }, ) - signature_fragment_generator = SignatureFragmentGenerator( - key_generator.get_keys( - start = txn.address.key_index, - iterations = AddressGenerator.DIGEST_ITERATIONS - )[0], - ) + signature_fragment_generator =\ + self._create_signature_fragment_generator(key_generator, txn) hash_fragment_iterator = self.hash.iter_chunks(9) @@ -751,6 +781,24 @@ def sign_inputs(self, key_generator): self[i+j].signature_message_fragment =\ signature_fragment_generator.send(next(hash_fragment_iterator)) - i += AddressGenerator.DIGEST_ITERATIONS - 1 + i += AddressGenerator.DIGEST_ITERATIONS + else: + # No signature needed (nor even possible, in some cases); skip + # this transaction. + i += 1 + + @staticmethod + def _create_signature_fragment_generator(key_generator, txn): + # type: (KeyGenerator, ProposedTransaction) -> SignatureFragmentGenerator + """ + Creates the SignatureFragmentGenerator to sign inputs. - i += 1 + Split into a separate method so that it can be mocked for unit + tests. + """ + return SignatureFragmentGenerator( + key_generator.get_keys( + start = txn.address.key_index, + iterations = AddressGenerator.DIGEST_ITERATIONS + )[0], + ) diff --git a/iota/types.py b/iota/types.py index 2d9a613..db627ae 100644 --- a/iota/types.py +++ b/iota/types.py @@ -4,14 +4,16 @@ from codecs import encode, decode from itertools import chain +from math import ceil from typing import Generator, Iterable, Iterator, List, MutableSequence, \ Optional, Text, Union +from six import PY2, binary_type + from iota import TRITS_PER_TRYTE, TrytesCodec from iota.crypto import Curl, HASH_LENGTH from iota.exceptions import with_context -from six import PY2, binary_type - +from iota.json import JsonSerializable __all__ = [ 'Address', @@ -73,7 +75,7 @@ def int_from_trits(trits): return sum(base * (3 ** power) for power, base in enumerate(trits)) -class TryteString(object): +class TryteString(JsonSerializable): """ A string representation of a sequence of trytes. @@ -367,9 +369,10 @@ def as_bytes(self, errors='strict'): def as_json_compatible(self): # type: () -> Text """ - Converts the TryteString into a JSON-compatible value. + Returns a JSON-compatible representation of the object. - See :py:class:`iota.json.JsonEncoder`. + References: + - :py:class:`iota.json.JsonEncoder`. """ return self._trytes.decode('ascii') @@ -448,8 +451,19 @@ def __init__(self, trytes, chunk_size): self._offset = 0 def __iter__(self): + # type: () -> ChunkIterator return self + def __len__(self): + # type: () -> int + """ + Returns how many chunks this iterator will return. + + Note: This method always returns the same result, no matter how + many iterations have been completed. + """ + return int(ceil(len(self.trytes) / self.chunk_size)) + def __next__(self): # type: () -> TryteString """ @@ -505,8 +519,8 @@ class Address(TryteString): """ LEN = Hash.LEN - def __init__(self, trytes, key_index=None): - # type: (TrytesCompatible, Optional[int]) -> None + def __init__(self, trytes, balance=None, key_index=None): + # type: (TrytesCompatible, Optional[int], Optional[int]) -> None super(Address, self).__init__(trytes, pad=self.LEN) self.checksum = None @@ -531,7 +545,7 @@ def __init__(self, trytes, key_index=None): # Make the address sans checksum accessible. self.address = self[:self.LEN] # type: TryteString - self.balance = None # type: Optional[int] + self.balance = balance """ Balance owned by this address. Must be set manually via the ``getInputs`` command. @@ -544,6 +558,10 @@ def __init__(self, trytes, key_index=None): self.key_index = key_index """ Index of the key used to generate this address. + Must be set manually via ``AddressGenerator``. + + References: + - :py:class:`iota.crypto.addresses.AddressGenerator` """ def is_checksum_valid(self): diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index 6798b52..186f020 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -2,14 +2,18 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Iterable, List, Optional from unittest import TestCase import filters as f from filters.test import BaseFilterTestCase +from mock import patch + from iota import Address, Iota, ProposedTransaction, Tag, TryteString from iota.commands.extended.prepare_transfers import PrepareTransfersCommand +from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed -from iota.filters import Trytes +from iota.filters import GeneratedAddress, Trytes from six import binary_type, text_type from test import MockAdapter @@ -75,8 +79,8 @@ def test_pass_happy_path(self): 'transfers': [self.transfer1, self.transfer2], 'inputs': [ - Address(self.trytes3), - Address(self.trytes4), + Address(self.trytes3, key_index=3), + Address(self.trytes4, key_index=4), ], } @@ -96,9 +100,11 @@ def test_pass_compatible_types(self): 'change_address': binary_type(self.trytes1), 'seed': bytearray(self.trytes2), + # These have to be :py:class:`Address` instances, so that we can + # set ``key_index``. 'inputs': [ - binary_type(self.trytes3), - bytearray(self.trytes4), + Address(self.trytes3, key_index=3), + Address(self.trytes4, key_index=4), ], # These still have to have the correct type, however. @@ -359,18 +365,16 @@ def test_fail_inputs_contents_invalid(self): self.assertFilterErrors( { 'inputs': [ - b'', - text_type(self.trytes1, 'ascii'), - True, None, - b'not valid trytes', + binary_type(self.trytes1), # This is actually valid; I just added it to make sure the # filter isn't cheating! - TryteString(self.trytes1), + Address(self.trytes1, key_index=1), - 2130706433, - b'9' * 82, + # Inputs must have ``key_index`` set, so that we can generate + # the correct private key to sign them. + Address(b'', key_index=None), ], 'seed': Seed(self.trytes1), @@ -381,13 +385,9 @@ def test_fail_inputs_contents_invalid(self): }, { - 'inputs.0': [f.Required.CODE_EMPTY], - 'inputs.1': [f.Type.CODE_WRONG_TYPE], - 'inputs.2': [f.Type.CODE_WRONG_TYPE], - 'inputs.3': [f.Required.CODE_EMPTY], - 'inputs.4': [Trytes.CODE_NOT_TRYTES], - 'inputs.6': [f.Type.CODE_WRONG_TYPE], - 'inputs.7': [Trytes.CODE_WRONG_FORMAT], + 'inputs.0': [f.Required.CODE_EMPTY], + 'inputs.1': [f.Type.CODE_WRONG_TYPE], + 'inputs.3': [GeneratedAddress.CODE_NO_KEY_INDEX], }, ) @@ -400,6 +400,19 @@ def setUp(self): self.adapter = MockAdapter() self.command = PrepareTransfersCommand(self.adapter) + def run(self, result=None): + # Ensure that all tranactions use a predictable timestamp. + self.timestamp = 1482938294 + + def get_current_timestamp(): + return self.timestamp + + with patch( + target = 'iota.transaction.get_current_timestamp', + new = get_current_timestamp, + ): + return super(PrepareTransfersCommandTestCase, self).run(result) + def test_wireup(self): """ Verify that the command is wired up correctly. @@ -424,10 +437,6 @@ def test_pass_inputs_not_needed(self): b'RMTHKVJYWNBKBGCKOQWBTKBOBJIZZYQITTFJZKLOI' ), tag = Tag(b'PYOTA9UNIT9TESTS9'), - - # Normally, it's not necessary to specify the timestamp for a - # transaction, but for unit tests, it's kind of important (: - timestamp = 1482938294, ), ProposedTransaction( @@ -436,8 +445,6 @@ def test_pass_inputs_not_needed(self): b'TESTVALUE9DONTUSEINPRODUCTION99999YMSWGX' b'VNDMLXPT9HMVAOWUUZMLSJZFWGKDVGXPSQAWAEBJN' ), - - timestamp = 1482938294, ), ], ) @@ -543,9 +550,461 @@ def test_pass_inputs_not_needed(self): }, ) - def test_pass_inputs_explicit(self): + def test_pass_inputs_explicit_no_change(self): + """ + Preparing a bundle with specified inputs, no change address needed. + """ + self.adapter.seed_response('getBalances', { + 'balances': [13, 29], + 'duration': '1', + + 'milestone': + 'TESTVALUE9DONTUSEINPRODUCTION99999ZNIUXU' + 'FIVFBBYQHFYZYIEEWZL9VPMMKIIYTEZRRHXJXKIKF', + }) + + mock_signature_fragment_generator = MockSignatureFragmentGenerator([ + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWGHM' + b'NUXBJTOBKZFDNJEMAOPPLR9OOQJCDVO9XSCYQJQVTXQDYWQEBIXKDZAFWINAHJELJT' + b'DPVMUEWSVCJA9ONDYBNANWCGLBQMEMTBFDMWLCMQHGJLGYDQGIMLSNQHBGSVTDZSGN' + b'QAL9OHRAPDKYSVBTNYRUUBNEEAINJMOVOHOWXUAIEDAIQDESQFCKJELHAVODSMXMKE' + b'HTDKCDIWWISXSAHQE9TJTLJZGXIABHU9CUACMLVSSYV9UJREPWFVYWWXPYYJRP9DOE' + b'KNDMBSBKKHIFMPXZXIJERXRZVBVDBYNZBBCCOSEDOLDGSNQK99HIYSWNYYEBLRT9MA' + b'DLXLLZJOSZCFWAVZY9XUPNZUVOSKBMKXXJNRKDBOSGUGME9QNBMHIWXWXPEEUVQAQV' + b'UXDJGMJOBXG9VJBWPRQRCCQSNBEHTLGOKJVYEPQOJO9QIZLYAVLCKVXKEKRGBSZJAC' + b'9KTSSNMDQGKCLPZDJAQ9PBQMLUONVVFAWTMREGFXJMRRGL9MKNPOZGOYRPDCYEJCYJ' + b'UN9HYNSNHXARMRJVXBUHOP9K9BIIEYGSHBUESKTAOQOEANEAIHYHVGSVNPXWRBTJAM' + b'KMWEQOSYEWXLSRYVOSTMPOGYNPDNFLOICXVHYBDHSXVRKVWNVZOZQDOITZWICSYEW9' + b'RGCPPUJYVIYVTSZILYENYUYUGDSGWVYWRMZJNCTTPVWDWXAPVZQQKI9CGEQPBFPCLG' + b'DDEGBUUTISNCMJXQCTUNKQTLCATNOIRPMEUQBQTHHQYRGDLZEUZBALNQDXJYZBVXDP' + b'LVOVVAUCQSCGRTUJRBBNRV9ORETTGFIXBBBVOPFHPKGPKVBYFTZMWUVZYVWWSDKQVO' + b'NMPLLQTV9IZUWLUWZNLCVJNPMG9CMXQG9D9WYCANBRMYV9DU9FMJT9JHT9RWCGLHFC' + b'ODXJVFQBLTKJWVNVGSUHNWLHNPLZDSWDMDVQTLVCSVFJJTIQZFAPCXJWDAXWJKJVOK' + b'HALCQQTIXABPFXPUFK9IKXYUGMPXNSQCJDVETOVEX9LXYLXWRW9PFEYJCUJHLUB9NX' + b'TUGLIQMDGPDPSJTWDYEWXQAICLN9BTGNBJWLVAXZGNCYXGHBMRUVVYTJGH9XDGSZHQ' + b'DYKFGMOWORSFDFBLJHBRTXRSEBALCJIJTQJYDZZKWZGVAPFVKVEOXGYRLMBSPFHUIJ' + b'ZZFMFVOTLPUWSYZCWFZMAALHRGSYSXSMOHWARYZZVIAKXAHGY9SROWPVFACXXLQEXX' + b'OJCKXRRZHBZXJIBWQMMZTRDFYQBSBBZQQXGCAAECMQINHJRBSGOYPCGWKPWCHBKOJT' + b'IGDASZFGONTUGDSOOLEMGOEBFCZZJZSCGXPHXHB9WGMMFVUTCHDBSAMYTECQZWGCXA' + b'WTCTIBZHQVUAIBPZHBBTZAERYU9XAMKBHCHGZISSPOWJIRZTAXDHMAYBPXOXWDIUDH' + b'NBTFJNVHHJO9AWAEC9UPRRFJLNGKTXJXFDGODDOPMGLALRIJBVIFLQTYQPKCKCRBYP' + b'BYGUUFJGJFVCOURNKCGNTQNNKHDDPIVZHCJSLDUYHVPAX9YJOFTTFSKFHTOOQQRCPY' + b'ZKTDVCUZGBOBZKLVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VZZGUNUCKOYLYXOV' + b'HMGULWGSRCGXZLJVNIMZBLFOJJKOTUREMBXYOZXDUP9ROUVYOSJBGGFZMIFTKHJHHJ' + b'GZJNOYQWFZAHLJWWDDFQQAMEGJUEUSIWOHKFJWRXRSJWYPGIGZGMFNAIDGDOUUQUVH' + b'JZQPJMLCGKGADXAXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVG' + b'XBBPBEBDVGZDBWMDMLPXYJBBRNOMKGR9TSVUXSRYXQTCTYLFQORMIGDKBJLNLCQXAC' + b'VCBJGVWRJNYPCKOAILPLMWBYKDLDXLIZMZFWDXUWDEGDUURQGMJNUGJXDXYJGKOTQB' + b'GCHATROPKEN9YTXDUOCMXPGHPDANTJFRRVEVBFVCNTWNMMOVAVKBNSJIWWBVHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHVIT' + b'GYROZZNQP' + ), + + TryteString( + b'SWHZKSNCOQXPCGRTYJPUGKLBNEJFXASKY9XAUROGDAO9QQLIVRZQDJDTPLNTBGUUFG' + b'ELJPSGUMGPIUNCCTQEFU9UZIJJYJXCYWRADRHHXKBEDG9HTCHJHXUJRKMIUFOSKDGM' + b'I9QPCYQSWDCUYKQQEONJEKYWELG9MSNBHRHILGSSKMRCQJBOGNYBKEMTOFEUBEOUBD' + b'9ULP9PHWYKXEQNDMUR9BGDGPEUFRFRGNGFJFPYQXABSDALTYKL9SM9VVQCOHY9AS99' + b'EYWSHUNEQVVNLS9CNPEVMPOKMWQYFPGTNJBZCDWYPFWSBKZOYXNNVMPODEHMHNIYZC' + b'HIEDEAB9TLFOWVHF99GVRWUZWSN9IQOKWIXERKRQETZS9ZJJSQRLLPQXEWNMFVVBWO' + b'IK9MBYCEGUJ9HJRIIMBVJNGXMGPGDLOLYWFVQNOKTFZRBJSSBJTETGAIUGZOYQOFTV' + b'BKAQY9SSJWJXXYAUVUQWPXVCISFSDSHDQCPVMG9GVDAO9GIMDHZWJOKSUEUFHBGSCZ' + b'KNBTZWJXSFYNJSBSEL9UMZBAZRGYCEHOSJBMKMPMNXKEVTMUDEFWBIKOXUSBNPTNEC' + b'GVLYSOGUDJDPHYFADXRAOLQXJSJDANINJEOMCFAWWITREGCDF9OZ9ZKHPJZJNMOVGX' + b'9OKQBSGVZYWKNOPVJEOZEI9BPE9GCUEQVAHSBBRBGQTEXVZCSL9ECOWPOWZCVSCBOU' + b'SNQMTJIEKHXL9NCPRMLRNKQEHYJCLRHGZKFNBJIPKSKPRFTSKFJULTBTXFDQHWUYOS' + b'DQBHPAINVEPKCCHJDTZOJIGJZOF9AEQDBKCZSZMIWUUVHVGAFKALGITVQQKBAHKCIF' + b'SVMVZ9UDQABVIANTBUQOFBIXQBWB9KKQOVJZNVBEDAZKUTRNKGJQWMTEKV9KGCIBRD' + b'CBAPKSTMCZGUV9HTAABQDKGQBCRFNXBMZRTHF9MO9GAGQDYDVLOFMDE9QQZYR9GDSB' + b'LUVVMKMCZIMDPNCVLGDKBACWQJRWOQNKBTSDJFKQMKTVKXVNAHRHZALJGVAMXWJYRA' + b'KTEJFXAHBQGSYWWQVECQYPXVFWILNFZKGGRIFCJBSIZRDJXRJHSURPCZKOWKLFRUMV' + b'ENEGMNKUAOGVUECDSGAZNQZKBJDJPVBXLOTID9QLMFNGIWKAAIQTJJROSZBXPQRXAU' + b'CV99OGCEOTQCJ9II9ASZL9XGNSVUXVKPXYOJMF9PX9GSLEROR9FXVQ9MLEMEW9IWNW' + b'BNVAYXZ9ZETTDSMLGZAKHE9IUJBFUHXW9KWCNZOZCCTFGBGWSDAQGGSPSQHOMUVJML' + b'WBDAKYQZMWPQLLYAGUMOVMVLFD9TO9OUBTVUHHUNSFSATSEGBFVGDZUBMTWWFDPSQV' + b'CUFRVKHYYPDWRPNSKXRFTVEIBVZNGUZRQCPXVKBPKQDDLEBWIEBIPTEJIYFHBXCUVC' + b'CKTKEJAYRZCKAXLMELINWUZHG9JFBSBAKHIXMWHUWUFHFNLXNO9GKINYKRTCNN99PH' + b'PHO9MJAGUYZAPNSPWUZ99E9BEADKETLOALWNANYMHSLLQSBS9YTYVJKTVWFUVS9MFO' + b'WCHLEUUFUWTYGLZXFDUXVABTVFXFPUEPIUEIAVSZSSZQJTHNGKBJXADRHVTIBERILM' + b'CCGWUITYQHGEEGWIZZOII9B9EVVVFJNEYEWH9ZVOJGHKVPYKDEZZSPBAOBQGGWPWXT' + b'CKSLSHJQYCDHAYIQ9QVSQFPXZDBYSJJKSNTRXORHLPVOYVMIGALRPXYWQWSJPPFTJC' + b'YXAATLBFNSGVXKFJXHYTILNOQZROUCDUTWOMGYBVTWPKJY9RVKKWQQMVCHJEUBELJD' + b'KJPGYLXUXATNOIJHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHV' + b'HE9EEBQYSNWECSPAJHGLTEUCXALBRVGXFENUCOONSUFZLHTLVQNPDZDIVDQHWVLDED' + b'PFQLJZWF9GFZMPZXFVEQECLUZBBFVSAPEXJLKKOMXEPHZAKP9WYTGQOML9FQSBMSFL' + b'OGRLFQKUCUWFX9DNAOZSSKBUV9IBVIRNUWYBKJVKLJ9PPNLGJATKDCAGVFIVPXRABH' + b'ZVZACJIG9WOKKLFCRDSMTWSCYHOZEEXRIMPQBXVXQAYKZIADSM9GUBICGKGQYNHKVY' + b'OZFRVCHNM' + ), + + TryteString( + b'KJVG9EKTMPWE9PKWGGJJPDISCX9CJXGWUUPOLKKBVUWUYNBACOOF9LEQGNM9YYGNXJ' + b'EMOBGSDCPLP9CQIFBCLENUCJGCCSWYU9WVFTRZZCPCZXEGMDDPSYDTYUIMVFSLGHTA' + b'ZWJRHY9ZQMFGROIIUIQVDSOTUIRMSROYDPTWRURMZAILGBWIVADPYHPTSFGKAPPMVT' + b'THGLYXZHPFUO9HBAJIOUJOOABAQSOQUKLSVQGSDIHEHGTQSPM9EDHLEQSFFAAQXR9M' + b'UREVQ9MEGXNXMNJVWXYEIRYAOFHDFZAKNVWKTVIHKXWVT9VOZRUPUKXASIFAZQVZSW' + b'HBQU9RGVLJMRVQCMCYSQEIMIAKOHNAKQLTIMEHMZMGAKCIPGHQTWORBLVKISGPKIIM' + b'AMQWMZUNTKJSQZAZNYEGORGNRTKCLNRSOQJRBUCPSDLKLGGRBACIULLZBFBUNQXACK' + b'L9WFEKKAHGLBBRNNEXZPPH9UZESFFKVBOPROFHQOKYAVTJDDVAUGUAURHLESIEIITD' + b'VVRCTRKOGUPERJHNJMXTLNVMWVDZITSPEHRYJKEZVTZSJEYTOQEGNJRMCJLYYKPGDF' + b'UFQHGWRDGEWBXYOGEZ9IXRWJAQLKHPROWIEVI9ILNOXTPOSRLETMNEQ9P9WLXCUZNM' + b'GFK9EYHABBCSEZSGMNJZOEEGRVNU9ASSOOLCXXZKZPFWU9EEUUQRACVGZPL9MQINGL' + b'YPUTUPTLPKWPHRFFBRHZQWIVOXPGAKCQQPRKPPZUHOJISYASMRYMCMJZNR9D9OQANU' + b'XGJXSUSZQFWDJUTNCDKAUAFYKJNVAMBLTPPRPIJRRKQMCIHHGKPQPUQHWJNIEPDLRA' + b'YSJXVSJVKAGBAJCMGQSCZFTEJSG9LUWZGFBGQUHFUHWDHND9WJBPOQQXDEATOBGXDG' + b'M9BKSDCOEZ9IENZPPDUPMKCUKYBIBTBMJPJLDNSOPEKHVGQKLGUISUFSYMHR9I9LRP' + b'LCXJTDHHEXKQEVIFOUGKJEILQIHFG9FWOUBXRHCRHLOYAXTFQUWKJBSX9GNPCWXUQJ' + b'RHDBOBRZPQAPMKCIZGULPZDYLLBMAFJZXGIRVAAVUUCSNGDGJQJTAPV9QXYIABIHBX' + b'ILKQLGDGXQUVADQGDFKKDKMU9WKBEEY9TAVRYQDQFKPDMLMUAEGBHVJPSIZOEQGCSY' + b'NJCICXROXHPZFUXASQJXZEHQTEUKFIYQIGJWORKAIQUFROYGMIDFAJOUFAYYWMXUGJ' + b'FPSRTGEUWWLOXEUTKZCZQHWFUNHTMZVIJ9VYOLBTAIFB9EN9NFVAABVFIBIWXLJSUO' + b'YELOQSIPK99AXSXCPECWOXFUVDIANVO9PKZUESMFWIEVWLEHLCVKDXEROLNEMYRRCJ' + b'DPAYVTYAYSL9AFZH9GXHXZORXZEQTUJEDJGCYCQAENYZRKDJSK9TOCKKCXOSSTOAIO' + b'9UVAKQJBVOS9RUQIESCIJYRWYRUPMIJEHR9EGZ9YMHQXALUUDMCFYFOMLIGORMMBCD' + b'JMFCNNELGPXHICRNRKONBKACHLLSABUNHQ9TU9OSSTQXGWBLRRTSKZORXILALQYRXD' + b'DMXPPUTEGTVCHSOVYZEEJMRRECGBMXBORUTIQUNMJDXBSZSYYA9UOTFWMQOHURUFSU' + b'ESLMILBBKGHTTFTZONNQIMJSLILKAQJRDTNVK9PHAMNKZXRHSOPGKKLJBRDYAC9BRU' + b'RJWUIJLUWXNQOSVVLFEBROMJCGVYZWIPOYFQRBUUNJLIGPVDLADFLZJGZBLEBEQEUD' + b'UZOIFFZLZRXCPQVMIARFLZRIOFFEHVFJZXFQFLCJSEXRPUKGMWBMGXEHIEZKOKGH9J' + b'XAUXACEBLXKLZT9P9NJGXRZWZJAZCNKR9CHRRNCFOLBCSZXKXOIGZHZSTDKTHOWZTE' + b'XWOIZLPEGPKTBRENSCOYBQJSZQ9XPNRORQOWMPGBXKSSLDIUVEAJODEUZJKZE9MBVT' + b'QXWFXXXOG9QGDWMNZEWVWVDZWIFKSWZIDNBYEJP9VBKQNOJEXVPZQTHVUCSK9QCMEP' + b'US9Y9FQPWEACAEBIQSVPJEL9ZBSETINIYMSPIXLADSHTDYRAYUTMXDCABIUUETMNLU' + b'RELTPAGEDNMQZALFWOPAI9WUFOSUTOFUUWFAFRFVYOPITBVSG9IBVNJCOITYMTCCIJ' + b'IZWVPYGQE' + ), + + TryteString( + b'GWLDXDNSEIQCQJKVVFEWPWR99OKSHTVIJCNFEGSUM9DUQRO9ZJUWOOGP9XLABZFDXN' + b'GOXZLWETWXTTBT9KIGB9VOMMTKNJTUUFGJIYZIMHEAEJTNTIIOLLO9VWCYX9JA9RML' + b'SB9COUYKMRZQWJXMIFXCETZWRDXHBBOYLYLURXBELK9YLIXXGHSP9TNNASKDGFVJQV' + b'99CMXRM9VHASOBYBTWIMAJLBRUPZQLDCKOFAPHG9DKVVEFHTZAGNC9KH9K9HIFNLUI' + b'NQFTQTSALBNV9HRWXDGDEBBKIMQCDWVTMPDIVCXHGKDFPAKTSYYJIROENCJOZXVBNL' + b'UIUJHHAXZ9PTMNFGRRCNHQUVEESVSYNSIQXDRKKBMWJOQSMIK9FPHTNAJUYTQ9BLOG' + b'9GZPXHACSPIFCDX9LIVQDISFAVZWQUXP9BROHMGBHFTVWEWCZRPTAMTXXLVLZBT9BM' + b'OSJXAIGYUXICBUGQDOJRMYFWYGLT9UBTKGZZPNDIPNVIHQIBXFQACGYPWTKJSRHVQL' + b'VJAJWFGNFLAJYOADR9XNOAYOLHKEUGWSOCXYJVHWLRRBE9XYLQDYJXYMURFPXTMNHE' + b'EXJGVY9ADSJICXGWOUKYWVMXMWSJQVPKTUQUSCHTREWZNTXBDUJWDVTMXPABBHGYOC' + b'UNFIFTUQTRAVTCFAQNNAAXBCRILNVZGGKEKIUOXBVMXLFNCSHFMH9PYR9DBXFKWIBT' + b'AZLQTMVEGCZLWESPAHZLVTCGUUJXEAPCEYBXGGARHGDODWULDHHMMKEIYRFFEMQTTG' + b'SGWTOGBZYEULWWFINFHGYEDHHXAJASMQCLBKWYXSBIWZLMEZVXUWP999OROQYLOFVA' + b'ZGJIGHMTGJSZNGXFWMMUCGGQXB9ASA9UCVZLVYZG9XBIF9HUAB9HBYERWFJ9IEDMAY' + b'ZSIFDHOX9HRQSDEGWUAODHRNVBQWTBK9JFZBNKBATUXBZOIEHPTFPQXSBGHGOEVMUT' + b'RPSTRFOKHWEUPUSEROZEBSMTSTZSHFTQ9UXYTMDVLAPXSHZGYLPVDGTCGHOQSWJJED' + b'ARRUPCYFHJOSPVSTNEERBJOERGU9TTOW9GSVZEODZOEQZATYADJ9NURBJNTPBYECGG' + b'WP9SVOZAWXT9RLKBKL9TBBWXSDOSXRJHPKMLIPWKXSM9MPNQWEYLDPRLTUUNEFHXUF' + b'CLLJLZIRGUMEJCTIHC9VPS9YPENZPBYKTSBPXIPZHNYZYDPOYRIFEZWOFDYMZTUOMZ' + b'ZHLSLZMTDIMTTXMHHTDLIVRSIDSWJBCDCKEYZPTLLZP9IMNJSRXICEGTPZXVXAVIBG' + b'JMMOUNPUKXHIANUPGJANUHTG9ZPZCBFRMLHYOPFAKGRZSZJARBEEPQZ9TKJRQLXEG9' + b'IOHETGXCMKT9XZUBPMIQWXRRRFF9POXJBXW9NPUIOYNET9CTUWJB9RQDHVIAFLKILV' + b'BDLOYZAKIRHAUXE9ORNAPVXRTUY9CNXAPFPNUADXHDQWGRCVBZMUASLOPAYHLNGNUV' + b'VTDQCSOSTOOILZFXBXUPILJVVDUIRBWQUYNOJX99BTZNYQZGTENKGEKKADMPDWQB9I' + b'CWBWFHKAPRNDGGWOUXDTJKMASYOPYNYPTOEN9EDLXVVUMELPGG9ZLAJXQFTIEA9HRJ' + b'QCJLRUSLBGIWRWRXMTSAYVNHNJCYDSYNBPH9XEI9NFEDANKTZ9RWSCMPV9XVBTBZVD' + b'O9HABGD9VDOIXFMWBCHERKTDPDQFQSVNZLZRPHVZTFTL9LRAIMXLMTEZFAKK9CMYVP' + b'RTGBXGIMHUUVWCHDUUEZMZFMDSUQRVVPHZDUTOTLPSKQEHWNLOXKGGJKHHUNQIJXUT' + b'NYMZIL9UOEKECBSTCRVTVKUWETWPECLAXJWUNXXNRDBR99KJSWCHJBTMK9TSLLKWUC' + b'MMWNABUZLKLCJXHPUWVLIEIHYTZRPTZJTUMDDVEFCDRQYHPBF9WVMATUIQXGWTGAHQ' + b'STNRVZZIPBRPIUOZLXRGEWSUVDXIQPAONF9QPFYIMUEMDXOMFPKKJNGRBNMKXNJUF9' + b'IQIHPEBHSLWQWXJZNEBKCQUSRWOEGMWFZYGHFUUHDBBOBKSTXT9HGOORUQMFBFBICA' + b'HBQNOBVDCZVGZGASCINUGVEMM9LLPWTNWWVKWYIYDIJEKAVBEFPAVMFWEOYMTOHLZV' + b'PRMIINUJT' + ), + ]) + + # noinspection PyUnusedLocal + def _create_signature_fragment_generator(bundle, key_generator, txn): + return mock_signature_fragment_generator + + with patch( + 'iota.transaction.ProposedBundle._create_signature_fragment_generator', + _create_signature_fragment_generator, + ): + response = self.command( + seed = Seed( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' + b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' + ), + + transfers = [ + ProposedTransaction( + value = 42, + address = Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + ), + ], + + inputs = [ + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', + + # + # Normally, you would use an AddressGenerator to create + # new addresses, so ``key_index`` would be populated + # automatically. + # + # But, AddressGenerator runs a bit slowly, so to speed up + # test execution, we will use hard-coded values. + # + key_index = 4, + ), + + Address( + trytes = + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + + key_index = 5, + ), + ], + ) + + self.assertDictEqual( + response, + + { + 'trytes': [ + # Ipnut #2, Part 2 of 2 + TryteString( + b'GWLDXDNSEIQCQJKVVFEWPWR99OKSHTVIJCNFEGSUM9DUQRO9ZJUWOOGP9XLABZFD' + b'XNGOXZLWETWXTTBT9KIGB9VOMMTKNJTUUFGJIYZIMHEAEJTNTIIOLLO9VWCYX9JA' + b'9RMLSB9COUYKMRZQWJXMIFXCETZWRDXHBBOYLYLURXBELK9YLIXXGHSP9TNNASKD' + b'GFVJQV99CMXRM9VHASOBYBTWIMAJLBRUPZQLDCKOFAPHG9DKVVEFHTZAGNC9KH9K' + b'9HIFNLUINQFTQTSALBNV9HRWXDGDEBBKIMQCDWVTMPDIVCXHGKDFPAKTSYYJIROE' + b'NCJOZXVBNLUIUJHHAXZ9PTMNFGRRCNHQUVEESVSYNSIQXDRKKBMWJOQSMIK9FPHT' + b'NAJUYTQ9BLOG9GZPXHACSPIFCDX9LIVQDISFAVZWQUXP9BROHMGBHFTVWEWCZRPT' + b'AMTXXLVLZBT9BMOSJXAIGYUXICBUGQDOJRMYFWYGLT9UBTKGZZPNDIPNVIHQIBXF' + b'QACGYPWTKJSRHVQLVJAJWFGNFLAJYOADR9XNOAYOLHKEUGWSOCXYJVHWLRRBE9XY' + b'LQDYJXYMURFPXTMNHEEXJGVY9ADSJICXGWOUKYWVMXMWSJQVPKTUQUSCHTREWZNT' + b'XBDUJWDVTMXPABBHGYOCUNFIFTUQTRAVTCFAQNNAAXBCRILNVZGGKEKIUOXBVMXL' + b'FNCSHFMH9PYR9DBXFKWIBTAZLQTMVEGCZLWESPAHZLVTCGUUJXEAPCEYBXGGARHG' + b'DODWULDHHMMKEIYRFFEMQTTGSGWTOGBZYEULWWFINFHGYEDHHXAJASMQCLBKWYXS' + b'BIWZLMEZVXUWP999OROQYLOFVAZGJIGHMTGJSZNGXFWMMUCGGQXB9ASA9UCVZLVY' + b'ZG9XBIF9HUAB9HBYERWFJ9IEDMAYZSIFDHOX9HRQSDEGWUAODHRNVBQWTBK9JFZB' + b'NKBATUXBZOIEHPTFPQXSBGHGOEVMUTRPSTRFOKHWEUPUSEROZEBSMTSTZSHFTQ9U' + b'XYTMDVLAPXSHZGYLPVDGTCGHOQSWJJEDARRUPCYFHJOSPVSTNEERBJOERGU9TTOW' + b'9GSVZEODZOEQZATYADJ9NURBJNTPBYECGGWP9SVOZAWXT9RLKBKL9TBBWXSDOSXR' + b'JHPKMLIPWKXSM9MPNQWEYLDPRLTUUNEFHXUFCLLJLZIRGUMEJCTIHC9VPS9YPENZ' + b'PBYKTSBPXIPZHNYZYDPOYRIFEZWOFDYMZTUOMZZHLSLZMTDIMTTXMHHTDLIVRSID' + b'SWJBCDCKEYZPTLLZP9IMNJSRXICEGTPZXVXAVIBGJMMOUNPUKXHIANUPGJANUHTG' + b'9ZPZCBFRMLHYOPFAKGRZSZJARBEEPQZ9TKJRQLXEG9IOHETGXCMKT9XZUBPMIQWX' + b'RRRFF9POXJBXW9NPUIOYNET9CTUWJB9RQDHVIAFLKILVBDLOYZAKIRHAUXE9ORNA' + b'PVXRTUY9CNXAPFPNUADXHDQWGRCVBZMUASLOPAYHLNGNUVVTDQCSOSTOOILZFXBX' + b'UPILJVVDUIRBWQUYNOJX99BTZNYQZGTENKGEKKADMPDWQB9ICWBWFHKAPRNDGGWO' + b'UXDTJKMASYOPYNYPTOEN9EDLXVVUMELPGG9ZLAJXQFTIEA9HRJQCJLRUSLBGIWRW' + b'RXMTSAYVNHNJCYDSYNBPH9XEI9NFEDANKTZ9RWSCMPV9XVBTBZVDO9HABGD9VDOI' + b'XFMWBCHERKTDPDQFQSVNZLZRPHVZTFTL9LRAIMXLMTEZFAKK9CMYVPRTGBXGIMHU' + b'UVWCHDUUEZMZFMDSUQRVVPHZDUTOTLPSKQEHWNLOXKGGJKHHUNQIJXUTNYMZIL9U' + b'OEKECBSTCRVTVKUWETWPECLAXJWUNXXNRDBR99KJSWCHJBTMK9TSLLKWUCMMWNAB' + b'UZLKLCJXHPUWVLIEIHYTZRPTZJTUMDDVEFCDRQYHPBF9WVMATUIQXGWTGAHQSTNR' + b'VZZIPBRPIUOZLXRGEWSUVDXIQPAONF9QPFYIMUEMDXOMFPKKJNGRBNMKXNJUF9IQ' + b'IHPEBHSLWQWXJZNEBKCQUSRWOEGMWFZYGHFUUHDBBOBKSTXT9HGOORUQMFBFBICA' + b'HBQNOBVDCZVGZGASCINUGVEMM9LLPWTNWWVKWYIYDIJEKAVBEFPAVMFWEOYMTOHL' + b'ZVPRMIINUJTTESTVALUEFOUR9DONTUSEINPRODUCTION99999WJRBOSBIMNTGDYK' + b'UDYYFJFGZOHORYSQPCWJRKHIOVIY999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99D99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #2, Part 1 of 2 + TryteString( + b'KJVG9EKTMPWE9PKWGGJJPDISCX9CJXGWUUPOLKKBVUWUYNBACOOF9LEQGNM9YYGN' + b'XJEMOBGSDCPLP9CQIFBCLENUCJGCCSWYU9WVFTRZZCPCZXEGMDDPSYDTYUIMVFSL' + b'GHTAZWJRHY9ZQMFGROIIUIQVDSOTUIRMSROYDPTWRURMZAILGBWIVADPYHPTSFGK' + b'APPMVTTHGLYXZHPFUO9HBAJIOUJOOABAQSOQUKLSVQGSDIHEHGTQSPM9EDHLEQSF' + b'FAAQXR9MUREVQ9MEGXNXMNJVWXYEIRYAOFHDFZAKNVWKTVIHKXWVT9VOZRUPUKXA' + b'SIFAZQVZSWHBQU9RGVLJMRVQCMCYSQEIMIAKOHNAKQLTIMEHMZMGAKCIPGHQTWOR' + b'BLVKISGPKIIMAMQWMZUNTKJSQZAZNYEGORGNRTKCLNRSOQJRBUCPSDLKLGGRBACI' + b'ULLZBFBUNQXACKL9WFEKKAHGLBBRNNEXZPPH9UZESFFKVBOPROFHQOKYAVTJDDVA' + b'UGUAURHLESIEIITDVVRCTRKOGUPERJHNJMXTLNVMWVDZITSPEHRYJKEZVTZSJEYT' + b'OQEGNJRMCJLYYKPGDFUFQHGWRDGEWBXYOGEZ9IXRWJAQLKHPROWIEVI9ILNOXTPO' + b'SRLETMNEQ9P9WLXCUZNMGFK9EYHABBCSEZSGMNJZOEEGRVNU9ASSOOLCXXZKZPFW' + b'U9EEUUQRACVGZPL9MQINGLYPUTUPTLPKWPHRFFBRHZQWIVOXPGAKCQQPRKPPZUHO' + b'JISYASMRYMCMJZNR9D9OQANUXGJXSUSZQFWDJUTNCDKAUAFYKJNVAMBLTPPRPIJR' + b'RKQMCIHHGKPQPUQHWJNIEPDLRAYSJXVSJVKAGBAJCMGQSCZFTEJSG9LUWZGFBGQU' + b'HFUHWDHND9WJBPOQQXDEATOBGXDGM9BKSDCOEZ9IENZPPDUPMKCUKYBIBTBMJPJL' + b'DNSOPEKHVGQKLGUISUFSYMHR9I9LRPLCXJTDHHEXKQEVIFOUGKJEILQIHFG9FWOU' + b'BXRHCRHLOYAXTFQUWKJBSX9GNPCWXUQJRHDBOBRZPQAPMKCIZGULPZDYLLBMAFJZ' + b'XGIRVAAVUUCSNGDGJQJTAPV9QXYIABIHBXILKQLGDGXQUVADQGDFKKDKMU9WKBEE' + b'Y9TAVRYQDQFKPDMLMUAEGBHVJPSIZOEQGCSYNJCICXROXHPZFUXASQJXZEHQTEUK' + b'FIYQIGJWORKAIQUFROYGMIDFAJOUFAYYWMXUGJFPSRTGEUWWLOXEUTKZCZQHWFUN' + b'HTMZVIJ9VYOLBTAIFB9EN9NFVAABVFIBIWXLJSUOYELOQSIPK99AXSXCPECWOXFU' + b'VDIANVO9PKZUESMFWIEVWLEHLCVKDXEROLNEMYRRCJDPAYVTYAYSL9AFZH9GXHXZ' + b'ORXZEQTUJEDJGCYCQAENYZRKDJSK9TOCKKCXOSSTOAIO9UVAKQJBVOS9RUQIESCI' + b'JYRWYRUPMIJEHR9EGZ9YMHQXALUUDMCFYFOMLIGORMMBCDJMFCNNELGPXHICRNRK' + b'ONBKACHLLSABUNHQ9TU9OSSTQXGWBLRRTSKZORXILALQYRXDDMXPPUTEGTVCHSOV' + b'YZEEJMRRECGBMXBORUTIQUNMJDXBSZSYYA9UOTFWMQOHURUFSUESLMILBBKGHTTF' + b'TZONNQIMJSLILKAQJRDTNVK9PHAMNKZXRHSOPGKKLJBRDYAC9BRURJWUIJLUWXNQ' + b'OSVVLFEBROMJCGVYZWIPOYFQRBUUNJLIGPVDLADFLZJGZBLEBEQEUDUZOIFFZLZR' + b'XCPQVMIARFLZRIOFFEHVFJZXFQFLCJSEXRPUKGMWBMGXEHIEZKOKGH9JXAUXACEB' + b'LXKLZT9P9NJGXRZWZJAZCNKR9CHRRNCFOLBCSZXKXOIGZHZSTDKTHOWZTEXWOIZL' + b'PEGPKTBRENSCOYBQJSZQ9XPNRORQOWMPGBXKSSLDIUVEAJODEUZJKZE9MBVTQXWF' + b'XXXOG9QGDWMNZEWVWVDZWIFKSWZIDNBYEJP9VBKQNOJEXVPZQTHVUCSK9QCMEPUS' + b'9Y9FQPWEACAEBIQSVPJEL9ZBSETINIYMSPIXLADSHTDYRAYUTMXDCABIUUETMNLU' + b'RELTPAGEDNMQZALFWOPAI9WUFOSUTOFUUWFAFRFVYOPITBVSG9IBVNJCOITYMTCC' + b'IJIZWVPYGQETESTVALUEFOUR9DONTUSEINPRODUCTION99999WJRBOSBIMNTGDYK' + b'UDYYFJFGZOHORYSQPCWJRKHIOVIYYZ9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99C99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 2 of 2 + TryteString( + b'SWHZKSNCOQXPCGRTYJPUGKLBNEJFXASKY9XAUROGDAO9QQLIVRZQDJDTPLNTBGUU' + b'FGELJPSGUMGPIUNCCTQEFU9UZIJJYJXCYWRADRHHXKBEDG9HTCHJHXUJRKMIUFOS' + b'KDGMI9QPCYQSWDCUYKQQEONJEKYWELG9MSNBHRHILGSSKMRCQJBOGNYBKEMTOFEU' + b'BEOUBD9ULP9PHWYKXEQNDMUR9BGDGPEUFRFRGNGFJFPYQXABSDALTYKL9SM9VVQC' + b'OHY9AS99EYWSHUNEQVVNLS9CNPEVMPOKMWQYFPGTNJBZCDWYPFWSBKZOYXNNVMPO' + b'DEHMHNIYZCHIEDEAB9TLFOWVHF99GVRWUZWSN9IQOKWIXERKRQETZS9ZJJSQRLLP' + b'QXEWNMFVVBWOIK9MBYCEGUJ9HJRIIMBVJNGXMGPGDLOLYWFVQNOKTFZRBJSSBJTE' + b'TGAIUGZOYQOFTVBKAQY9SSJWJXXYAUVUQWPXVCISFSDSHDQCPVMG9GVDAO9GIMDH' + b'ZWJOKSUEUFHBGSCZKNBTZWJXSFYNJSBSEL9UMZBAZRGYCEHOSJBMKMPMNXKEVTMU' + b'DEFWBIKOXUSBNPTNECGVLYSOGUDJDPHYFADXRAOLQXJSJDANINJEOMCFAWWITREG' + b'CDF9OZ9ZKHPJZJNMOVGX9OKQBSGVZYWKNOPVJEOZEI9BPE9GCUEQVAHSBBRBGQTE' + b'XVZCSL9ECOWPOWZCVSCBOUSNQMTJIEKHXL9NCPRMLRNKQEHYJCLRHGZKFNBJIPKS' + b'KPRFTSKFJULTBTXFDQHWUYOSDQBHPAINVEPKCCHJDTZOJIGJZOF9AEQDBKCZSZMI' + b'WUUVHVGAFKALGITVQQKBAHKCIFSVMVZ9UDQABVIANTBUQOFBIXQBWB9KKQOVJZNV' + b'BEDAZKUTRNKGJQWMTEKV9KGCIBRDCBAPKSTMCZGUV9HTAABQDKGQBCRFNXBMZRTH' + b'F9MO9GAGQDYDVLOFMDE9QQZYR9GDSBLUVVMKMCZIMDPNCVLGDKBACWQJRWOQNKBT' + b'SDJFKQMKTVKXVNAHRHZALJGVAMXWJYRAKTEJFXAHBQGSYWWQVECQYPXVFWILNFZK' + b'GGRIFCJBSIZRDJXRJHSURPCZKOWKLFRUMVENEGMNKUAOGVUECDSGAZNQZKBJDJPV' + b'BXLOTID9QLMFNGIWKAAIQTJJROSZBXPQRXAUCV99OGCEOTQCJ9II9ASZL9XGNSVU' + b'XVKPXYOJMF9PX9GSLEROR9FXVQ9MLEMEW9IWNWBNVAYXZ9ZETTDSMLGZAKHE9IUJ' + b'BFUHXW9KWCNZOZCCTFGBGWSDAQGGSPSQHOMUVJMLWBDAKYQZMWPQLLYAGUMOVMVL' + b'FD9TO9OUBTVUHHUNSFSATSEGBFVGDZUBMTWWFDPSQVCUFRVKHYYPDWRPNSKXRFTV' + b'EIBVZNGUZRQCPXVKBPKQDDLEBWIEBIPTEJIYFHBXCUVCCKTKEJAYRZCKAXLMELIN' + b'WUZHG9JFBSBAKHIXMWHUWUFHFNLXNO9GKINYKRTCNN99PHPHO9MJAGUYZAPNSPWU' + b'Z99E9BEADKETLOALWNANYMHSLLQSBS9YTYVJKTVWFUVS9MFOWCHLEUUFUWTYGLZX' + b'FDUXVABTVFXFPUEPIUEIAVSZSSZQJTHNGKBJXADRHVTIBERILMCCGWUITYQHGEEG' + b'WIZZOII9B9EVVVFJNEYEWH9ZVOJGHKVPYKDEZZSPBAOBQGGWPWXTCKSLSHJQYCDH' + b'AYIQ9QVSQFPXZDBYSJJKSNTRXORHLPVOYVMIGALRPXYWQWSJPPFTJCYXAATLBFNS' + b'GVXKFJXHYTILNOQZROUCDUTWOMGYBVTWPKJY9RVKKWQQMVCHJEUBELJDKJPGYLXU' + b'XATNOIJHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHVHE9EEB' + b'QYSNWECSPAJHGLTEUCXALBRVGXFENUCOONSUFZLHTLVQNPDZDIVDQHWVLDEDPFQL' + b'JZWF9GFZMPZXFVEQECLUZBBFVSAPEXJLKKOMXEPHZAKP9WYTGQOML9FQSBMSFLOG' + b'RLFQKUCUWFX9DNAOZSSKBUV9IBVIRNUWYBKJVKLJ9PPNLGJATKDCAGVFIVPXRABH' + b'ZVZACJIG9WOKKLFCRDSMTWSCYHOZEEXRIMPQBXVXQAYKZIADSM9GUBICGKGQYNHK' + b'VYOZFRVCHNMTESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVW999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99B99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 1 of 2 + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWG' + b'HMNUXBJTOBKZFDNJEMAOPPLR9OOQJCDVO9XSCYQJQVTXQDYWQEBIXKDZAFWINAHJ' + b'ELJTDPVMUEWSVCJA9ONDYBNANWCGLBQMEMTBFDMWLCMQHGJLGYDQGIMLSNQHBGSV' + b'TDZSGNQAL9OHRAPDKYSVBTNYRUUBNEEAINJMOVOHOWXUAIEDAIQDESQFCKJELHAV' + b'ODSMXMKEHTDKCDIWWISXSAHQE9TJTLJZGXIABHU9CUACMLVSSYV9UJREPWFVYWWX' + b'PYYJRP9DOEKNDMBSBKKHIFMPXZXIJERXRZVBVDBYNZBBCCOSEDOLDGSNQK99HIYS' + b'WNYYEBLRT9MADLXLLZJOSZCFWAVZY9XUPNZUVOSKBMKXXJNRKDBOSGUGME9QNBMH' + b'IWXWXPEEUVQAQVUXDJGMJOBXG9VJBWPRQRCCQSNBEHTLGOKJVYEPQOJO9QIZLYAV' + b'LCKVXKEKRGBSZJAC9KTSSNMDQGKCLPZDJAQ9PBQMLUONVVFAWTMREGFXJMRRGL9M' + b'KNPOZGOYRPDCYEJCYJUN9HYNSNHXARMRJVXBUHOP9K9BIIEYGSHBUESKTAOQOEAN' + b'EAIHYHVGSVNPXWRBTJAMKMWEQOSYEWXLSRYVOSTMPOGYNPDNFLOICXVHYBDHSXVR' + b'KVWNVZOZQDOITZWICSYEW9RGCPPUJYVIYVTSZILYENYUYUGDSGWVYWRMZJNCTTPV' + b'WDWXAPVZQQKI9CGEQPBFPCLGDDEGBUUTISNCMJXQCTUNKQTLCATNOIRPMEUQBQTH' + b'HQYRGDLZEUZBALNQDXJYZBVXDPLVOVVAUCQSCGRTUJRBBNRV9ORETTGFIXBBBVOP' + b'FHPKGPKVBYFTZMWUVZYVWWSDKQVONMPLLQTV9IZUWLUWZNLCVJNPMG9CMXQG9D9W' + b'YCANBRMYV9DU9FMJT9JHT9RWCGLHFCODXJVFQBLTKJWVNVGSUHNWLHNPLZDSWDMD' + b'VQTLVCSVFJJTIQZFAPCXJWDAXWJKJVOKHALCQQTIXABPFXPUFK9IKXYUGMPXNSQC' + b'JDVETOVEX9LXYLXWRW9PFEYJCUJHLUB9NXTUGLIQMDGPDPSJTWDYEWXQAICLN9BT' + b'GNBJWLVAXZGNCYXGHBMRUVVYTJGH9XDGSZHQDYKFGMOWORSFDFBLJHBRTXRSEBAL' + b'CJIJTQJYDZZKWZGVAPFVKVEOXGYRLMBSPFHUIJZZFMFVOTLPUWSYZCWFZMAALHRG' + b'SYSXSMOHWARYZZVIAKXAHGY9SROWPVFACXXLQEXXOJCKXRRZHBZXJIBWQMMZTRDF' + b'YQBSBBZQQXGCAAECMQINHJRBSGOYPCGWKPWCHBKOJTIGDASZFGONTUGDSOOLEMGO' + b'EBFCZZJZSCGXPHXHB9WGMMFVUTCHDBSAMYTECQZWGCXAWTCTIBZHQVUAIBPZHBBT' + b'ZAERYU9XAMKBHCHGZISSPOWJIRZTAXDHMAYBPXOXWDIUDHNBTFJNVHHJO9AWAEC9' + b'UPRRFJLNGKTXJXFDGODDOPMGLALRIJBVIFLQTYQPKCKCRBYPBYGUUFJGJFVCOURN' + b'KCGNTQNNKHDDPIVZHCJSLDUYHVPAX9YJOFTTFSKFHTOOQQRCPYZKTDVCUZGBOBZK' + b'LVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VZZGUNUCKOYLYXOVHMGULWGSRCGX' + b'ZLJVNIMZBLFOJJKOTUREMBXYOZXDUP9ROUVYOSJBGGFZMIFTKHJHHJGZJNOYQWFZ' + b'AHLJWWDDFQQAMEGJUEUSIWOHKFJWRXRSJWYPGIGZGMFNAIDGDOUUQUVHJZQPJMLC' + b'GKGADXAXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVGXBBPBE' + b'BDVGZDBWMDMLPXYJBBRNOMKGR9TSVUXSRYXQTCTYLFQORMIGDKBJLNLCQXACVCBJ' + b'GVWRJNYPCKOAILPLMWBYKDLDXLIZMZFWDXUWDEGDUURQGMJNUGJXDXYJGKOTQBGC' + b'HATROPKEN9YTXDUOCMXPGHPDANTJFRRVEVBFVCNTWNMMOVAVKBNSJIWWBVHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHV' + b'ITGYROZZNQPTESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVWN99999999999999999999999999999999999' + b'999999999999999999NYBKIVD99A99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Spend transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9' + b'RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99999999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) + + def test_pass_inputs_explicit_with_change(self): """ - Preparing a bundle with specified inputs. + Preparing a bundle with specified inputs, change address needed. """ # :todo: Implement test. self.skipTest('Not implemented yet.') @@ -557,9 +1016,18 @@ def test_fail_inputs_explicit_insufficient(self): # :todo: Implement test. self.skipTest('Not implemented yet.') - def test_pass_inputs_implicit(self): + def test_pass_inputs_implicit_no_change(self): """ - Preparing a bundle that finds inputs to use automatically. + Preparing a bundle that finds inputs to use automatically, no + change address needed. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_pass_inputs_implicit_with_change(self): + """ + Preparing a bundle that finds inputs to use automatically, change + address needed. """ # :todo: Implement test. self.skipTest('Not implemented yet.') @@ -577,3 +1045,47 @@ def test_pass_change_address_auto_generated(self): """ # :todo: Implement test. self.skipTest('Not implemented yet.') + + +class MockSignatureFragmentGenerator(object): + """ + Mocks the behavior of ``SignatureFragmentGenerator`` to speed up unit + tests. + + Note that ``SignatureFragmentGenerator`` already has its own test + case, so this approach does not compromise test integrity. + + References: + - :py:class:`iota.crypto.signing.SignatureFragmentGenerator` + - :py:meth:`iota.transaction.ProposedBundle.sign_inputs` + """ + def __init__( + self, + fragments = None, + length = AddressGenerator.DIGEST_ITERATIONS + ): + # type: (Optional[Iterable[TryteString]], int) -> None + """ + :param fragments: + Provide fragments to seed (shortcut for calling :py:meth:`seed`). + + :param length: + Length that the generator will report to the bundle, used to + ensure that it iterates the correct number of times. + """ + super(MockSignatureFragmentGenerator, self).__init__() + + self.fragments = list(fragments or []) # type: List[TryteString] + self.length = length + + def __len__(self): + return self.length + + def seed(self, fragment): + # type: (TryteString) -> None + self.fragments.append(fragment) + + # noinspection PyUnusedLocal + def send(self, source_trytes): + # type: (TryteString) -> TryteString + return self.fragments.pop(0) From ecbe3e9aa345488c1db4ce623adb6a0ffa4f372b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 14:56:32 -0500 Subject: [PATCH 208/239] Verified `prepareTransfers` w/ explicit inputs and change. --- .../extended/prepare_transfers_test.py | 319 +++++++++++++++++- 1 file changed, 317 insertions(+), 2 deletions(-) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index 186f020..3bddf1d 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -1006,8 +1006,323 @@ def test_pass_inputs_explicit_with_change(self): """ Preparing a bundle with specified inputs, change address needed. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.adapter.seed_response('getBalances', { + 'balances': [86], + 'duration': '1', + + 'milestone': + 'TESTVALUE9DONTUSEINPRODUCTION99999ZNIUXU' + 'FIVFBBYQHFYZYIEEWZL9VPMMKIIYTEZRRHXJXKIKF', + }) + + mock_signature_fragment_generator = MockSignatureFragmentGenerator([ + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWGHM' + b'NUXBJTOBKZFDNJEZUKCKWGUHVSU9ZJYAVSQSOFDCOIEP9LCXYLTEFMCYUJAAHLYUHQ' + b'P99S9XRWHXHRPZCWHDMIDYW9OQAWUPTFMBTJGDCWRVNVRDPIWISVYNUDWUGBPNNFZD' + b'WRVZ9FGAVSEWFXRXGGLXJTPJTJLC9JYHMFBKYAUJRAMHQHKUUZHRWZIVC9KFEEXXVN' + b'EXJRYUSFV9PEPFUDCNRRTSCZXSTUEGJKDV9UCYNZSBRDYGOKFGYKWVFCYSWBUJYVGE' + b'UXWTDGPWTWURH9RKEZRFCUUBFBPKSFONMDXWGYKWAUWVUOQVBIGQMMKQVDYAZ9SVFI' + b'UUNMHOJGRQVXZGIIPKVNNBKABGKZLRNFK9KSIHTCGYPVCWYGDS9OIZWLNINYRLGJQC' + b'UBWYMAVDWFAURLALQPMRMFRAZCMCPOWM99SGBVEZPAFAXHXNENNWXLF9ZVHZIDWBLF' + b'KVWKBUYNBXOXTVPDWAGZXIOMDAEKNMRFGZVIGIFOSHGMPIPWNOWQDMHPKOJTYYECKN' + b'GCDDTJVALGPZSX9IH9LEGQSDACLBWKNXUW9BAZSHAISUJDTPJDOASLVRXFNJJHXQTK' + b'MKZUZIMJFPOKHEQXSCJQH9JPRNZHDVVZKWTHWWFNFMHFXPUIEEA9HPHJTCJJWZPUHK' + b'AAWJQQSAIF9HRETYYPXAZ9YCFJRCXTGCOLJQA9HDLFNTVDMYPRCYPQR9MNBBAMGOJX' + b'PRFCUSIIZN9VROZDPMOKZBCILKGB9EPCXOYWLPHFXTYBCMLRVHWNQDSQUIHHTAUTZC' + b'JFQ9CO9GTONKYKMDBSREZC9SUBHYK9JDOBYDBUBUIO9TRXQLAYHDDSXGJ9NB9FKMUU' + b'US9GANWVMQLIHX9MPJGLTAOMCZTQYDYVOWXHGHYCV9VDCXHGTCOOUEXIITVKHXCSUS' + b'OIRTMEAKMTYZCMAWURNX9JOVDICICKHXQYBXKWTXWXBZVZWRIDC9YCZVSKYIKJYYMF' + b'YQRTWBNJHWXRL9JFSZAXJYYTGDYLTHLWRMBUEG9QTGNRPVTBGRYFPEJQSIWTLPGV9C' + b'CMCO9TCKLKSJEAMFKQMXEYETISVEYDOSCRZ99RFDPUQPHMQ9NVRUBXITDGFZCYQNFC' + b'SULGRHPONWJDVWT9UELEKEPQEAFKDLDNYPABC9GUASVFJBFZF9Z9CHIUNLJWHKGDYK' + b'ADLUCRNEPAIWYSX9LT9QWQRKU9WEVDPKSTSA9PPEVNTBNLN9ZOPETINXGKA9DCOHPD' + b'QMMOOOCKYVEZJ9ZJQRJHNCKRFDRPHUVPGVGQYKZBLOILZTPIX9MIBKTXOJKVAYRLSX' + b'DTOEEKLF9WWZGLSGIOQZWCJJHSBTXYWRDYVEQTCNUENYWDRLZZIVTGCXEAJDRY9OVM' + b'XJGCSQSGYFLGYDZUH9EHUDQTCXLSDPMNDYQRZYRXYXKY9GIYOSIDQPXXHKJKDQLSCU' + b'Y9FFBTPSTJFEFROCEXFFYTFYHQROAVTYKQOCOQQWBN9RKJ9JJEURKTVOECYRITTYKN' + b'OGCD9OPQ9WDMKRPIUNRAVUSLFMC9WZWHSESGLDUYHVPAX9YJOFTTFSKFHTOOQQRCPY' + b'ZKTDVCUZGBOBZKLVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VDFKWVEWV9WGXPJH' + b'UBWYXGECBPQOPOHG9YCVXDWOXTEAOFBCEEAV9JCHUVLIRIMHXMUSZPOMMRBF9PLVLR' + b'JYTXTBANBZWFQWGNGFGXFOZ9YGMQSZFEJHLFZTTVHRLJPATA9TYCM9LSEWMNEUDNWQ' + b'FLUXOFUNVDKSNIIXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVG' + b'XBBPBEBDVGZDBWMDMLPXYJBBRNOMKGPMCG9FTSLMRADFVPUTTEIOUCBLPRYZHGOICN' + b'C9BT9WHJJJPDOSOMLD9EKRGKYUHUMMCAVHGYWOVQXFLTCXAAUDYKGKGKOYHLDCCQSK' + b'NHJHPSXTJVTW9QPFOQ9FDZIDDKIVF9CDYGU9ABRESMDLIBONAQWFVGCNOTEDHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHVIT' + b'GYROZZNQP' + ), + + TryteString( + b'ZOJNUMZOBEHLYDSDAVZKXHF9MAHAJICBMJTZZHTQTCACVQAUSSCFUMGCSJTONNKXFI' + b'NPOAXQIKSJ9GUV9GXM9KYDCDWUHULIJMSKMOLDZBYE9FTGFMKLODKHFF9YUCPTYFFM' + b'9EDCJDCKRFLZUHGGYNYFJLBFWXCIUF9HMGUQKPUCJ9OQ99FXHSUSRRBEUSSCKCYPIE' + b'AFZJQNXEUYWLEXKZWLRINBEGAZTJMYTUEQTTORMIIQASISHSHZDQJXANFLKOIRUEJU' + b'PZZHUJFWHEXFIZ9OU99SQLDDNLARDFPGYSCMXQCMGPRB9QLM99QUBLTLTKWYXHVAFU' + b'VVAMHEYCCNVEITSPVQWMSEIZJSLPWNGWISKWQNXCNRNOIGRGUHGYWLOFNXBDCT9JLA' + b'9CEKW9BFGOESKGOQLJBTLUMOICBEZDHCR9SZCJUZVXIEAVITFJFDGNJII9LSW9IQKV' + b'99UJWWAACGIRPCZUENXGILUXCMJIGW9REUNA99MWSANWL9KVKKXCKXLRGDT9NXIGQV' + b'ZWG9NBQPOQKEEET9ZUSENFPGFDNNHGBITCPASGHOPBNYKKEHKHVATNVWX9ZGTISUKP' + b'KTMWMPCGVVJSGMRJWNFICSFUAVAHIZWA9PDOIXFJGWCPTZHUDDUFJVQPBYNJREQ99U' + b'HOESTT9FELDMVK9VHZYPRVOWEW9NXTCYDCIMT9UIWGXUFYILOPOCJFVVEJEJN9ULGX' + b'IABFJWWRKAD9NHZBULMWUKESZLCPRQVVKWOHEWSTLOFNA9KNERURWJPROBBXEWICDK' + b'KCQXWYMJUCQLWEUPFXRSNMIJWQUEJUNIKDYJILXCGCLFETWOZYIUZVJVYVB9YGXSSD' + b'XYXSJXTOQZ9CCCAKMCNNKQCYEDGSGTBICCOGEHRIVMICUQPUUFRFCBF9NUUWSQBTVI' + b'YFVWAASTQJZFDDWWUUIHPKTIIVAGGIEQCZUEVOFDMQLDESMQDPQUSOOKZJ9QLXTAFP' + b'XXILFHFUIFJTKSEHXXZBPTZUGLYUZNORFOEKQDEIWGXZPBXSOGGQFILUJTKDLWVKPV' + b'ISU9QOATYVKJHLDLOKROZNFAGS9CICXXIUQQVLLRPPPDYJVSCW9OWIHKADCVSKPWTE' + b'NYEWQWEHP9DDWOUJDWSTSOGYQPALFMKCTUGLSXHNYETTMYTS999SYQVQSPHQPKRJSU' + b'Y9QTABAJOJAAMGVBCSLAAOBXZOJZLIFXUYOVXBKHPFVTKKGSIHUXMBDTMGNVL9NXYC' + b'HOVTLGDICIWTCIGNRHLBZBVSXMPBFAWIXPCDJWNDUFHUVLBSPBWICZNYIUJPRRTOCS' + b'SCVPNBXEDCMHKFVDMHJTSP9JI9BXTD9ZILEEOCBMHCQRRDNL9EUKJGJ9MPQGQU9ZFY' + b'GVSNOYAEC9NWTCVEJBSXLYWTUPMXNAAWXSBIAJYSGYHGLYOMAHFTYMICZRDZTQXHAQ' + b'GVXENKIGW9XZTPBAIMZLHWAJCGY9ZDNQOTGDRCTXSJCEJVTTMVRYYKWAFYSV9WVEVC' + b'FAXJKJNUC9NQHPEXWIOHOJQEXJNLEW9GLO9AJCJXIEXDONOGKXFJ9OXXXETUEHLBXA' + b'JGFPHKAQDCRTKQBXPZYQZBQODTVIBUTSAEXMBFBMTAXOQZCOHWEWRJEKNKHZXXSO9U' + b'SZRWUPZAASWDBXOVAEGSAGYDIOZWSSEAIQVRWFDSOXSRRRQHRCWDJWZXXJOGPZRLKQ' + b'OA9DOY9RXZNWBFJTKUOVRRQNSDUOFGCUQNHOBMJSFQZXVBPHHBRRIXZNLXAH9P9EFM' + b'GRPGSCFRZINEPOQPXPKHTSRJWARXRGJGYMTPUKQISLV9GUC9VTJLOISKGUZCTZEYND' + b'TURLBPXGNQLVXHAHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHV' + b'HE9EEBQYSNWECSPAJHGLTEUCXALBRVTKMWSWCBPUMZFVSEEFIHBAGJVVQV9QLFEGGY' + b'VPNSDOBZEQGLEFLCQVPDJA9MQDRHYNVZVNTYNJ9GJCXKED9NEWTD9RVMNA9HOHUBLL' + b'ASNQSDLDZKOMFOEGBJZPYVYZCVHYFEGSVEHSWV9WAGMEQIUDZQZUACWYQLTD9LHBVK' + b'KNXXXDWQUWRJKTCDP9CEJOHLLPTWCIKKHHIFAFFDVMFZR9A9LYVMTQAPAXAVPJOZKW' + b'FQNAJTO99' + ), + ]) + + # noinspection PyUnusedLocal + def _create_signature_fragment_generator(bundle, key_generator, txn): + return mock_signature_fragment_generator + + with patch( + 'iota.transaction.ProposedBundle._create_signature_fragment_generator', + _create_signature_fragment_generator, + ): + response = self.command( + seed = Seed( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' + b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' + ), + + transfers = [ + ProposedTransaction( + value = 42, + address = Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + ), + ], + + inputs = [ + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', + + key_index = 4, + ), + ], + + change_address = + Address( + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + ), + ) + + self.assertDictEqual( + response, + + { + 'trytes': [ + # Change transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJRBOSBIMNTGDYK' + b'UDYYFJFGZOHORYSQPCWJRKHIOVIYQB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99C99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 2 of 2 + TryteString( + b'ZOJNUMZOBEHLYDSDAVZKXHF9MAHAJICBMJTZZHTQTCACVQAUSSCFUMGCSJTONNKX' + b'FINPOAXQIKSJ9GUV9GXM9KYDCDWUHULIJMSKMOLDZBYE9FTGFMKLODKHFF9YUCPT' + b'YFFM9EDCJDCKRFLZUHGGYNYFJLBFWXCIUF9HMGUQKPUCJ9OQ99FXHSUSRRBEUSSC' + b'KCYPIEAFZJQNXEUYWLEXKZWLRINBEGAZTJMYTUEQTTORMIIQASISHSHZDQJXANFL' + b'KOIRUEJUPZZHUJFWHEXFIZ9OU99SQLDDNLARDFPGYSCMXQCMGPRB9QLM99QUBLTL' + b'TKWYXHVAFUVVAMHEYCCNVEITSPVQWMSEIZJSLPWNGWISKWQNXCNRNOIGRGUHGYWL' + b'OFNXBDCT9JLA9CEKW9BFGOESKGOQLJBTLUMOICBEZDHCR9SZCJUZVXIEAVITFJFD' + b'GNJII9LSW9IQKV99UJWWAACGIRPCZUENXGILUXCMJIGW9REUNA99MWSANWL9KVKK' + b'XCKXLRGDT9NXIGQVZWG9NBQPOQKEEET9ZUSENFPGFDNNHGBITCPASGHOPBNYKKEH' + b'KHVATNVWX9ZGTISUKPKTMWMPCGVVJSGMRJWNFICSFUAVAHIZWA9PDOIXFJGWCPTZ' + b'HUDDUFJVQPBYNJREQ99UHOESTT9FELDMVK9VHZYPRVOWEW9NXTCYDCIMT9UIWGXU' + b'FYILOPOCJFVVEJEJN9ULGXIABFJWWRKAD9NHZBULMWUKESZLCPRQVVKWOHEWSTLO' + b'FNA9KNERURWJPROBBXEWICDKKCQXWYMJUCQLWEUPFXRSNMIJWQUEJUNIKDYJILXC' + b'GCLFETWOZYIUZVJVYVB9YGXSSDXYXSJXTOQZ9CCCAKMCNNKQCYEDGSGTBICCOGEH' + b'RIVMICUQPUUFRFCBF9NUUWSQBTVIYFVWAASTQJZFDDWWUUIHPKTIIVAGGIEQCZUE' + b'VOFDMQLDESMQDPQUSOOKZJ9QLXTAFPXXILFHFUIFJTKSEHXXZBPTZUGLYUZNORFO' + b'EKQDEIWGXZPBXSOGGQFILUJTKDLWVKPVISU9QOATYVKJHLDLOKROZNFAGS9CICXX' + b'IUQQVLLRPPPDYJVSCW9OWIHKADCVSKPWTENYEWQWEHP9DDWOUJDWSTSOGYQPALFM' + b'KCTUGLSXHNYETTMYTS999SYQVQSPHQPKRJSUY9QTABAJOJAAMGVBCSLAAOBXZOJZ' + b'LIFXUYOVXBKHPFVTKKGSIHUXMBDTMGNVL9NXYCHOVTLGDICIWTCIGNRHLBZBVSXM' + b'PBFAWIXPCDJWNDUFHUVLBSPBWICZNYIUJPRRTOCSSCVPNBXEDCMHKFVDMHJTSP9J' + b'I9BXTD9ZILEEOCBMHCQRRDNL9EUKJGJ9MPQGQU9ZFYGVSNOYAEC9NWTCVEJBSXLY' + b'WTUPMXNAAWXSBIAJYSGYHGLYOMAHFTYMICZRDZTQXHAQGVXENKIGW9XZTPBAIMZL' + b'HWAJCGY9ZDNQOTGDRCTXSJCEJVTTMVRYYKWAFYSV9WVEVCFAXJKJNUC9NQHPEXWI' + b'OHOJQEXJNLEW9GLO9AJCJXIEXDONOGKXFJ9OXXXETUEHLBXAJGFPHKAQDCRTKQBX' + b'PZYQZBQODTVIBUTSAEXMBFBMTAXOQZCOHWEWRJEKNKHZXXSO9USZRWUPZAASWDBX' + b'OVAEGSAGYDIOZWSSEAIQVRWFDSOXSRRRQHRCWDJWZXXJOGPZRLKQOA9DOY9RXZNW' + b'BFJTKUOVRRQNSDUOFGCUQNHOBMJSFQZXVBPHHBRRIXZNLXAH9P9EFMGRPGSCFRZI' + b'NEPOQPXPKHTSRJWARXRGJGYMTPUKQISLV9GUC9VTJLOISKGUZCTZEYNDTURLBPXG' + b'NQLVXHAHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHVHE9EEB' + b'QYSNWECSPAJHGLTEUCXALBRVTKMWSWCBPUMZFVSEEFIHBAGJVVQV9QLFEGGYVPNS' + b'DOBZEQGLEFLCQVPDJA9MQDRHYNVZVNTYNJ9GJCXKED9NEWTD9RVMNA9HOHUBLLAS' + b'NQSDLDZKOMFOEGBJZPYVYZCVHYFEGSVEHSWV9WAGMEQIUDZQZUACWYQLTD9LHBVK' + b'KNXXXDWQUWRJKTCDP9CEJOHLLPTWCIKKHHIFAFFDVMFZR9A9LYVMTQAPAXAVPJOZ' + b'KWFQNAJTO99TESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVW999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99B99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 1 of 2 + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWG' + b'HMNUXBJTOBKZFDNJEZUKCKWGUHVSU9ZJYAVSQSOFDCOIEP9LCXYLTEFMCYUJAAHL' + b'YUHQP99S9XRWHXHRPZCWHDMIDYW9OQAWUPTFMBTJGDCWRVNVRDPIWISVYNUDWUGB' + b'PNNFZDWRVZ9FGAVSEWFXRXGGLXJTPJTJLC9JYHMFBKYAUJRAMHQHKUUZHRWZIVC9' + b'KFEEXXVNEXJRYUSFV9PEPFUDCNRRTSCZXSTUEGJKDV9UCYNZSBRDYGOKFGYKWVFC' + b'YSWBUJYVGEUXWTDGPWTWURH9RKEZRFCUUBFBPKSFONMDXWGYKWAUWVUOQVBIGQMM' + b'KQVDYAZ9SVFIUUNMHOJGRQVXZGIIPKVNNBKABGKZLRNFK9KSIHTCGYPVCWYGDS9O' + b'IZWLNINYRLGJQCUBWYMAVDWFAURLALQPMRMFRAZCMCPOWM99SGBVEZPAFAXHXNEN' + b'NWXLF9ZVHZIDWBLFKVWKBUYNBXOXTVPDWAGZXIOMDAEKNMRFGZVIGIFOSHGMPIPW' + b'NOWQDMHPKOJTYYECKNGCDDTJVALGPZSX9IH9LEGQSDACLBWKNXUW9BAZSHAISUJD' + b'TPJDOASLVRXFNJJHXQTKMKZUZIMJFPOKHEQXSCJQH9JPRNZHDVVZKWTHWWFNFMHF' + b'XPUIEEA9HPHJTCJJWZPUHKAAWJQQSAIF9HRETYYPXAZ9YCFJRCXTGCOLJQA9HDLF' + b'NTVDMYPRCYPQR9MNBBAMGOJXPRFCUSIIZN9VROZDPMOKZBCILKGB9EPCXOYWLPHF' + b'XTYBCMLRVHWNQDSQUIHHTAUTZCJFQ9CO9GTONKYKMDBSREZC9SUBHYK9JDOBYDBU' + b'BUIO9TRXQLAYHDDSXGJ9NB9FKMUUUS9GANWVMQLIHX9MPJGLTAOMCZTQYDYVOWXH' + b'GHYCV9VDCXHGTCOOUEXIITVKHXCSUSOIRTMEAKMTYZCMAWURNX9JOVDICICKHXQY' + b'BXKWTXWXBZVZWRIDC9YCZVSKYIKJYYMFYQRTWBNJHWXRL9JFSZAXJYYTGDYLTHLW' + b'RMBUEG9QTGNRPVTBGRYFPEJQSIWTLPGV9CCMCO9TCKLKSJEAMFKQMXEYETISVEYD' + b'OSCRZ99RFDPUQPHMQ9NVRUBXITDGFZCYQNFCSULGRHPONWJDVWT9UELEKEPQEAFK' + b'DLDNYPABC9GUASVFJBFZF9Z9CHIUNLJWHKGDYKADLUCRNEPAIWYSX9LT9QWQRKU9' + b'WEVDPKSTSA9PPEVNTBNLN9ZOPETINXGKA9DCOHPDQMMOOOCKYVEZJ9ZJQRJHNCKR' + b'FDRPHUVPGVGQYKZBLOILZTPIX9MIBKTXOJKVAYRLSXDTOEEKLF9WWZGLSGIOQZWC' + b'JJHSBTXYWRDYVEQTCNUENYWDRLZZIVTGCXEAJDRY9OVMXJGCSQSGYFLGYDZUH9EH' + b'UDQTCXLSDPMNDYQRZYRXYXKY9GIYOSIDQPXXHKJKDQLSCUY9FFBTPSTJFEFROCEX' + b'FFYTFYHQROAVTYKQOCOQQWBN9RKJ9JJEURKTVOECYRITTYKNOGCD9OPQ9WDMKRPI' + b'UNRAVUSLFMC9WZWHSESGLDUYHVPAX9YJOFTTFSKFHTOOQQRCPYZKTDVCUZGBOBZK' + b'LVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VDFKWVEWV9WGXPJHUBWYXGECBPQO' + b'POHG9YCVXDWOXTEAOFBCEEAV9JCHUVLIRIMHXMUSZPOMMRBF9PLVLRJYTXTBANBZ' + b'WFQWGNGFGXFOZ9YGMQSZFEJHLFZTTVHRLJPATA9TYCM9LSEWMNEUDNWQFLUXOFUN' + b'VDKSNIIXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVGXBBPBE' + b'BDVGZDBWMDMLPXYJBBRNOMKGPMCG9FTSLMRADFVPUTTEIOUCBLPRYZHGOICNC9BT' + b'9WHJJJPDOSOMLD9EKRGKYUHUMMCAVHGYWOVQXFLTCXAAUDYKGKGKOYHLDCCQSKNH' + b'JHPSXTJVTW9QPFOQ9FDZIDDKIVF9CDYGU9ABRESMDLIBONAQWFVGCNOTEDHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHV' + b'ITGYROZZNQPTESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVWVX9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99A99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Spend transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9' + b'RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99999999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) def test_fail_inputs_explicit_insufficient(self): """ From 9ede12a69c795c6b4aabc72aed7c6b2db5e80959 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 14:59:27 -0500 Subject: [PATCH 209/239] Verified `prepareTransfers` w/ explicit inputs, insufficient balance. --- .../extended/prepare_transfers_test.py | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index 3bddf1d..b5d2633 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -9,7 +9,8 @@ from filters.test import BaseFilterTestCase from mock import patch -from iota import Address, Iota, ProposedTransaction, Tag, TryteString +from iota import Address, BadApiResponse, Iota, ProposedTransaction, Tag, \ + TryteString from iota.commands.extended.prepare_transfers import PrepareTransfersCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed @@ -1328,8 +1329,42 @@ def test_fail_inputs_explicit_insufficient(self): """ Specified inputs are not sufficient to cover spend amount. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.adapter.seed_response('getBalances', { + 'balances': [30], + 'duration': '1', + + 'milestone': + 'TESTVALUE9DONTUSEINPRODUCTION99999ZNIUXU' + 'FIVFBBYQHFYZYIEEWZL9VPMMKIIYTEZRRHXJXKIKF', + }) + + with self.assertRaises(BadApiResponse): + self.command( + seed = Seed( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' + b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' + ), + + transfers = [ + ProposedTransaction( + value = 42, + address = Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + ), + ], + + inputs = [ + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', + + key_index = 4, + ), + ], + ) def test_pass_inputs_implicit_no_change(self): """ From 691f3ae7a9589c0f1d9ca87b3a8647bd13eab3cf Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 15:11:36 -0500 Subject: [PATCH 210/239] Verified `prepareTransfers` w/ implicit inputs, no change. --- .../extended/prepare_transfers_test.py | 462 +++++++++++++++++- 1 file changed, 460 insertions(+), 2 deletions(-) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index b5d2633..bc2cf64 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -1371,8 +1371,466 @@ def test_pass_inputs_implicit_no_change(self): Preparing a bundle that finds inputs to use automatically, no change address needed. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + def mock_get_inputs(command, request): + """ + To keep the unit test focused, we will mock the ``getInputs`` + command. + + References: + - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` + """ + return { + 'inputs': [ + { + 'address': + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', + + balance = 13, + key_index = 4, + ), + + 'balance': 13, + 'keyIndex': 4, + }, + + { + 'address': + Address( + trytes = + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + + balance = 29, + key_index = 5, + ), + + 'balance': 29, + 'keyIndex': 5, + }, + ], + + 'totalBalance': 42, + } + + mock_signature_fragment_generator = MockSignatureFragmentGenerator([ + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWGHM' + b'NUXBJTOBKZFDNJEMAOPPLR9OOQJCDVO9XSCYQJQVTXQDYWQEBIXKDZAFWINAHJELJT' + b'DPVMUEWSVCJA9ONDYBNANWCGLBQMEMTBFDMWLCMQHGJLGYDQGIMLSNQHBGSVTDZSGN' + b'QAL9OHRAPDKYSVBTNYRUUBNEEAINJMOVOHOWXUAIEDAIQDESQFCKJELHAVODSMXMKE' + b'HTDKCDIWWISXSAHQE9TJTLJZGXIABHU9CUACMLVSSYV9UJREPWFVYWWXPYYJRP9DOE' + b'KNDMBSBKKHIFMPXZXIJERXRZVBVDBYNZBBCCOSEDOLDGSNQK99HIYSWNYYEBLRT9MA' + b'DLXLLZJOSZCFWAVZY9XUPNZUVOSKBMKXXJNRKDBOSGUGME9QNBMHIWXWXPEEUVQAQV' + b'UXDJGMJOBXG9VJBWPRQRCCQSNBEHTLGOKJVYEPQOJO9QIZLYAVLCKVXKEKRGBSZJAC' + b'9KTSSNMDQGKCLPZDJAQ9PBQMLUONVVFAWTMREGFXJMRRGL9MKNPOZGOYRPDCYEJCYJ' + b'UN9HYNSNHXARMRJVXBUHOP9K9BIIEYGSHBUESKTAOQOEANEAIHYHVGSVNPXWRBTJAM' + b'KMWEQOSYEWXLSRYVOSTMPOGYNPDNFLOICXVHYBDHSXVRKVWNVZOZQDOITZWICSYEW9' + b'RGCPPUJYVIYVTSZILYENYUYUGDSGWVYWRMZJNCTTPVWDWXAPVZQQKI9CGEQPBFPCLG' + b'DDEGBUUTISNCMJXQCTUNKQTLCATNOIRPMEUQBQTHHQYRGDLZEUZBALNQDXJYZBVXDP' + b'LVOVVAUCQSCGRTUJRBBNRV9ORETTGFIXBBBVOPFHPKGPKVBYFTZMWUVZYVWWSDKQVO' + b'NMPLLQTV9IZUWLUWZNLCVJNPMG9CMXQG9D9WYCANBRMYV9DU9FMJT9JHT9RWCGLHFC' + b'ODXJVFQBLTKJWVNVGSUHNWLHNPLZDSWDMDVQTLVCSVFJJTIQZFAPCXJWDAXWJKJVOK' + b'HALCQQTIXABPFXPUFK9IKXYUGMPXNSQCJDVETOVEX9LXYLXWRW9PFEYJCUJHLUB9NX' + b'TUGLIQMDGPDPSJTWDYEWXQAICLN9BTGNBJWLVAXZGNCYXGHBMRUVVYTJGH9XDGSZHQ' + b'DYKFGMOWORSFDFBLJHBRTXRSEBALCJIJTQJYDZZKWZGVAPFVKVEOXGYRLMBSPFHUIJ' + b'ZZFMFVOTLPUWSYZCWFZMAALHRGSYSXSMOHWARYZZVIAKXAHGY9SROWPVFACXXLQEXX' + b'OJCKXRRZHBZXJIBWQMMZTRDFYQBSBBZQQXGCAAECMQINHJRBSGOYPCGWKPWCHBKOJT' + b'IGDASZFGONTUGDSOOLEMGOEBFCZZJZSCGXPHXHB9WGMMFVUTCHDBSAMYTECQZWGCXA' + b'WTCTIBZHQVUAIBPZHBBTZAERYU9XAMKBHCHGZISSPOWJIRZTAXDHMAYBPXOXWDIUDH' + b'NBTFJNVHHJO9AWAEC9UPRRFJLNGKTXJXFDGODDOPMGLALRIJBVIFLQTYQPKCKCRBYP' + b'BYGUUFJGJFVCOURNKCGNTQNNKHDDPIVZHCJSLDUYHVPAX9YJOFTTFSKFHTOOQQRCPY' + b'ZKTDVCUZGBOBZKLVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VZZGUNUCKOYLYXOV' + b'HMGULWGSRCGXZLJVNIMZBLFOJJKOTUREMBXYOZXDUP9ROUVYOSJBGGFZMIFTKHJHHJ' + b'GZJNOYQWFZAHLJWWDDFQQAMEGJUEUSIWOHKFJWRXRSJWYPGIGZGMFNAIDGDOUUQUVH' + b'JZQPJMLCGKGADXAXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVG' + b'XBBPBEBDVGZDBWMDMLPXYJBBRNOMKGR9TSVUXSRYXQTCTYLFQORMIGDKBJLNLCQXAC' + b'VCBJGVWRJNYPCKOAILPLMWBYKDLDXLIZMZFWDXUWDEGDUURQGMJNUGJXDXYJGKOTQB' + b'GCHATROPKEN9YTXDUOCMXPGHPDANTJFRRVEVBFVCNTWNMMOVAVKBNSJIWWBVHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHVIT' + b'GYROZZNQP' + ), + + TryteString( + b'SWHZKSNCOQXPCGRTYJPUGKLBNEJFXASKY9XAUROGDAO9QQLIVRZQDJDTPLNTBGUUFG' + b'ELJPSGUMGPIUNCCTQEFU9UZIJJYJXCYWRADRHHXKBEDG9HTCHJHXUJRKMIUFOSKDGM' + b'I9QPCYQSWDCUYKQQEONJEKYWELG9MSNBHRHILGSSKMRCQJBOGNYBKEMTOFEUBEOUBD' + b'9ULP9PHWYKXEQNDMUR9BGDGPEUFRFRGNGFJFPYQXABSDALTYKL9SM9VVQCOHY9AS99' + b'EYWSHUNEQVVNLS9CNPEVMPOKMWQYFPGTNJBZCDWYPFWSBKZOYXNNVMPODEHMHNIYZC' + b'HIEDEAB9TLFOWVHF99GVRWUZWSN9IQOKWIXERKRQETZS9ZJJSQRLLPQXEWNMFVVBWO' + b'IK9MBYCEGUJ9HJRIIMBVJNGXMGPGDLOLYWFVQNOKTFZRBJSSBJTETGAIUGZOYQOFTV' + b'BKAQY9SSJWJXXYAUVUQWPXVCISFSDSHDQCPVMG9GVDAO9GIMDHZWJOKSUEUFHBGSCZ' + b'KNBTZWJXSFYNJSBSEL9UMZBAZRGYCEHOSJBMKMPMNXKEVTMUDEFWBIKOXUSBNPTNEC' + b'GVLYSOGUDJDPHYFADXRAOLQXJSJDANINJEOMCFAWWITREGCDF9OZ9ZKHPJZJNMOVGX' + b'9OKQBSGVZYWKNOPVJEOZEI9BPE9GCUEQVAHSBBRBGQTEXVZCSL9ECOWPOWZCVSCBOU' + b'SNQMTJIEKHXL9NCPRMLRNKQEHYJCLRHGZKFNBJIPKSKPRFTSKFJULTBTXFDQHWUYOS' + b'DQBHPAINVEPKCCHJDTZOJIGJZOF9AEQDBKCZSZMIWUUVHVGAFKALGITVQQKBAHKCIF' + b'SVMVZ9UDQABVIANTBUQOFBIXQBWB9KKQOVJZNVBEDAZKUTRNKGJQWMTEKV9KGCIBRD' + b'CBAPKSTMCZGUV9HTAABQDKGQBCRFNXBMZRTHF9MO9GAGQDYDVLOFMDE9QQZYR9GDSB' + b'LUVVMKMCZIMDPNCVLGDKBACWQJRWOQNKBTSDJFKQMKTVKXVNAHRHZALJGVAMXWJYRA' + b'KTEJFXAHBQGSYWWQVECQYPXVFWILNFZKGGRIFCJBSIZRDJXRJHSURPCZKOWKLFRUMV' + b'ENEGMNKUAOGVUECDSGAZNQZKBJDJPVBXLOTID9QLMFNGIWKAAIQTJJROSZBXPQRXAU' + b'CV99OGCEOTQCJ9II9ASZL9XGNSVUXVKPXYOJMF9PX9GSLEROR9FXVQ9MLEMEW9IWNW' + b'BNVAYXZ9ZETTDSMLGZAKHE9IUJBFUHXW9KWCNZOZCCTFGBGWSDAQGGSPSQHOMUVJML' + b'WBDAKYQZMWPQLLYAGUMOVMVLFD9TO9OUBTVUHHUNSFSATSEGBFVGDZUBMTWWFDPSQV' + b'CUFRVKHYYPDWRPNSKXRFTVEIBVZNGUZRQCPXVKBPKQDDLEBWIEBIPTEJIYFHBXCUVC' + b'CKTKEJAYRZCKAXLMELINWUZHG9JFBSBAKHIXMWHUWUFHFNLXNO9GKINYKRTCNN99PH' + b'PHO9MJAGUYZAPNSPWUZ99E9BEADKETLOALWNANYMHSLLQSBS9YTYVJKTVWFUVS9MFO' + b'WCHLEUUFUWTYGLZXFDUXVABTVFXFPUEPIUEIAVSZSSZQJTHNGKBJXADRHVTIBERILM' + b'CCGWUITYQHGEEGWIZZOII9B9EVVVFJNEYEWH9ZVOJGHKVPYKDEZZSPBAOBQGGWPWXT' + b'CKSLSHJQYCDHAYIQ9QVSQFPXZDBYSJJKSNTRXORHLPVOYVMIGALRPXYWQWSJPPFTJC' + b'YXAATLBFNSGVXKFJXHYTILNOQZROUCDUTWOMGYBVTWPKJY9RVKKWQQMVCHJEUBELJD' + b'KJPGYLXUXATNOIJHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHV' + b'HE9EEBQYSNWECSPAJHGLTEUCXALBRVGXFENUCOONSUFZLHTLVQNPDZDIVDQHWVLDED' + b'PFQLJZWF9GFZMPZXFVEQECLUZBBFVSAPEXJLKKOMXEPHZAKP9WYTGQOML9FQSBMSFL' + b'OGRLFQKUCUWFX9DNAOZSSKBUV9IBVIRNUWYBKJVKLJ9PPNLGJATKDCAGVFIVPXRABH' + b'ZVZACJIG9WOKKLFCRDSMTWSCYHOZEEXRIMPQBXVXQAYKZIADSM9GUBICGKGQYNHKVY' + b'OZFRVCHNM' + ), + + TryteString( + b'KJVG9EKTMPWE9PKWGGJJPDISCX9CJXGWUUPOLKKBVUWUYNBACOOF9LEQGNM9YYGNXJ' + b'EMOBGSDCPLP9CQIFBCLENUCJGCCSWYU9WVFTRZZCPCZXEGMDDPSYDTYUIMVFSLGHTA' + b'ZWJRHY9ZQMFGROIIUIQVDSOTUIRMSROYDPTWRURMZAILGBWIVADPYHPTSFGKAPPMVT' + b'THGLYXZHPFUO9HBAJIOUJOOABAQSOQUKLSVQGSDIHEHGTQSPM9EDHLEQSFFAAQXR9M' + b'UREVQ9MEGXNXMNJVWXYEIRYAOFHDFZAKNVWKTVIHKXWVT9VOZRUPUKXASIFAZQVZSW' + b'HBQU9RGVLJMRVQCMCYSQEIMIAKOHNAKQLTIMEHMZMGAKCIPGHQTWORBLVKISGPKIIM' + b'AMQWMZUNTKJSQZAZNYEGORGNRTKCLNRSOQJRBUCPSDLKLGGRBACIULLZBFBUNQXACK' + b'L9WFEKKAHGLBBRNNEXZPPH9UZESFFKVBOPROFHQOKYAVTJDDVAUGUAURHLESIEIITD' + b'VVRCTRKOGUPERJHNJMXTLNVMWVDZITSPEHRYJKEZVTZSJEYTOQEGNJRMCJLYYKPGDF' + b'UFQHGWRDGEWBXYOGEZ9IXRWJAQLKHPROWIEVI9ILNOXTPOSRLETMNEQ9P9WLXCUZNM' + b'GFK9EYHABBCSEZSGMNJZOEEGRVNU9ASSOOLCXXZKZPFWU9EEUUQRACVGZPL9MQINGL' + b'YPUTUPTLPKWPHRFFBRHZQWIVOXPGAKCQQPRKPPZUHOJISYASMRYMCMJZNR9D9OQANU' + b'XGJXSUSZQFWDJUTNCDKAUAFYKJNVAMBLTPPRPIJRRKQMCIHHGKPQPUQHWJNIEPDLRA' + b'YSJXVSJVKAGBAJCMGQSCZFTEJSG9LUWZGFBGQUHFUHWDHND9WJBPOQQXDEATOBGXDG' + b'M9BKSDCOEZ9IENZPPDUPMKCUKYBIBTBMJPJLDNSOPEKHVGQKLGUISUFSYMHR9I9LRP' + b'LCXJTDHHEXKQEVIFOUGKJEILQIHFG9FWOUBXRHCRHLOYAXTFQUWKJBSX9GNPCWXUQJ' + b'RHDBOBRZPQAPMKCIZGULPZDYLLBMAFJZXGIRVAAVUUCSNGDGJQJTAPV9QXYIABIHBX' + b'ILKQLGDGXQUVADQGDFKKDKMU9WKBEEY9TAVRYQDQFKPDMLMUAEGBHVJPSIZOEQGCSY' + b'NJCICXROXHPZFUXASQJXZEHQTEUKFIYQIGJWORKAIQUFROYGMIDFAJOUFAYYWMXUGJ' + b'FPSRTGEUWWLOXEUTKZCZQHWFUNHTMZVIJ9VYOLBTAIFB9EN9NFVAABVFIBIWXLJSUO' + b'YELOQSIPK99AXSXCPECWOXFUVDIANVO9PKZUESMFWIEVWLEHLCVKDXEROLNEMYRRCJ' + b'DPAYVTYAYSL9AFZH9GXHXZORXZEQTUJEDJGCYCQAENYZRKDJSK9TOCKKCXOSSTOAIO' + b'9UVAKQJBVOS9RUQIESCIJYRWYRUPMIJEHR9EGZ9YMHQXALUUDMCFYFOMLIGORMMBCD' + b'JMFCNNELGPXHICRNRKONBKACHLLSABUNHQ9TU9OSSTQXGWBLRRTSKZORXILALQYRXD' + b'DMXPPUTEGTVCHSOVYZEEJMRRECGBMXBORUTIQUNMJDXBSZSYYA9UOTFWMQOHURUFSU' + b'ESLMILBBKGHTTFTZONNQIMJSLILKAQJRDTNVK9PHAMNKZXRHSOPGKKLJBRDYAC9BRU' + b'RJWUIJLUWXNQOSVVLFEBROMJCGVYZWIPOYFQRBUUNJLIGPVDLADFLZJGZBLEBEQEUD' + b'UZOIFFZLZRXCPQVMIARFLZRIOFFEHVFJZXFQFLCJSEXRPUKGMWBMGXEHIEZKOKGH9J' + b'XAUXACEBLXKLZT9P9NJGXRZWZJAZCNKR9CHRRNCFOLBCSZXKXOIGZHZSTDKTHOWZTE' + b'XWOIZLPEGPKTBRENSCOYBQJSZQ9XPNRORQOWMPGBXKSSLDIUVEAJODEUZJKZE9MBVT' + b'QXWFXXXOG9QGDWMNZEWVWVDZWIFKSWZIDNBYEJP9VBKQNOJEXVPZQTHVUCSK9QCMEP' + b'US9Y9FQPWEACAEBIQSVPJEL9ZBSETINIYMSPIXLADSHTDYRAYUTMXDCABIUUETMNLU' + b'RELTPAGEDNMQZALFWOPAI9WUFOSUTOFUUWFAFRFVYOPITBVSG9IBVNJCOITYMTCCIJ' + b'IZWVPYGQE' + ), + + TryteString( + b'GWLDXDNSEIQCQJKVVFEWPWR99OKSHTVIJCNFEGSUM9DUQRO9ZJUWOOGP9XLABZFDXN' + b'GOXZLWETWXTTBT9KIGB9VOMMTKNJTUUFGJIYZIMHEAEJTNTIIOLLO9VWCYX9JA9RML' + b'SB9COUYKMRZQWJXMIFXCETZWRDXHBBOYLYLURXBELK9YLIXXGHSP9TNNASKDGFVJQV' + b'99CMXRM9VHASOBYBTWIMAJLBRUPZQLDCKOFAPHG9DKVVEFHTZAGNC9KH9K9HIFNLUI' + b'NQFTQTSALBNV9HRWXDGDEBBKIMQCDWVTMPDIVCXHGKDFPAKTSYYJIROENCJOZXVBNL' + b'UIUJHHAXZ9PTMNFGRRCNHQUVEESVSYNSIQXDRKKBMWJOQSMIK9FPHTNAJUYTQ9BLOG' + b'9GZPXHACSPIFCDX9LIVQDISFAVZWQUXP9BROHMGBHFTVWEWCZRPTAMTXXLVLZBT9BM' + b'OSJXAIGYUXICBUGQDOJRMYFWYGLT9UBTKGZZPNDIPNVIHQIBXFQACGYPWTKJSRHVQL' + b'VJAJWFGNFLAJYOADR9XNOAYOLHKEUGWSOCXYJVHWLRRBE9XYLQDYJXYMURFPXTMNHE' + b'EXJGVY9ADSJICXGWOUKYWVMXMWSJQVPKTUQUSCHTREWZNTXBDUJWDVTMXPABBHGYOC' + b'UNFIFTUQTRAVTCFAQNNAAXBCRILNVZGGKEKIUOXBVMXLFNCSHFMH9PYR9DBXFKWIBT' + b'AZLQTMVEGCZLWESPAHZLVTCGUUJXEAPCEYBXGGARHGDODWULDHHMMKEIYRFFEMQTTG' + b'SGWTOGBZYEULWWFINFHGYEDHHXAJASMQCLBKWYXSBIWZLMEZVXUWP999OROQYLOFVA' + b'ZGJIGHMTGJSZNGXFWMMUCGGQXB9ASA9UCVZLVYZG9XBIF9HUAB9HBYERWFJ9IEDMAY' + b'ZSIFDHOX9HRQSDEGWUAODHRNVBQWTBK9JFZBNKBATUXBZOIEHPTFPQXSBGHGOEVMUT' + b'RPSTRFOKHWEUPUSEROZEBSMTSTZSHFTQ9UXYTMDVLAPXSHZGYLPVDGTCGHOQSWJJED' + b'ARRUPCYFHJOSPVSTNEERBJOERGU9TTOW9GSVZEODZOEQZATYADJ9NURBJNTPBYECGG' + b'WP9SVOZAWXT9RLKBKL9TBBWXSDOSXRJHPKMLIPWKXSM9MPNQWEYLDPRLTUUNEFHXUF' + b'CLLJLZIRGUMEJCTIHC9VPS9YPENZPBYKTSBPXIPZHNYZYDPOYRIFEZWOFDYMZTUOMZ' + b'ZHLSLZMTDIMTTXMHHTDLIVRSIDSWJBCDCKEYZPTLLZP9IMNJSRXICEGTPZXVXAVIBG' + b'JMMOUNPUKXHIANUPGJANUHTG9ZPZCBFRMLHYOPFAKGRZSZJARBEEPQZ9TKJRQLXEG9' + b'IOHETGXCMKT9XZUBPMIQWXRRRFF9POXJBXW9NPUIOYNET9CTUWJB9RQDHVIAFLKILV' + b'BDLOYZAKIRHAUXE9ORNAPVXRTUY9CNXAPFPNUADXHDQWGRCVBZMUASLOPAYHLNGNUV' + b'VTDQCSOSTOOILZFXBXUPILJVVDUIRBWQUYNOJX99BTZNYQZGTENKGEKKADMPDWQB9I' + b'CWBWFHKAPRNDGGWOUXDTJKMASYOPYNYPTOEN9EDLXVVUMELPGG9ZLAJXQFTIEA9HRJ' + b'QCJLRUSLBGIWRWRXMTSAYVNHNJCYDSYNBPH9XEI9NFEDANKTZ9RWSCMPV9XVBTBZVD' + b'O9HABGD9VDOIXFMWBCHERKTDPDQFQSVNZLZRPHVZTFTL9LRAIMXLMTEZFAKK9CMYVP' + b'RTGBXGIMHUUVWCHDUUEZMZFMDSUQRVVPHZDUTOTLPSKQEHWNLOXKGGJKHHUNQIJXUT' + b'NYMZIL9UOEKECBSTCRVTVKUWETWPECLAXJWUNXXNRDBR99KJSWCHJBTMK9TSLLKWUC' + b'MMWNABUZLKLCJXHPUWVLIEIHYTZRPTZJTUMDDVEFCDRQYHPBF9WVMATUIQXGWTGAHQ' + b'STNRVZZIPBRPIUOZLXRGEWSUVDXIQPAONF9QPFYIMUEMDXOMFPKKJNGRBNMKXNJUF9' + b'IQIHPEBHSLWQWXJZNEBKCQUSRWOEGMWFZYGHFUUHDBBOBKSTXT9HGOORUQMFBFBICA' + b'HBQNOBVDCZVGZGASCINUGVEMM9LLPWTNWWVKWYIYDIJEKAVBEFPAVMFWEOYMTOHLZV' + b'PRMIINUJT' + ), + ]) + + # noinspection PyUnusedLocal + def _create_signature_fragment_generator(bundle, key_generator, txn): + return mock_signature_fragment_generator + + with patch( + 'iota.transaction.ProposedBundle._create_signature_fragment_generator', + _create_signature_fragment_generator, + ): + with patch( + 'iota.commands.extended.get_inputs.GetInputsCommand._execute', + mock_get_inputs, + ): + response = self.command( + seed = Seed( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' + b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' + ), + + transfers = [ + ProposedTransaction( + value = 42, + address = Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + ), + ], + ) + + self.assertDictEqual( + response, + + { + 'trytes': [ + # Ipnut #2, Part 2 of 2 + TryteString( + b'GWLDXDNSEIQCQJKVVFEWPWR99OKSHTVIJCNFEGSUM9DUQRO9ZJUWOOGP9XLABZFD' + b'XNGOXZLWETWXTTBT9KIGB9VOMMTKNJTUUFGJIYZIMHEAEJTNTIIOLLO9VWCYX9JA' + b'9RMLSB9COUYKMRZQWJXMIFXCETZWRDXHBBOYLYLURXBELK9YLIXXGHSP9TNNASKD' + b'GFVJQV99CMXRM9VHASOBYBTWIMAJLBRUPZQLDCKOFAPHG9DKVVEFHTZAGNC9KH9K' + b'9HIFNLUINQFTQTSALBNV9HRWXDGDEBBKIMQCDWVTMPDIVCXHGKDFPAKTSYYJIROE' + b'NCJOZXVBNLUIUJHHAXZ9PTMNFGRRCNHQUVEESVSYNSIQXDRKKBMWJOQSMIK9FPHT' + b'NAJUYTQ9BLOG9GZPXHACSPIFCDX9LIVQDISFAVZWQUXP9BROHMGBHFTVWEWCZRPT' + b'AMTXXLVLZBT9BMOSJXAIGYUXICBUGQDOJRMYFWYGLT9UBTKGZZPNDIPNVIHQIBXF' + b'QACGYPWTKJSRHVQLVJAJWFGNFLAJYOADR9XNOAYOLHKEUGWSOCXYJVHWLRRBE9XY' + b'LQDYJXYMURFPXTMNHEEXJGVY9ADSJICXGWOUKYWVMXMWSJQVPKTUQUSCHTREWZNT' + b'XBDUJWDVTMXPABBHGYOCUNFIFTUQTRAVTCFAQNNAAXBCRILNVZGGKEKIUOXBVMXL' + b'FNCSHFMH9PYR9DBXFKWIBTAZLQTMVEGCZLWESPAHZLVTCGUUJXEAPCEYBXGGARHG' + b'DODWULDHHMMKEIYRFFEMQTTGSGWTOGBZYEULWWFINFHGYEDHHXAJASMQCLBKWYXS' + b'BIWZLMEZVXUWP999OROQYLOFVAZGJIGHMTGJSZNGXFWMMUCGGQXB9ASA9UCVZLVY' + b'ZG9XBIF9HUAB9HBYERWFJ9IEDMAYZSIFDHOX9HRQSDEGWUAODHRNVBQWTBK9JFZB' + b'NKBATUXBZOIEHPTFPQXSBGHGOEVMUTRPSTRFOKHWEUPUSEROZEBSMTSTZSHFTQ9U' + b'XYTMDVLAPXSHZGYLPVDGTCGHOQSWJJEDARRUPCYFHJOSPVSTNEERBJOERGU9TTOW' + b'9GSVZEODZOEQZATYADJ9NURBJNTPBYECGGWP9SVOZAWXT9RLKBKL9TBBWXSDOSXR' + b'JHPKMLIPWKXSM9MPNQWEYLDPRLTUUNEFHXUFCLLJLZIRGUMEJCTIHC9VPS9YPENZ' + b'PBYKTSBPXIPZHNYZYDPOYRIFEZWOFDYMZTUOMZZHLSLZMTDIMTTXMHHTDLIVRSID' + b'SWJBCDCKEYZPTLLZP9IMNJSRXICEGTPZXVXAVIBGJMMOUNPUKXHIANUPGJANUHTG' + b'9ZPZCBFRMLHYOPFAKGRZSZJARBEEPQZ9TKJRQLXEG9IOHETGXCMKT9XZUBPMIQWX' + b'RRRFF9POXJBXW9NPUIOYNET9CTUWJB9RQDHVIAFLKILVBDLOYZAKIRHAUXE9ORNA' + b'PVXRTUY9CNXAPFPNUADXHDQWGRCVBZMUASLOPAYHLNGNUVVTDQCSOSTOOILZFXBX' + b'UPILJVVDUIRBWQUYNOJX99BTZNYQZGTENKGEKKADMPDWQB9ICWBWFHKAPRNDGGWO' + b'UXDTJKMASYOPYNYPTOEN9EDLXVVUMELPGG9ZLAJXQFTIEA9HRJQCJLRUSLBGIWRW' + b'RXMTSAYVNHNJCYDSYNBPH9XEI9NFEDANKTZ9RWSCMPV9XVBTBZVDO9HABGD9VDOI' + b'XFMWBCHERKTDPDQFQSVNZLZRPHVZTFTL9LRAIMXLMTEZFAKK9CMYVPRTGBXGIMHU' + b'UVWCHDUUEZMZFMDSUQRVVPHZDUTOTLPSKQEHWNLOXKGGJKHHUNQIJXUTNYMZIL9U' + b'OEKECBSTCRVTVKUWETWPECLAXJWUNXXNRDBR99KJSWCHJBTMK9TSLLKWUCMMWNAB' + b'UZLKLCJXHPUWVLIEIHYTZRPTZJTUMDDVEFCDRQYHPBF9WVMATUIQXGWTGAHQSTNR' + b'VZZIPBRPIUOZLXRGEWSUVDXIQPAONF9QPFYIMUEMDXOMFPKKJNGRBNMKXNJUF9IQ' + b'IHPEBHSLWQWXJZNEBKCQUSRWOEGMWFZYGHFUUHDBBOBKSTXT9HGOORUQMFBFBICA' + b'HBQNOBVDCZVGZGASCINUGVEMM9LLPWTNWWVKWYIYDIJEKAVBEFPAVMFWEOYMTOHL' + b'ZVPRMIINUJTTESTVALUEFOUR9DONTUSEINPRODUCTION99999WJRBOSBIMNTGDYK' + b'UDYYFJFGZOHORYSQPCWJRKHIOVIY999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99D99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #2, Part 1 of 2 + TryteString( + b'KJVG9EKTMPWE9PKWGGJJPDISCX9CJXGWUUPOLKKBVUWUYNBACOOF9LEQGNM9YYGN' + b'XJEMOBGSDCPLP9CQIFBCLENUCJGCCSWYU9WVFTRZZCPCZXEGMDDPSYDTYUIMVFSL' + b'GHTAZWJRHY9ZQMFGROIIUIQVDSOTUIRMSROYDPTWRURMZAILGBWIVADPYHPTSFGK' + b'APPMVTTHGLYXZHPFUO9HBAJIOUJOOABAQSOQUKLSVQGSDIHEHGTQSPM9EDHLEQSF' + b'FAAQXR9MUREVQ9MEGXNXMNJVWXYEIRYAOFHDFZAKNVWKTVIHKXWVT9VOZRUPUKXA' + b'SIFAZQVZSWHBQU9RGVLJMRVQCMCYSQEIMIAKOHNAKQLTIMEHMZMGAKCIPGHQTWOR' + b'BLVKISGPKIIMAMQWMZUNTKJSQZAZNYEGORGNRTKCLNRSOQJRBUCPSDLKLGGRBACI' + b'ULLZBFBUNQXACKL9WFEKKAHGLBBRNNEXZPPH9UZESFFKVBOPROFHQOKYAVTJDDVA' + b'UGUAURHLESIEIITDVVRCTRKOGUPERJHNJMXTLNVMWVDZITSPEHRYJKEZVTZSJEYT' + b'OQEGNJRMCJLYYKPGDFUFQHGWRDGEWBXYOGEZ9IXRWJAQLKHPROWIEVI9ILNOXTPO' + b'SRLETMNEQ9P9WLXCUZNMGFK9EYHABBCSEZSGMNJZOEEGRVNU9ASSOOLCXXZKZPFW' + b'U9EEUUQRACVGZPL9MQINGLYPUTUPTLPKWPHRFFBRHZQWIVOXPGAKCQQPRKPPZUHO' + b'JISYASMRYMCMJZNR9D9OQANUXGJXSUSZQFWDJUTNCDKAUAFYKJNVAMBLTPPRPIJR' + b'RKQMCIHHGKPQPUQHWJNIEPDLRAYSJXVSJVKAGBAJCMGQSCZFTEJSG9LUWZGFBGQU' + b'HFUHWDHND9WJBPOQQXDEATOBGXDGM9BKSDCOEZ9IENZPPDUPMKCUKYBIBTBMJPJL' + b'DNSOPEKHVGQKLGUISUFSYMHR9I9LRPLCXJTDHHEXKQEVIFOUGKJEILQIHFG9FWOU' + b'BXRHCRHLOYAXTFQUWKJBSX9GNPCWXUQJRHDBOBRZPQAPMKCIZGULPZDYLLBMAFJZ' + b'XGIRVAAVUUCSNGDGJQJTAPV9QXYIABIHBXILKQLGDGXQUVADQGDFKKDKMU9WKBEE' + b'Y9TAVRYQDQFKPDMLMUAEGBHVJPSIZOEQGCSYNJCICXROXHPZFUXASQJXZEHQTEUK' + b'FIYQIGJWORKAIQUFROYGMIDFAJOUFAYYWMXUGJFPSRTGEUWWLOXEUTKZCZQHWFUN' + b'HTMZVIJ9VYOLBTAIFB9EN9NFVAABVFIBIWXLJSUOYELOQSIPK99AXSXCPECWOXFU' + b'VDIANVO9PKZUESMFWIEVWLEHLCVKDXEROLNEMYRRCJDPAYVTYAYSL9AFZH9GXHXZ' + b'ORXZEQTUJEDJGCYCQAENYZRKDJSK9TOCKKCXOSSTOAIO9UVAKQJBVOS9RUQIESCI' + b'JYRWYRUPMIJEHR9EGZ9YMHQXALUUDMCFYFOMLIGORMMBCDJMFCNNELGPXHICRNRK' + b'ONBKACHLLSABUNHQ9TU9OSSTQXGWBLRRTSKZORXILALQYRXDDMXPPUTEGTVCHSOV' + b'YZEEJMRRECGBMXBORUTIQUNMJDXBSZSYYA9UOTFWMQOHURUFSUESLMILBBKGHTTF' + b'TZONNQIMJSLILKAQJRDTNVK9PHAMNKZXRHSOPGKKLJBRDYAC9BRURJWUIJLUWXNQ' + b'OSVVLFEBROMJCGVYZWIPOYFQRBUUNJLIGPVDLADFLZJGZBLEBEQEUDUZOIFFZLZR' + b'XCPQVMIARFLZRIOFFEHVFJZXFQFLCJSEXRPUKGMWBMGXEHIEZKOKGH9JXAUXACEB' + b'LXKLZT9P9NJGXRZWZJAZCNKR9CHRRNCFOLBCSZXKXOIGZHZSTDKTHOWZTEXWOIZL' + b'PEGPKTBRENSCOYBQJSZQ9XPNRORQOWMPGBXKSSLDIUVEAJODEUZJKZE9MBVTQXWF' + b'XXXOG9QGDWMNZEWVWVDZWIFKSWZIDNBYEJP9VBKQNOJEXVPZQTHVUCSK9QCMEPUS' + b'9Y9FQPWEACAEBIQSVPJEL9ZBSETINIYMSPIXLADSHTDYRAYUTMXDCABIUUETMNLU' + b'RELTPAGEDNMQZALFWOPAI9WUFOSUTOFUUWFAFRFVYOPITBVSG9IBVNJCOITYMTCC' + b'IJIZWVPYGQETESTVALUEFOUR9DONTUSEINPRODUCTION99999WJRBOSBIMNTGDYK' + b'UDYYFJFGZOHORYSQPCWJRKHIOVIYYZ9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99C99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 2 of 2 + TryteString( + b'SWHZKSNCOQXPCGRTYJPUGKLBNEJFXASKY9XAUROGDAO9QQLIVRZQDJDTPLNTBGUU' + b'FGELJPSGUMGPIUNCCTQEFU9UZIJJYJXCYWRADRHHXKBEDG9HTCHJHXUJRKMIUFOS' + b'KDGMI9QPCYQSWDCUYKQQEONJEKYWELG9MSNBHRHILGSSKMRCQJBOGNYBKEMTOFEU' + b'BEOUBD9ULP9PHWYKXEQNDMUR9BGDGPEUFRFRGNGFJFPYQXABSDALTYKL9SM9VVQC' + b'OHY9AS99EYWSHUNEQVVNLS9CNPEVMPOKMWQYFPGTNJBZCDWYPFWSBKZOYXNNVMPO' + b'DEHMHNIYZCHIEDEAB9TLFOWVHF99GVRWUZWSN9IQOKWIXERKRQETZS9ZJJSQRLLP' + b'QXEWNMFVVBWOIK9MBYCEGUJ9HJRIIMBVJNGXMGPGDLOLYWFVQNOKTFZRBJSSBJTE' + b'TGAIUGZOYQOFTVBKAQY9SSJWJXXYAUVUQWPXVCISFSDSHDQCPVMG9GVDAO9GIMDH' + b'ZWJOKSUEUFHBGSCZKNBTZWJXSFYNJSBSEL9UMZBAZRGYCEHOSJBMKMPMNXKEVTMU' + b'DEFWBIKOXUSBNPTNECGVLYSOGUDJDPHYFADXRAOLQXJSJDANINJEOMCFAWWITREG' + b'CDF9OZ9ZKHPJZJNMOVGX9OKQBSGVZYWKNOPVJEOZEI9BPE9GCUEQVAHSBBRBGQTE' + b'XVZCSL9ECOWPOWZCVSCBOUSNQMTJIEKHXL9NCPRMLRNKQEHYJCLRHGZKFNBJIPKS' + b'KPRFTSKFJULTBTXFDQHWUYOSDQBHPAINVEPKCCHJDTZOJIGJZOF9AEQDBKCZSZMI' + b'WUUVHVGAFKALGITVQQKBAHKCIFSVMVZ9UDQABVIANTBUQOFBIXQBWB9KKQOVJZNV' + b'BEDAZKUTRNKGJQWMTEKV9KGCIBRDCBAPKSTMCZGUV9HTAABQDKGQBCRFNXBMZRTH' + b'F9MO9GAGQDYDVLOFMDE9QQZYR9GDSBLUVVMKMCZIMDPNCVLGDKBACWQJRWOQNKBT' + b'SDJFKQMKTVKXVNAHRHZALJGVAMXWJYRAKTEJFXAHBQGSYWWQVECQYPXVFWILNFZK' + b'GGRIFCJBSIZRDJXRJHSURPCZKOWKLFRUMVENEGMNKUAOGVUECDSGAZNQZKBJDJPV' + b'BXLOTID9QLMFNGIWKAAIQTJJROSZBXPQRXAUCV99OGCEOTQCJ9II9ASZL9XGNSVU' + b'XVKPXYOJMF9PX9GSLEROR9FXVQ9MLEMEW9IWNWBNVAYXZ9ZETTDSMLGZAKHE9IUJ' + b'BFUHXW9KWCNZOZCCTFGBGWSDAQGGSPSQHOMUVJMLWBDAKYQZMWPQLLYAGUMOVMVL' + b'FD9TO9OUBTVUHHUNSFSATSEGBFVGDZUBMTWWFDPSQVCUFRVKHYYPDWRPNSKXRFTV' + b'EIBVZNGUZRQCPXVKBPKQDDLEBWIEBIPTEJIYFHBXCUVCCKTKEJAYRZCKAXLMELIN' + b'WUZHG9JFBSBAKHIXMWHUWUFHFNLXNO9GKINYKRTCNN99PHPHO9MJAGUYZAPNSPWU' + b'Z99E9BEADKETLOALWNANYMHSLLQSBS9YTYVJKTVWFUVS9MFOWCHLEUUFUWTYGLZX' + b'FDUXVABTVFXFPUEPIUEIAVSZSSZQJTHNGKBJXADRHVTIBERILMCCGWUITYQHGEEG' + b'WIZZOII9B9EVVVFJNEYEWH9ZVOJGHKVPYKDEZZSPBAOBQGGWPWXTCKSLSHJQYCDH' + b'AYIQ9QVSQFPXZDBYSJJKSNTRXORHLPVOYVMIGALRPXYWQWSJPPFTJCYXAATLBFNS' + b'GVXKFJXHYTILNOQZROUCDUTWOMGYBVTWPKJY9RVKKWQQMVCHJEUBELJDKJPGYLXU' + b'XATNOIJHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHVHE9EEB' + b'QYSNWECSPAJHGLTEUCXALBRVGXFENUCOONSUFZLHTLVQNPDZDIVDQHWVLDEDPFQL' + b'JZWF9GFZMPZXFVEQECLUZBBFVSAPEXJLKKOMXEPHZAKP9WYTGQOML9FQSBMSFLOG' + b'RLFQKUCUWFX9DNAOZSSKBUV9IBVIRNUWYBKJVKLJ9PPNLGJATKDCAGVFIVPXRABH' + b'ZVZACJIG9WOKKLFCRDSMTWSCYHOZEEXRIMPQBXVXQAYKZIADSM9GUBICGKGQYNHK' + b'VYOZFRVCHNMTESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVW999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99B99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 1 of 2 + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWG' + b'HMNUXBJTOBKZFDNJEMAOPPLR9OOQJCDVO9XSCYQJQVTXQDYWQEBIXKDZAFWINAHJ' + b'ELJTDPVMUEWSVCJA9ONDYBNANWCGLBQMEMTBFDMWLCMQHGJLGYDQGIMLSNQHBGSV' + b'TDZSGNQAL9OHRAPDKYSVBTNYRUUBNEEAINJMOVOHOWXUAIEDAIQDESQFCKJELHAV' + b'ODSMXMKEHTDKCDIWWISXSAHQE9TJTLJZGXIABHU9CUACMLVSSYV9UJREPWFVYWWX' + b'PYYJRP9DOEKNDMBSBKKHIFMPXZXIJERXRZVBVDBYNZBBCCOSEDOLDGSNQK99HIYS' + b'WNYYEBLRT9MADLXLLZJOSZCFWAVZY9XUPNZUVOSKBMKXXJNRKDBOSGUGME9QNBMH' + b'IWXWXPEEUVQAQVUXDJGMJOBXG9VJBWPRQRCCQSNBEHTLGOKJVYEPQOJO9QIZLYAV' + b'LCKVXKEKRGBSZJAC9KTSSNMDQGKCLPZDJAQ9PBQMLUONVVFAWTMREGFXJMRRGL9M' + b'KNPOZGOYRPDCYEJCYJUN9HYNSNHXARMRJVXBUHOP9K9BIIEYGSHBUESKTAOQOEAN' + b'EAIHYHVGSVNPXWRBTJAMKMWEQOSYEWXLSRYVOSTMPOGYNPDNFLOICXVHYBDHSXVR' + b'KVWNVZOZQDOITZWICSYEW9RGCPPUJYVIYVTSZILYENYUYUGDSGWVYWRMZJNCTTPV' + b'WDWXAPVZQQKI9CGEQPBFPCLGDDEGBUUTISNCMJXQCTUNKQTLCATNOIRPMEUQBQTH' + b'HQYRGDLZEUZBALNQDXJYZBVXDPLVOVVAUCQSCGRTUJRBBNRV9ORETTGFIXBBBVOP' + b'FHPKGPKVBYFTZMWUVZYVWWSDKQVONMPLLQTV9IZUWLUWZNLCVJNPMG9CMXQG9D9W' + b'YCANBRMYV9DU9FMJT9JHT9RWCGLHFCODXJVFQBLTKJWVNVGSUHNWLHNPLZDSWDMD' + b'VQTLVCSVFJJTIQZFAPCXJWDAXWJKJVOKHALCQQTIXABPFXPUFK9IKXYUGMPXNSQC' + b'JDVETOVEX9LXYLXWRW9PFEYJCUJHLUB9NXTUGLIQMDGPDPSJTWDYEWXQAICLN9BT' + b'GNBJWLVAXZGNCYXGHBMRUVVYTJGH9XDGSZHQDYKFGMOWORSFDFBLJHBRTXRSEBAL' + b'CJIJTQJYDZZKWZGVAPFVKVEOXGYRLMBSPFHUIJZZFMFVOTLPUWSYZCWFZMAALHRG' + b'SYSXSMOHWARYZZVIAKXAHGY9SROWPVFACXXLQEXXOJCKXRRZHBZXJIBWQMMZTRDF' + b'YQBSBBZQQXGCAAECMQINHJRBSGOYPCGWKPWCHBKOJTIGDASZFGONTUGDSOOLEMGO' + b'EBFCZZJZSCGXPHXHB9WGMMFVUTCHDBSAMYTECQZWGCXAWTCTIBZHQVUAIBPZHBBT' + b'ZAERYU9XAMKBHCHGZISSPOWJIRZTAXDHMAYBPXOXWDIUDHNBTFJNVHHJO9AWAEC9' + b'UPRRFJLNGKTXJXFDGODDOPMGLALRIJBVIFLQTYQPKCKCRBYPBYGUUFJGJFVCOURN' + b'KCGNTQNNKHDDPIVZHCJSLDUYHVPAX9YJOFTTFSKFHTOOQQRCPYZKTDVCUZGBOBZK' + b'LVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VZZGUNUCKOYLYXOVHMGULWGSRCGX' + b'ZLJVNIMZBLFOJJKOTUREMBXYOZXDUP9ROUVYOSJBGGFZMIFTKHJHHJGZJNOYQWFZ' + b'AHLJWWDDFQQAMEGJUEUSIWOHKFJWRXRSJWYPGIGZGMFNAIDGDOUUQUVHJZQPJMLC' + b'GKGADXAXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVGXBBPBE' + b'BDVGZDBWMDMLPXYJBBRNOMKGR9TSVUXSRYXQTCTYLFQORMIGDKBJLNLCQXACVCBJ' + b'GVWRJNYPCKOAILPLMWBYKDLDXLIZMZFWDXUWDEGDUURQGMJNUGJXDXYJGKOTQBGC' + b'HATROPKEN9YTXDUOCMXPGHPDANTJFRRVEVBFVCNTWNMMOVAVKBNSJIWWBVHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHV' + b'ITGYROZZNQPTESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVWN99999999999999999999999999999999999' + b'999999999999999999NYBKIVD99A99999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Spend transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9' + b'RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99999999999D99999999PNTRTNQJVPM9LE9XJLX' + b'YPUNOHQTOPTXDKJRPBLBCRIJPGPANCHVKGTPBRGHOVTLHVFPJKFRMZJWTUDNYC99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) def test_pass_inputs_implicit_with_change(self): """ From 2083bf43c8b11118d6035e18e3ee075f60721f8c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 15:15:31 -0500 Subject: [PATCH 211/239] Verified `prepareTransfers` w/ implicit inputs, with change. --- .../extended/prepare_transfers_test.py | 339 +++++++++++++++++- 1 file changed, 336 insertions(+), 3 deletions(-) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index bc2cf64..ad8ea98 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -1371,12 +1371,14 @@ def test_pass_inputs_implicit_no_change(self): Preparing a bundle that finds inputs to use automatically, no change address needed. """ + # noinspection PyUnusedLocal def mock_get_inputs(command, request): """ To keep the unit test focused, we will mock the ``getInputs`` - command. + command that ``prepareTransfers`` calls internally. References: + - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` """ return { @@ -1837,8 +1839,339 @@ def test_pass_inputs_implicit_with_change(self): Preparing a bundle that finds inputs to use automatically, change address needed. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + # noinspection PyUnusedLocal + def mock_get_inputs(command, request): + """ + To keep the unit test focused, we will mock the ``getInputs`` + command that ``prepareTransfers`` calls internally. + + References: + - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` + """ + return { + 'inputs': [ + { + 'address': + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', + + balance = 86, + key_index = 4, + ), + + 'balance': 86, + 'keyIndex': 4, + }, + ], + + 'totalBalance': 86, + } + + mock_signature_fragment_generator = MockSignatureFragmentGenerator([ + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWGHM' + b'NUXBJTOBKZFDNJEZUKCKWGUHVSU9ZJYAVSQSOFDCOIEP9LCXYLTEFMCYUJAAHLYUHQ' + b'P99S9XRWHXHRPZCWHDMIDYW9OQAWUPTFMBTJGDCWRVNVRDPIWISVYNUDWUGBPNNFZD' + b'WRVZ9FGAVSEWFXRXGGLXJTPJTJLC9JYHMFBKYAUJRAMHQHKUUZHRWZIVC9KFEEXXVN' + b'EXJRYUSFV9PEPFUDCNRRTSCZXSTUEGJKDV9UCYNZSBRDYGOKFGYKWVFCYSWBUJYVGE' + b'UXWTDGPWTWURH9RKEZRFCUUBFBPKSFONMDXWGYKWAUWVUOQVBIGQMMKQVDYAZ9SVFI' + b'UUNMHOJGRQVXZGIIPKVNNBKABGKZLRNFK9KSIHTCGYPVCWYGDS9OIZWLNINYRLGJQC' + b'UBWYMAVDWFAURLALQPMRMFRAZCMCPOWM99SGBVEZPAFAXHXNENNWXLF9ZVHZIDWBLF' + b'KVWKBUYNBXOXTVPDWAGZXIOMDAEKNMRFGZVIGIFOSHGMPIPWNOWQDMHPKOJTYYECKN' + b'GCDDTJVALGPZSX9IH9LEGQSDACLBWKNXUW9BAZSHAISUJDTPJDOASLVRXFNJJHXQTK' + b'MKZUZIMJFPOKHEQXSCJQH9JPRNZHDVVZKWTHWWFNFMHFXPUIEEA9HPHJTCJJWZPUHK' + b'AAWJQQSAIF9HRETYYPXAZ9YCFJRCXTGCOLJQA9HDLFNTVDMYPRCYPQR9MNBBAMGOJX' + b'PRFCUSIIZN9VROZDPMOKZBCILKGB9EPCXOYWLPHFXTYBCMLRVHWNQDSQUIHHTAUTZC' + b'JFQ9CO9GTONKYKMDBSREZC9SUBHYK9JDOBYDBUBUIO9TRXQLAYHDDSXGJ9NB9FKMUU' + b'US9GANWVMQLIHX9MPJGLTAOMCZTQYDYVOWXHGHYCV9VDCXHGTCOOUEXIITVKHXCSUS' + b'OIRTMEAKMTYZCMAWURNX9JOVDICICKHXQYBXKWTXWXBZVZWRIDC9YCZVSKYIKJYYMF' + b'YQRTWBNJHWXRL9JFSZAXJYYTGDYLTHLWRMBUEG9QTGNRPVTBGRYFPEJQSIWTLPGV9C' + b'CMCO9TCKLKSJEAMFKQMXEYETISVEYDOSCRZ99RFDPUQPHMQ9NVRUBXITDGFZCYQNFC' + b'SULGRHPONWJDVWT9UELEKEPQEAFKDLDNYPABC9GUASVFJBFZF9Z9CHIUNLJWHKGDYK' + b'ADLUCRNEPAIWYSX9LT9QWQRKU9WEVDPKSTSA9PPEVNTBNLN9ZOPETINXGKA9DCOHPD' + b'QMMOOOCKYVEZJ9ZJQRJHNCKRFDRPHUVPGVGQYKZBLOILZTPIX9MIBKTXOJKVAYRLSX' + b'DTOEEKLF9WWZGLSGIOQZWCJJHSBTXYWRDYVEQTCNUENYWDRLZZIVTGCXEAJDRY9OVM' + b'XJGCSQSGYFLGYDZUH9EHUDQTCXLSDPMNDYQRZYRXYXKY9GIYOSIDQPXXHKJKDQLSCU' + b'Y9FFBTPSTJFEFROCEXFFYTFYHQROAVTYKQOCOQQWBN9RKJ9JJEURKTVOECYRITTYKN' + b'OGCD9OPQ9WDMKRPIUNRAVUSLFMC9WZWHSESGLDUYHVPAX9YJOFTTFSKFHTOOQQRCPY' + b'ZKTDVCUZGBOBZKLVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VDFKWVEWV9WGXPJH' + b'UBWYXGECBPQOPOHG9YCVXDWOXTEAOFBCEEAV9JCHUVLIRIMHXMUSZPOMMRBF9PLVLR' + b'JYTXTBANBZWFQWGNGFGXFOZ9YGMQSZFEJHLFZTTVHRLJPATA9TYCM9LSEWMNEUDNWQ' + b'FLUXOFUNVDKSNIIXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVG' + b'XBBPBEBDVGZDBWMDMLPXYJBBRNOMKGPMCG9FTSLMRADFVPUTTEIOUCBLPRYZHGOICN' + b'C9BT9WHJJJPDOSOMLD9EKRGKYUHUMMCAVHGYWOVQXFLTCXAAUDYKGKGKOYHLDCCQSK' + b'NHJHPSXTJVTW9QPFOQ9FDZIDDKIVF9CDYGU9ABRESMDLIBONAQWFVGCNOTEDHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHVIT' + b'GYROZZNQP' + ), + + TryteString( + b'ZOJNUMZOBEHLYDSDAVZKXHF9MAHAJICBMJTZZHTQTCACVQAUSSCFUMGCSJTONNKXFI' + b'NPOAXQIKSJ9GUV9GXM9KYDCDWUHULIJMSKMOLDZBYE9FTGFMKLODKHFF9YUCPTYFFM' + b'9EDCJDCKRFLZUHGGYNYFJLBFWXCIUF9HMGUQKPUCJ9OQ99FXHSUSRRBEUSSCKCYPIE' + b'AFZJQNXEUYWLEXKZWLRINBEGAZTJMYTUEQTTORMIIQASISHSHZDQJXANFLKOIRUEJU' + b'PZZHUJFWHEXFIZ9OU99SQLDDNLARDFPGYSCMXQCMGPRB9QLM99QUBLTLTKWYXHVAFU' + b'VVAMHEYCCNVEITSPVQWMSEIZJSLPWNGWISKWQNXCNRNOIGRGUHGYWLOFNXBDCT9JLA' + b'9CEKW9BFGOESKGOQLJBTLUMOICBEZDHCR9SZCJUZVXIEAVITFJFDGNJII9LSW9IQKV' + b'99UJWWAACGIRPCZUENXGILUXCMJIGW9REUNA99MWSANWL9KVKKXCKXLRGDT9NXIGQV' + b'ZWG9NBQPOQKEEET9ZUSENFPGFDNNHGBITCPASGHOPBNYKKEHKHVATNVWX9ZGTISUKP' + b'KTMWMPCGVVJSGMRJWNFICSFUAVAHIZWA9PDOIXFJGWCPTZHUDDUFJVQPBYNJREQ99U' + b'HOESTT9FELDMVK9VHZYPRVOWEW9NXTCYDCIMT9UIWGXUFYILOPOCJFVVEJEJN9ULGX' + b'IABFJWWRKAD9NHZBULMWUKESZLCPRQVVKWOHEWSTLOFNA9KNERURWJPROBBXEWICDK' + b'KCQXWYMJUCQLWEUPFXRSNMIJWQUEJUNIKDYJILXCGCLFETWOZYIUZVJVYVB9YGXSSD' + b'XYXSJXTOQZ9CCCAKMCNNKQCYEDGSGTBICCOGEHRIVMICUQPUUFRFCBF9NUUWSQBTVI' + b'YFVWAASTQJZFDDWWUUIHPKTIIVAGGIEQCZUEVOFDMQLDESMQDPQUSOOKZJ9QLXTAFP' + b'XXILFHFUIFJTKSEHXXZBPTZUGLYUZNORFOEKQDEIWGXZPBXSOGGQFILUJTKDLWVKPV' + b'ISU9QOATYVKJHLDLOKROZNFAGS9CICXXIUQQVLLRPPPDYJVSCW9OWIHKADCVSKPWTE' + b'NYEWQWEHP9DDWOUJDWSTSOGYQPALFMKCTUGLSXHNYETTMYTS999SYQVQSPHQPKRJSU' + b'Y9QTABAJOJAAMGVBCSLAAOBXZOJZLIFXUYOVXBKHPFVTKKGSIHUXMBDTMGNVL9NXYC' + b'HOVTLGDICIWTCIGNRHLBZBVSXMPBFAWIXPCDJWNDUFHUVLBSPBWICZNYIUJPRRTOCS' + b'SCVPNBXEDCMHKFVDMHJTSP9JI9BXTD9ZILEEOCBMHCQRRDNL9EUKJGJ9MPQGQU9ZFY' + b'GVSNOYAEC9NWTCVEJBSXLYWTUPMXNAAWXSBIAJYSGYHGLYOMAHFTYMICZRDZTQXHAQ' + b'GVXENKIGW9XZTPBAIMZLHWAJCGY9ZDNQOTGDRCTXSJCEJVTTMVRYYKWAFYSV9WVEVC' + b'FAXJKJNUC9NQHPEXWIOHOJQEXJNLEW9GLO9AJCJXIEXDONOGKXFJ9OXXXETUEHLBXA' + b'JGFPHKAQDCRTKQBXPZYQZBQODTVIBUTSAEXMBFBMTAXOQZCOHWEWRJEKNKHZXXSO9U' + b'SZRWUPZAASWDBXOVAEGSAGYDIOZWSSEAIQVRWFDSOXSRRRQHRCWDJWZXXJOGPZRLKQ' + b'OA9DOY9RXZNWBFJTKUOVRRQNSDUOFGCUQNHOBMJSFQZXVBPHHBRRIXZNLXAH9P9EFM' + b'GRPGSCFRZINEPOQPXPKHTSRJWARXRGJGYMTPUKQISLV9GUC9VTJLOISKGUZCTZEYND' + b'TURLBPXGNQLVXHAHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHV' + b'HE9EEBQYSNWECSPAJHGLTEUCXALBRVTKMWSWCBPUMZFVSEEFIHBAGJVVQV9QLFEGGY' + b'VPNSDOBZEQGLEFLCQVPDJA9MQDRHYNVZVNTYNJ9GJCXKED9NEWTD9RVMNA9HOHUBLL' + b'ASNQSDLDZKOMFOEGBJZPYVYZCVHYFEGSVEHSWV9WAGMEQIUDZQZUACWYQLTD9LHBVK' + b'KNXXXDWQUWRJKTCDP9CEJOHLLPTWCIKKHHIFAFFDVMFZR9A9LYVMTQAPAXAVPJOZKW' + b'FQNAJTO99' + ), + ]) + + # noinspection PyUnusedLocal + def _create_signature_fragment_generator(bundle, key_generator, txn): + return mock_signature_fragment_generator + + with patch( + 'iota.transaction.ProposedBundle._create_signature_fragment_generator', + _create_signature_fragment_generator, + ): + with patch( + 'iota.commands.extended.get_inputs.GetInputsCommand._execute', + mock_get_inputs, + ): + response = self.command( + seed = Seed( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' + b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' + ), + + transfers = [ + ProposedTransaction( + value = 42, + address = Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + ), + ], + + change_address = + Address( + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + ), + ) + + self.assertDictEqual( + response, + + { + 'trytes': [ + # Change transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJRBOSBIMNTGDYK' + b'UDYYFJFGZOHORYSQPCWJRKHIOVIYQB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99C99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 2 of 2 + TryteString( + b'ZOJNUMZOBEHLYDSDAVZKXHF9MAHAJICBMJTZZHTQTCACVQAUSSCFUMGCSJTONNKX' + b'FINPOAXQIKSJ9GUV9GXM9KYDCDWUHULIJMSKMOLDZBYE9FTGFMKLODKHFF9YUCPT' + b'YFFM9EDCJDCKRFLZUHGGYNYFJLBFWXCIUF9HMGUQKPUCJ9OQ99FXHSUSRRBEUSSC' + b'KCYPIEAFZJQNXEUYWLEXKZWLRINBEGAZTJMYTUEQTTORMIIQASISHSHZDQJXANFL' + b'KOIRUEJUPZZHUJFWHEXFIZ9OU99SQLDDNLARDFPGYSCMXQCMGPRB9QLM99QUBLTL' + b'TKWYXHVAFUVVAMHEYCCNVEITSPVQWMSEIZJSLPWNGWISKWQNXCNRNOIGRGUHGYWL' + b'OFNXBDCT9JLA9CEKW9BFGOESKGOQLJBTLUMOICBEZDHCR9SZCJUZVXIEAVITFJFD' + b'GNJII9LSW9IQKV99UJWWAACGIRPCZUENXGILUXCMJIGW9REUNA99MWSANWL9KVKK' + b'XCKXLRGDT9NXIGQVZWG9NBQPOQKEEET9ZUSENFPGFDNNHGBITCPASGHOPBNYKKEH' + b'KHVATNVWX9ZGTISUKPKTMWMPCGVVJSGMRJWNFICSFUAVAHIZWA9PDOIXFJGWCPTZ' + b'HUDDUFJVQPBYNJREQ99UHOESTT9FELDMVK9VHZYPRVOWEW9NXTCYDCIMT9UIWGXU' + b'FYILOPOCJFVVEJEJN9ULGXIABFJWWRKAD9NHZBULMWUKESZLCPRQVVKWOHEWSTLO' + b'FNA9KNERURWJPROBBXEWICDKKCQXWYMJUCQLWEUPFXRSNMIJWQUEJUNIKDYJILXC' + b'GCLFETWOZYIUZVJVYVB9YGXSSDXYXSJXTOQZ9CCCAKMCNNKQCYEDGSGTBICCOGEH' + b'RIVMICUQPUUFRFCBF9NUUWSQBTVIYFVWAASTQJZFDDWWUUIHPKTIIVAGGIEQCZUE' + b'VOFDMQLDESMQDPQUSOOKZJ9QLXTAFPXXILFHFUIFJTKSEHXXZBPTZUGLYUZNORFO' + b'EKQDEIWGXZPBXSOGGQFILUJTKDLWVKPVISU9QOATYVKJHLDLOKROZNFAGS9CICXX' + b'IUQQVLLRPPPDYJVSCW9OWIHKADCVSKPWTENYEWQWEHP9DDWOUJDWSTSOGYQPALFM' + b'KCTUGLSXHNYETTMYTS999SYQVQSPHQPKRJSUY9QTABAJOJAAMGVBCSLAAOBXZOJZ' + b'LIFXUYOVXBKHPFVTKKGSIHUXMBDTMGNVL9NXYCHOVTLGDICIWTCIGNRHLBZBVSXM' + b'PBFAWIXPCDJWNDUFHUVLBSPBWICZNYIUJPRRTOCSSCVPNBXEDCMHKFVDMHJTSP9J' + b'I9BXTD9ZILEEOCBMHCQRRDNL9EUKJGJ9MPQGQU9ZFYGVSNOYAEC9NWTCVEJBSXLY' + b'WTUPMXNAAWXSBIAJYSGYHGLYOMAHFTYMICZRDZTQXHAQGVXENKIGW9XZTPBAIMZL' + b'HWAJCGY9ZDNQOTGDRCTXSJCEJVTTMVRYYKWAFYSV9WVEVCFAXJKJNUC9NQHPEXWI' + b'OHOJQEXJNLEW9GLO9AJCJXIEXDONOGKXFJ9OXXXETUEHLBXAJGFPHKAQDCRTKQBX' + b'PZYQZBQODTVIBUTSAEXMBFBMTAXOQZCOHWEWRJEKNKHZXXSO9USZRWUPZAASWDBX' + b'OVAEGSAGYDIOZWSSEAIQVRWFDSOXSRRRQHRCWDJWZXXJOGPZRLKQOA9DOY9RXZNW' + b'BFJTKUOVRRQNSDUOFGCUQNHOBMJSFQZXVBPHHBRRIXZNLXAH9P9EFMGRPGSCFRZI' + b'NEPOQPXPKHTSRJWARXRGJGYMTPUKQISLV9GUC9VTJLOISKGUZCTZEYNDTURLBPXG' + b'NQLVXHAHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHVHE9EEB' + b'QYSNWECSPAJHGLTEUCXALBRVTKMWSWCBPUMZFVSEEFIHBAGJVVQV9QLFEGGYVPNS' + b'DOBZEQGLEFLCQVPDJA9MQDRHYNVZVNTYNJ9GJCXKED9NEWTD9RVMNA9HOHUBLLAS' + b'NQSDLDZKOMFOEGBJZPYVYZCVHYFEGSVEHSWV9WAGMEQIUDZQZUACWYQLTD9LHBVK' + b'KNXXXDWQUWRJKTCDP9CEJOHLLPTWCIKKHHIFAFFDVMFZR9A9LYVMTQAPAXAVPJOZ' + b'KWFQNAJTO99TESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVW999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99B99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 1 of 2 + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWG' + b'HMNUXBJTOBKZFDNJEZUKCKWGUHVSU9ZJYAVSQSOFDCOIEP9LCXYLTEFMCYUJAAHL' + b'YUHQP99S9XRWHXHRPZCWHDMIDYW9OQAWUPTFMBTJGDCWRVNVRDPIWISVYNUDWUGB' + b'PNNFZDWRVZ9FGAVSEWFXRXGGLXJTPJTJLC9JYHMFBKYAUJRAMHQHKUUZHRWZIVC9' + b'KFEEXXVNEXJRYUSFV9PEPFUDCNRRTSCZXSTUEGJKDV9UCYNZSBRDYGOKFGYKWVFC' + b'YSWBUJYVGEUXWTDGPWTWURH9RKEZRFCUUBFBPKSFONMDXWGYKWAUWVUOQVBIGQMM' + b'KQVDYAZ9SVFIUUNMHOJGRQVXZGIIPKVNNBKABGKZLRNFK9KSIHTCGYPVCWYGDS9O' + b'IZWLNINYRLGJQCUBWYMAVDWFAURLALQPMRMFRAZCMCPOWM99SGBVEZPAFAXHXNEN' + b'NWXLF9ZVHZIDWBLFKVWKBUYNBXOXTVPDWAGZXIOMDAEKNMRFGZVIGIFOSHGMPIPW' + b'NOWQDMHPKOJTYYECKNGCDDTJVALGPZSX9IH9LEGQSDACLBWKNXUW9BAZSHAISUJD' + b'TPJDOASLVRXFNJJHXQTKMKZUZIMJFPOKHEQXSCJQH9JPRNZHDVVZKWTHWWFNFMHF' + b'XPUIEEA9HPHJTCJJWZPUHKAAWJQQSAIF9HRETYYPXAZ9YCFJRCXTGCOLJQA9HDLF' + b'NTVDMYPRCYPQR9MNBBAMGOJXPRFCUSIIZN9VROZDPMOKZBCILKGB9EPCXOYWLPHF' + b'XTYBCMLRVHWNQDSQUIHHTAUTZCJFQ9CO9GTONKYKMDBSREZC9SUBHYK9JDOBYDBU' + b'BUIO9TRXQLAYHDDSXGJ9NB9FKMUUUS9GANWVMQLIHX9MPJGLTAOMCZTQYDYVOWXH' + b'GHYCV9VDCXHGTCOOUEXIITVKHXCSUSOIRTMEAKMTYZCMAWURNX9JOVDICICKHXQY' + b'BXKWTXWXBZVZWRIDC9YCZVSKYIKJYYMFYQRTWBNJHWXRL9JFSZAXJYYTGDYLTHLW' + b'RMBUEG9QTGNRPVTBGRYFPEJQSIWTLPGV9CCMCO9TCKLKSJEAMFKQMXEYETISVEYD' + b'OSCRZ99RFDPUQPHMQ9NVRUBXITDGFZCYQNFCSULGRHPONWJDVWT9UELEKEPQEAFK' + b'DLDNYPABC9GUASVFJBFZF9Z9CHIUNLJWHKGDYKADLUCRNEPAIWYSX9LT9QWQRKU9' + b'WEVDPKSTSA9PPEVNTBNLN9ZOPETINXGKA9DCOHPDQMMOOOCKYVEZJ9ZJQRJHNCKR' + b'FDRPHUVPGVGQYKZBLOILZTPIX9MIBKTXOJKVAYRLSXDTOEEKLF9WWZGLSGIOQZWC' + b'JJHSBTXYWRDYVEQTCNUENYWDRLZZIVTGCXEAJDRY9OVMXJGCSQSGYFLGYDZUH9EH' + b'UDQTCXLSDPMNDYQRZYRXYXKY9GIYOSIDQPXXHKJKDQLSCUY9FFBTPSTJFEFROCEX' + b'FFYTFYHQROAVTYKQOCOQQWBN9RKJ9JJEURKTVOECYRITTYKNOGCD9OPQ9WDMKRPI' + b'UNRAVUSLFMC9WZWHSESGLDUYHVPAX9YJOFTTFSKFHTOOQQRCPYZKTDVCUZGBOBZK' + b'LVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VDFKWVEWV9WGXPJHUBWYXGECBPQO' + b'POHG9YCVXDWOXTEAOFBCEEAV9JCHUVLIRIMHXMUSZPOMMRBF9PLVLRJYTXTBANBZ' + b'WFQWGNGFGXFOZ9YGMQSZFEJHLFZTTVHRLJPATA9TYCM9LSEWMNEUDNWQFLUXOFUN' + b'VDKSNIIXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVGXBBPBE' + b'BDVGZDBWMDMLPXYJBBRNOMKGPMCG9FTSLMRADFVPUTTEIOUCBLPRYZHGOICNC9BT' + b'9WHJJJPDOSOMLD9EKRGKYUHUMMCAVHGYWOVQXFLTCXAAUDYKGKGKOYHLDCCQSKNH' + b'JHPSXTJVTW9QPFOQ9FDZIDDKIVF9CDYGU9ABRESMDLIBONAQWFVGCNOTEDHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHV' + b'ITGYROZZNQPTESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVWVX9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99A99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Spend transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9' + b'RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99999999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) def test_fail_inputs_implicit_insufficient(self): """ From 0031fafacb2327e477072faa18d005eca8ba1ec5 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 15:18:53 -0500 Subject: [PATCH 212/239] Cleaned up tests a little bit. --- .../extended/prepare_transfers_test.py | 154 ++++++++---------- 1 file changed, 65 insertions(+), 89 deletions(-) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index ad8ea98..f4b7ff3 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -7,7 +7,7 @@ import filters as f from filters.test import BaseFilterTestCase -from mock import patch +from mock import Mock, patch from iota import Address, BadApiResponse, Iota, ProposedTransaction, Tag, \ TryteString @@ -714,13 +714,9 @@ def test_pass_inputs_explicit_no_change(self): ), ]) - # noinspection PyUnusedLocal - def _create_signature_fragment_generator(bundle, key_generator, txn): - return mock_signature_fragment_generator - with patch( 'iota.transaction.ProposedBundle._create_signature_fragment_generator', - _create_signature_fragment_generator, + Mock(return_value=mock_signature_fragment_generator), ): response = self.command( seed = Seed( @@ -1092,13 +1088,9 @@ def test_pass_inputs_explicit_with_change(self): ), ]) - # noinspection PyUnusedLocal - def _create_signature_fragment_generator(bundle, key_generator, txn): - return mock_signature_fragment_generator - with patch( 'iota.transaction.ProposedBundle._create_signature_fragment_generator', - _create_signature_fragment_generator, + Mock(return_value=mock_signature_fragment_generator), ): response = self.command( seed = Seed( @@ -1371,51 +1363,47 @@ def test_pass_inputs_implicit_no_change(self): Preparing a bundle that finds inputs to use automatically, no change address needed. """ - # noinspection PyUnusedLocal - def mock_get_inputs(command, request): - """ - To keep the unit test focused, we will mock the ``getInputs`` - command that ``prepareTransfers`` calls internally. - - References: - - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` - - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` - """ - return { - 'inputs': [ - { - 'address': - Address( - trytes = - b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' - b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', - - balance = 13, - key_index = 4, - ), + # To keep the unit test focused, we will mock the ``getInputs`` + # command that ``prepareTransfers`` calls internally. + # + # References: + # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` + mock_get_inputs = Mock(return_value={ + 'inputs': [ + { + 'address': + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', - 'balance': 13, - 'keyIndex': 4, - }, + balance = 13, + key_index = 4, + ), - { - 'address': - Address( - trytes = - b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' - b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + 'balance': 13, + 'keyIndex': 4, + }, - balance = 29, - key_index = 5, - ), + { + 'address': + Address( + trytes = + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', - 'balance': 29, - 'keyIndex': 5, - }, - ], + balance = 29, + key_index = 5, + ), - 'totalBalance': 42, - } + 'balance': 29, + 'keyIndex': 5, + }, + ], + + 'totalBalance': 42, + }) mock_signature_fragment_generator = MockSignatureFragmentGenerator([ TryteString( @@ -1567,13 +1555,9 @@ def mock_get_inputs(command, request): ), ]) - # noinspection PyUnusedLocal - def _create_signature_fragment_generator(bundle, key_generator, txn): - return mock_signature_fragment_generator - with patch( 'iota.transaction.ProposedBundle._create_signature_fragment_generator', - _create_signature_fragment_generator, + Mock(return_value=mock_signature_fragment_generator), ): with patch( 'iota.commands.extended.get_inputs.GetInputsCommand._execute', @@ -1839,36 +1823,32 @@ def test_pass_inputs_implicit_with_change(self): Preparing a bundle that finds inputs to use automatically, change address needed. """ - # noinspection PyUnusedLocal - def mock_get_inputs(command, request): - """ - To keep the unit test focused, we will mock the ``getInputs`` - command that ``prepareTransfers`` calls internally. - - References: - - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` - - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` - """ - return { - 'inputs': [ - { - 'address': - Address( - trytes = - b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' - b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', - - balance = 86, - key_index = 4, - ), + # To keep the unit test focused, we will mock the ``getInputs`` + # command that ``prepareTransfers`` calls internally. + # + # References: + # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` + mock_get_inputs = Mock(return_value={ + 'inputs': [ + { + 'address': + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', - 'balance': 86, - 'keyIndex': 4, - }, - ], + balance = 86, + key_index = 4, + ), + + 'balance': 86, + 'keyIndex': 4, + }, + ], - 'totalBalance': 86, - } + 'totalBalance': 86, + }) mock_signature_fragment_generator = MockSignatureFragmentGenerator([ TryteString( @@ -1946,13 +1926,9 @@ def mock_get_inputs(command, request): ), ]) - # noinspection PyUnusedLocal - def _create_signature_fragment_generator(bundle, key_generator, txn): - return mock_signature_fragment_generator - with patch( 'iota.transaction.ProposedBundle._create_signature_fragment_generator', - _create_signature_fragment_generator, + Mock(return_value=mock_signature_fragment_generator), ): with patch( 'iota.commands.extended.get_inputs.GetInputsCommand._execute', From 006468298d2611af604da635b939a83ef1d2585c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 16:15:29 -0500 Subject: [PATCH 213/239] Verify `prepareTransfers` w/ implicit inputs, insufficient balance. --- .../extended/prepare_transfers_test.py | 31 +++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index f4b7ff3..f5bb85e 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -2153,8 +2153,35 @@ def test_fail_inputs_implicit_insufficient(self): """ Account's total balance is not enough to cover spend amount. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + # To keep the unit test focused, we will mock the ``getInputs`` + # command that ``prepareTransfers`` calls internally. + # + # References: + # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` + mock_get_inputs = Mock(side_effect=BadApiResponse) + + with patch( + 'iota.commands.extended.get_inputs.GetInputsCommand._execute', + mock_get_inputs, + ): + with self.assertRaises(BadApiResponse): + self.command( + seed = Seed( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' + b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' + ), + + transfers = [ + ProposedTransaction( + value = 42, + address = Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + ), + ], + ) def test_pass_change_address_auto_generated(self): """ From f30763f10193d18f2701b13c2f5cbbc26bf85659 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 16:19:15 -0500 Subject: [PATCH 214/239] Impl'd remaining tests for `prepareTransfers`. --- .../extended/prepare_transfers_test.py | 329 +++++++++++++++++- 1 file changed, 327 insertions(+), 2 deletions(-) diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfers_test.py index f5bb85e..3367e92 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfers_test.py @@ -2187,8 +2187,333 @@ def test_pass_change_address_auto_generated(self): """ Preparing a bundle with an auto-generated change address. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + # To keep the unit test focused, we will mock the ``getNewAddresses`` + # command that ``prepareTransfers`` calls internally. + # + # References: + # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.get_new_addresses.GetNewAddressesCommand` + mock_get_new_addresses_command = Mock(return_value=[ + Address( + trytes = + b'TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJ' + b'RBOSBIMNTGDYKUDYYFJFGZOHORYSQPCWJRKHIOVIY', + + key_index = 5, + ), + ]) + + self.adapter.seed_response('getBalances', { + 'balances': [86], + 'duration': '1', + + 'milestone': + 'TESTVALUE9DONTUSEINPRODUCTION99999ZNIUXU' + 'FIVFBBYQHFYZYIEEWZL9VPMMKIIYTEZRRHXJXKIKF', + }) + + mock_signature_fragment_generator = MockSignatureFragmentGenerator([ + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWGHM' + b'NUXBJTOBKZFDNJEZUKCKWGUHVSU9ZJYAVSQSOFDCOIEP9LCXYLTEFMCYUJAAHLYUHQ' + b'P99S9XRWHXHRPZCWHDMIDYW9OQAWUPTFMBTJGDCWRVNVRDPIWISVYNUDWUGBPNNFZD' + b'WRVZ9FGAVSEWFXRXGGLXJTPJTJLC9JYHMFBKYAUJRAMHQHKUUZHRWZIVC9KFEEXXVN' + b'EXJRYUSFV9PEPFUDCNRRTSCZXSTUEGJKDV9UCYNZSBRDYGOKFGYKWVFCYSWBUJYVGE' + b'UXWTDGPWTWURH9RKEZRFCUUBFBPKSFONMDXWGYKWAUWVUOQVBIGQMMKQVDYAZ9SVFI' + b'UUNMHOJGRQVXZGIIPKVNNBKABGKZLRNFK9KSIHTCGYPVCWYGDS9OIZWLNINYRLGJQC' + b'UBWYMAVDWFAURLALQPMRMFRAZCMCPOWM99SGBVEZPAFAXHXNENNWXLF9ZVHZIDWBLF' + b'KVWKBUYNBXOXTVPDWAGZXIOMDAEKNMRFGZVIGIFOSHGMPIPWNOWQDMHPKOJTYYECKN' + b'GCDDTJVALGPZSX9IH9LEGQSDACLBWKNXUW9BAZSHAISUJDTPJDOASLVRXFNJJHXQTK' + b'MKZUZIMJFPOKHEQXSCJQH9JPRNZHDVVZKWTHWWFNFMHFXPUIEEA9HPHJTCJJWZPUHK' + b'AAWJQQSAIF9HRETYYPXAZ9YCFJRCXTGCOLJQA9HDLFNTVDMYPRCYPQR9MNBBAMGOJX' + b'PRFCUSIIZN9VROZDPMOKZBCILKGB9EPCXOYWLPHFXTYBCMLRVHWNQDSQUIHHTAUTZC' + b'JFQ9CO9GTONKYKMDBSREZC9SUBHYK9JDOBYDBUBUIO9TRXQLAYHDDSXGJ9NB9FKMUU' + b'US9GANWVMQLIHX9MPJGLTAOMCZTQYDYVOWXHGHYCV9VDCXHGTCOOUEXIITVKHXCSUS' + b'OIRTMEAKMTYZCMAWURNX9JOVDICICKHXQYBXKWTXWXBZVZWRIDC9YCZVSKYIKJYYMF' + b'YQRTWBNJHWXRL9JFSZAXJYYTGDYLTHLWRMBUEG9QTGNRPVTBGRYFPEJQSIWTLPGV9C' + b'CMCO9TCKLKSJEAMFKQMXEYETISVEYDOSCRZ99RFDPUQPHMQ9NVRUBXITDGFZCYQNFC' + b'SULGRHPONWJDVWT9UELEKEPQEAFKDLDNYPABC9GUASVFJBFZF9Z9CHIUNLJWHKGDYK' + b'ADLUCRNEPAIWYSX9LT9QWQRKU9WEVDPKSTSA9PPEVNTBNLN9ZOPETINXGKA9DCOHPD' + b'QMMOOOCKYVEZJ9ZJQRJHNCKRFDRPHUVPGVGQYKZBLOILZTPIX9MIBKTXOJKVAYRLSX' + b'DTOEEKLF9WWZGLSGIOQZWCJJHSBTXYWRDYVEQTCNUENYWDRLZZIVTGCXEAJDRY9OVM' + b'XJGCSQSGYFLGYDZUH9EHUDQTCXLSDPMNDYQRZYRXYXKY9GIYOSIDQPXXHKJKDQLSCU' + b'Y9FFBTPSTJFEFROCEXFFYTFYHQROAVTYKQOCOQQWBN9RKJ9JJEURKTVOECYRITTYKN' + b'OGCD9OPQ9WDMKRPIUNRAVUSLFMC9WZWHSESGLDUYHVPAX9YJOFTTFSKFHTOOQQRCPY' + b'ZKTDVCUZGBOBZKLVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VDFKWVEWV9WGXPJH' + b'UBWYXGECBPQOPOHG9YCVXDWOXTEAOFBCEEAV9JCHUVLIRIMHXMUSZPOMMRBF9PLVLR' + b'JYTXTBANBZWFQWGNGFGXFOZ9YGMQSZFEJHLFZTTVHRLJPATA9TYCM9LSEWMNEUDNWQ' + b'FLUXOFUNVDKSNIIXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVG' + b'XBBPBEBDVGZDBWMDMLPXYJBBRNOMKGPMCG9FTSLMRADFVPUTTEIOUCBLPRYZHGOICN' + b'C9BT9WHJJJPDOSOMLD9EKRGKYUHUMMCAVHGYWOVQXFLTCXAAUDYKGKGKOYHLDCCQSK' + b'NHJHPSXTJVTW9QPFOQ9FDZIDDKIVF9CDYGU9ABRESMDLIBONAQWFVGCNOTEDHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHVIT' + b'GYROZZNQP' + ), + + TryteString( + b'ZOJNUMZOBEHLYDSDAVZKXHF9MAHAJICBMJTZZHTQTCACVQAUSSCFUMGCSJTONNKXFI' + b'NPOAXQIKSJ9GUV9GXM9KYDCDWUHULIJMSKMOLDZBYE9FTGFMKLODKHFF9YUCPTYFFM' + b'9EDCJDCKRFLZUHGGYNYFJLBFWXCIUF9HMGUQKPUCJ9OQ99FXHSUSRRBEUSSCKCYPIE' + b'AFZJQNXEUYWLEXKZWLRINBEGAZTJMYTUEQTTORMIIQASISHSHZDQJXANFLKOIRUEJU' + b'PZZHUJFWHEXFIZ9OU99SQLDDNLARDFPGYSCMXQCMGPRB9QLM99QUBLTLTKWYXHVAFU' + b'VVAMHEYCCNVEITSPVQWMSEIZJSLPWNGWISKWQNXCNRNOIGRGUHGYWLOFNXBDCT9JLA' + b'9CEKW9BFGOESKGOQLJBTLUMOICBEZDHCR9SZCJUZVXIEAVITFJFDGNJII9LSW9IQKV' + b'99UJWWAACGIRPCZUENXGILUXCMJIGW9REUNA99MWSANWL9KVKKXCKXLRGDT9NXIGQV' + b'ZWG9NBQPOQKEEET9ZUSENFPGFDNNHGBITCPASGHOPBNYKKEHKHVATNVWX9ZGTISUKP' + b'KTMWMPCGVVJSGMRJWNFICSFUAVAHIZWA9PDOIXFJGWCPTZHUDDUFJVQPBYNJREQ99U' + b'HOESTT9FELDMVK9VHZYPRVOWEW9NXTCYDCIMT9UIWGXUFYILOPOCJFVVEJEJN9ULGX' + b'IABFJWWRKAD9NHZBULMWUKESZLCPRQVVKWOHEWSTLOFNA9KNERURWJPROBBXEWICDK' + b'KCQXWYMJUCQLWEUPFXRSNMIJWQUEJUNIKDYJILXCGCLFETWOZYIUZVJVYVB9YGXSSD' + b'XYXSJXTOQZ9CCCAKMCNNKQCYEDGSGTBICCOGEHRIVMICUQPUUFRFCBF9NUUWSQBTVI' + b'YFVWAASTQJZFDDWWUUIHPKTIIVAGGIEQCZUEVOFDMQLDESMQDPQUSOOKZJ9QLXTAFP' + b'XXILFHFUIFJTKSEHXXZBPTZUGLYUZNORFOEKQDEIWGXZPBXSOGGQFILUJTKDLWVKPV' + b'ISU9QOATYVKJHLDLOKROZNFAGS9CICXXIUQQVLLRPPPDYJVSCW9OWIHKADCVSKPWTE' + b'NYEWQWEHP9DDWOUJDWSTSOGYQPALFMKCTUGLSXHNYETTMYTS999SYQVQSPHQPKRJSU' + b'Y9QTABAJOJAAMGVBCSLAAOBXZOJZLIFXUYOVXBKHPFVTKKGSIHUXMBDTMGNVL9NXYC' + b'HOVTLGDICIWTCIGNRHLBZBVSXMPBFAWIXPCDJWNDUFHUVLBSPBWICZNYIUJPRRTOCS' + b'SCVPNBXEDCMHKFVDMHJTSP9JI9BXTD9ZILEEOCBMHCQRRDNL9EUKJGJ9MPQGQU9ZFY' + b'GVSNOYAEC9NWTCVEJBSXLYWTUPMXNAAWXSBIAJYSGYHGLYOMAHFTYMICZRDZTQXHAQ' + b'GVXENKIGW9XZTPBAIMZLHWAJCGY9ZDNQOTGDRCTXSJCEJVTTMVRYYKWAFYSV9WVEVC' + b'FAXJKJNUC9NQHPEXWIOHOJQEXJNLEW9GLO9AJCJXIEXDONOGKXFJ9OXXXETUEHLBXA' + b'JGFPHKAQDCRTKQBXPZYQZBQODTVIBUTSAEXMBFBMTAXOQZCOHWEWRJEKNKHZXXSO9U' + b'SZRWUPZAASWDBXOVAEGSAGYDIOZWSSEAIQVRWFDSOXSRRRQHRCWDJWZXXJOGPZRLKQ' + b'OA9DOY9RXZNWBFJTKUOVRRQNSDUOFGCUQNHOBMJSFQZXVBPHHBRRIXZNLXAH9P9EFM' + b'GRPGSCFRZINEPOQPXPKHTSRJWARXRGJGYMTPUKQISLV9GUC9VTJLOISKGUZCTZEYND' + b'TURLBPXGNQLVXHAHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHV' + b'HE9EEBQYSNWECSPAJHGLTEUCXALBRVTKMWSWCBPUMZFVSEEFIHBAGJVVQV9QLFEGGY' + b'VPNSDOBZEQGLEFLCQVPDJA9MQDRHYNVZVNTYNJ9GJCXKED9NEWTD9RVMNA9HOHUBLL' + b'ASNQSDLDZKOMFOEGBJZPYVYZCVHYFEGSVEHSWV9WAGMEQIUDZQZUACWYQLTD9LHBVK' + b'KNXXXDWQUWRJKTCDP9CEJOHLLPTWCIKKHHIFAFFDVMFZR9A9LYVMTQAPAXAVPJOZKW' + b'FQNAJTO99' + ), + ]) + + with patch( + 'iota.transaction.ProposedBundle._create_signature_fragment_generator', + Mock(return_value=mock_signature_fragment_generator), + ): + with patch( + 'iota.commands.extended.get_new_addresses.GetNewAddressesCommand._execute', + mock_get_new_addresses_command, + ): + response = self.command( + seed = Seed( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999C9V' + b'C9RHFCQAIGSFICL9HIY9ZEUATFVHFGAEUHSECGQAK' + ), + + transfers = [ + ProposedTransaction( + value = 42, + address = Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999XYY' + b'NXZLKBYNFPXA9RUGZVEGVPLLFJEM9ZZOUINE9ONOW' + ), + ), + ], + + inputs = [ + Address( + trytes = + b'TESTVALUETHREE9DONTUSEINPRODUCTION99999N' + b'UMQE9RGHNRRSKKAOSD9WEYBHIUM9LWUWKEFSQOCVW', + + key_index = 4, + ), + ], + ) + + self.assertDictEqual( + response, + + { + 'trytes': [ + # Change transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUEFOUR9DONTUSEINPRODUCTION99999WJRBOSBIMNTGDYK' + b'UDYYFJFGZOHORYSQPCWJRKHIOVIYQB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99C99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 2 of 2 + TryteString( + b'ZOJNUMZOBEHLYDSDAVZKXHF9MAHAJICBMJTZZHTQTCACVQAUSSCFUMGCSJTONNKX' + b'FINPOAXQIKSJ9GUV9GXM9KYDCDWUHULIJMSKMOLDZBYE9FTGFMKLODKHFF9YUCPT' + b'YFFM9EDCJDCKRFLZUHGGYNYFJLBFWXCIUF9HMGUQKPUCJ9OQ99FXHSUSRRBEUSSC' + b'KCYPIEAFZJQNXEUYWLEXKZWLRINBEGAZTJMYTUEQTTORMIIQASISHSHZDQJXANFL' + b'KOIRUEJUPZZHUJFWHEXFIZ9OU99SQLDDNLARDFPGYSCMXQCMGPRB9QLM99QUBLTL' + b'TKWYXHVAFUVVAMHEYCCNVEITSPVQWMSEIZJSLPWNGWISKWQNXCNRNOIGRGUHGYWL' + b'OFNXBDCT9JLA9CEKW9BFGOESKGOQLJBTLUMOICBEZDHCR9SZCJUZVXIEAVITFJFD' + b'GNJII9LSW9IQKV99UJWWAACGIRPCZUENXGILUXCMJIGW9REUNA99MWSANWL9KVKK' + b'XCKXLRGDT9NXIGQVZWG9NBQPOQKEEET9ZUSENFPGFDNNHGBITCPASGHOPBNYKKEH' + b'KHVATNVWX9ZGTISUKPKTMWMPCGVVJSGMRJWNFICSFUAVAHIZWA9PDOIXFJGWCPTZ' + b'HUDDUFJVQPBYNJREQ99UHOESTT9FELDMVK9VHZYPRVOWEW9NXTCYDCIMT9UIWGXU' + b'FYILOPOCJFVVEJEJN9ULGXIABFJWWRKAD9NHZBULMWUKESZLCPRQVVKWOHEWSTLO' + b'FNA9KNERURWJPROBBXEWICDKKCQXWYMJUCQLWEUPFXRSNMIJWQUEJUNIKDYJILXC' + b'GCLFETWOZYIUZVJVYVB9YGXSSDXYXSJXTOQZ9CCCAKMCNNKQCYEDGSGTBICCOGEH' + b'RIVMICUQPUUFRFCBF9NUUWSQBTVIYFVWAASTQJZFDDWWUUIHPKTIIVAGGIEQCZUE' + b'VOFDMQLDESMQDPQUSOOKZJ9QLXTAFPXXILFHFUIFJTKSEHXXZBPTZUGLYUZNORFO' + b'EKQDEIWGXZPBXSOGGQFILUJTKDLWVKPVISU9QOATYVKJHLDLOKROZNFAGS9CICXX' + b'IUQQVLLRPPPDYJVSCW9OWIHKADCVSKPWTENYEWQWEHP9DDWOUJDWSTSOGYQPALFM' + b'KCTUGLSXHNYETTMYTS999SYQVQSPHQPKRJSUY9QTABAJOJAAMGVBCSLAAOBXZOJZ' + b'LIFXUYOVXBKHPFVTKKGSIHUXMBDTMGNVL9NXYCHOVTLGDICIWTCIGNRHLBZBVSXM' + b'PBFAWIXPCDJWNDUFHUVLBSPBWICZNYIUJPRRTOCSSCVPNBXEDCMHKFVDMHJTSP9J' + b'I9BXTD9ZILEEOCBMHCQRRDNL9EUKJGJ9MPQGQU9ZFYGVSNOYAEC9NWTCVEJBSXLY' + b'WTUPMXNAAWXSBIAJYSGYHGLYOMAHFTYMICZRDZTQXHAQGVXENKIGW9XZTPBAIMZL' + b'HWAJCGY9ZDNQOTGDRCTXSJCEJVTTMVRYYKWAFYSV9WVEVCFAXJKJNUC9NQHPEXWI' + b'OHOJQEXJNLEW9GLO9AJCJXIEXDONOGKXFJ9OXXXETUEHLBXAJGFPHKAQDCRTKQBX' + b'PZYQZBQODTVIBUTSAEXMBFBMTAXOQZCOHWEWRJEKNKHZXXSO9USZRWUPZAASWDBX' + b'OVAEGSAGYDIOZWSSEAIQVRWFDSOXSRRRQHRCWDJWZXXJOGPZRLKQOA9DOY9RXZNW' + b'BFJTKUOVRRQNSDUOFGCUQNHOBMJSFQZXVBPHHBRRIXZNLXAH9P9EFMGRPGSCFRZI' + b'NEPOQPXPKHTSRJWARXRGJGYMTPUKQISLV9GUC9VTJLOISKGUZCTZEYNDTURLBPXG' + b'NQLVXHAHUVNGIHVMZOHLEUBDTRFXFXXVRYBRUF9ULNMSZZOZBYDJUWTMHVHE9EEB' + b'QYSNWECSPAJHGLTEUCXALBRVTKMWSWCBPUMZFVSEEFIHBAGJVVQV9QLFEGGYVPNS' + b'DOBZEQGLEFLCQVPDJA9MQDRHYNVZVNTYNJ9GJCXKED9NEWTD9RVMNA9HOHUBLLAS' + b'NQSDLDZKOMFOEGBJZPYVYZCVHYFEGSVEHSWV9WAGMEQIUDZQZUACWYQLTD9LHBVK' + b'KNXXXDWQUWRJKTCDP9CEJOHLLPTWCIKKHHIFAFFDVMFZR9A9LYVMTQAPAXAVPJOZ' + b'KWFQNAJTO99TESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVW999999999999999999999999999999999999' + b'999999999999999999NYBKIVD99B99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Input #1, Part 1 of 2 + TryteString( + b'OGTAZHXTC9FFCADHPLXKNQPKBWWOJGDCEKSHUPGLOFGXRNDRUWGKN9TYYKWVEWWG' + b'HMNUXBJTOBKZFDNJEZUKCKWGUHVSU9ZJYAVSQSOFDCOIEP9LCXYLTEFMCYUJAAHL' + b'YUHQP99S9XRWHXHRPZCWHDMIDYW9OQAWUPTFMBTJGDCWRVNVRDPIWISVYNUDWUGB' + b'PNNFZDWRVZ9FGAVSEWFXRXGGLXJTPJTJLC9JYHMFBKYAUJRAMHQHKUUZHRWZIVC9' + b'KFEEXXVNEXJRYUSFV9PEPFUDCNRRTSCZXSTUEGJKDV9UCYNZSBRDYGOKFGYKWVFC' + b'YSWBUJYVGEUXWTDGPWTWURH9RKEZRFCUUBFBPKSFONMDXWGYKWAUWVUOQVBIGQMM' + b'KQVDYAZ9SVFIUUNMHOJGRQVXZGIIPKVNNBKABGKZLRNFK9KSIHTCGYPVCWYGDS9O' + b'IZWLNINYRLGJQCUBWYMAVDWFAURLALQPMRMFRAZCMCPOWM99SGBVEZPAFAXHXNEN' + b'NWXLF9ZVHZIDWBLFKVWKBUYNBXOXTVPDWAGZXIOMDAEKNMRFGZVIGIFOSHGMPIPW' + b'NOWQDMHPKOJTYYECKNGCDDTJVALGPZSX9IH9LEGQSDACLBWKNXUW9BAZSHAISUJD' + b'TPJDOASLVRXFNJJHXQTKMKZUZIMJFPOKHEQXSCJQH9JPRNZHDVVZKWTHWWFNFMHF' + b'XPUIEEA9HPHJTCJJWZPUHKAAWJQQSAIF9HRETYYPXAZ9YCFJRCXTGCOLJQA9HDLF' + b'NTVDMYPRCYPQR9MNBBAMGOJXPRFCUSIIZN9VROZDPMOKZBCILKGB9EPCXOYWLPHF' + b'XTYBCMLRVHWNQDSQUIHHTAUTZCJFQ9CO9GTONKYKMDBSREZC9SUBHYK9JDOBYDBU' + b'BUIO9TRXQLAYHDDSXGJ9NB9FKMUUUS9GANWVMQLIHX9MPJGLTAOMCZTQYDYVOWXH' + b'GHYCV9VDCXHGTCOOUEXIITVKHXCSUSOIRTMEAKMTYZCMAWURNX9JOVDICICKHXQY' + b'BXKWTXWXBZVZWRIDC9YCZVSKYIKJYYMFYQRTWBNJHWXRL9JFSZAXJYYTGDYLTHLW' + b'RMBUEG9QTGNRPVTBGRYFPEJQSIWTLPGV9CCMCO9TCKLKSJEAMFKQMXEYETISVEYD' + b'OSCRZ99RFDPUQPHMQ9NVRUBXITDGFZCYQNFCSULGRHPONWJDVWT9UELEKEPQEAFK' + b'DLDNYPABC9GUASVFJBFZF9Z9CHIUNLJWHKGDYKADLUCRNEPAIWYSX9LT9QWQRKU9' + b'WEVDPKSTSA9PPEVNTBNLN9ZOPETINXGKA9DCOHPDQMMOOOCKYVEZJ9ZJQRJHNCKR' + b'FDRPHUVPGVGQYKZBLOILZTPIX9MIBKTXOJKVAYRLSXDTOEEKLF9WWZGLSGIOQZWC' + b'JJHSBTXYWRDYVEQTCNUENYWDRLZZIVTGCXEAJDRY9OVMXJGCSQSGYFLGYDZUH9EH' + b'UDQTCXLSDPMNDYQRZYRXYXKY9GIYOSIDQPXXHKJKDQLSCUY9FFBTPSTJFEFROCEX' + b'FFYTFYHQROAVTYKQOCOQQWBN9RKJ9JJEURKTVOECYRITTYKNOGCD9OPQ9WDMKRPI' + b'UNRAVUSLFMC9WZWHSESGLDUYHVPAX9YJOFTTFSKFHTOOQQRCPYZKTDVCUZGBOBZK' + b'LVBVBCWTUS9XOBJADZYN9TMLGCKXEXFEQFQ9VDFKWVEWV9WGXPJHUBWYXGECBPQO' + b'POHG9YCVXDWOXTEAOFBCEEAV9JCHUVLIRIMHXMUSZPOMMRBF9PLVLRJYTXTBANBZ' + b'WFQWGNGFGXFOZ9YGMQSZFEJHLFZTTVHRLJPATA9TYCM9LSEWMNEUDNWQFLUXOFUN' + b'VDKSNIIXCXVUYZZOKVYNNQDZVUQEQFWVF9EIQELSWDJXGMQRVUGGVBMRVGXBBPBE' + b'BDVGZDBWMDMLPXYJBBRNOMKGPMCG9FTSLMRADFVPUTTEIOUCBLPRYZHGOICNC9BT' + b'9WHJJJPDOSOMLD9EKRGKYUHUMMCAVHGYWOVQXFLTCXAAUDYKGKGKOYHLDCCQSKNH' + b'JHPSXTJVTW9QPFOQ9FDZIDDKIVF9CDYGU9ABRESMDLIBONAQWFVGCNOTEDHBMCSU' + b'H9GKYZPBX9QJELYYMSGDFU9EVTROODXVUAELBUKKXCDYNMHYBVAVUYGABCRIYOHV' + b'ITGYROZZNQPTESTVALUETHREE9DONTUSEINPRODUCTION99999NUMQE9RGHNRRSK' + b'KAOSD9WEYBHIUM9LWUWKEFSQOCVWVX9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99A99999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + # Spend transaction, Part 1 of 1 + TryteString( + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUETWO9DONTUSEINPRODUCTION99999XYYNXZLKBYNFPXA9' + b'RUGZVEGVPLLFJEM9ZZOUINE9ONOWOB9999999999999999999999999999999999' + b'999999999999999999NYBKIVD99999999999C99999999VEUNVMI9BSZTFZMGEZJ' + b'CPMPOTRTUR9PSISHCXAESJQU9CEYAGXVHBAXAFRWHQNAFHGNID9BAOMKSJJDEO99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) class MockSignatureFragmentGenerator(object): From e371b97c8cde700be7467a7e86711665fcf7c0ce Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 28 Dec 2016 16:43:38 -0500 Subject: [PATCH 215/239] Renamed `prepareTransfers` to `prepareTransfer`. --- iota/api.py | 4 +-- ...epare_transfers.py => prepare_transfer.py} | 16 ++++----- iota/commands/extended/send_transfer.py | 4 +-- iota/transaction.py | 2 +- ...sfers_test.py => prepare_transfer_test.py} | 36 +++++++++---------- 5 files changed, 31 insertions(+), 31 deletions(-) rename iota/commands/extended/{prepare_transfers.py => prepare_transfer.py} (91%) rename test/commands/extended/{prepare_transfers_test.py => prepare_transfer_test.py} (99%) diff --git a/iota/api.py b/iota/api.py index 8235285..527c925 100644 --- a/iota/api.py +++ b/iota/api.py @@ -508,7 +508,7 @@ def get_transfers(self, start=0, end=None, inclusion_states=False): inclusion_states = inclusion_states, ) - def prepare_transfers(self, transfers, inputs=None, change_address=None): + def prepare_transfer(self, transfers, inputs=None, change_address=None): # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address]) -> dict """ Prepares transactions to be broadcast to the Tangle, by generating @@ -544,7 +544,7 @@ def prepare_transfers(self, transfers, inputs=None, change_address=None): References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers """ - return self.prepareTransfers( + return self.prepareTransfer( seed = self.seed, transfers = transfers, inputs = inputs, diff --git a/iota/commands/extended/prepare_transfers.py b/iota/commands/extended/prepare_transfer.py similarity index 91% rename from iota/commands/extended/prepare_transfers.py rename to iota/commands/extended/prepare_transfer.py index a47707e..b0a516c 100644 --- a/iota/commands/extended/prepare_transfers.py +++ b/iota/commands/extended/prepare_transfer.py @@ -17,20 +17,20 @@ from iota.filters import GeneratedAddress, Trytes __all__ = [ - 'PrepareTransfersCommand', + 'PrepareTransferCommand', ] -class PrepareTransfersCommand(FilterCommand): +class PrepareTransferCommand(FilterCommand): """ - Executes ``prepareTransfers`` extended API command. + Executes ``prepareTransfer`` extended API command. - See :py:meth:`iota.api.Iota.prepare_transfers` for more info. + See :py:meth:`iota.api.Iota.prepare_transfer` for more info. """ - command = 'prepareTransfers' + command = 'prepareTransfer' def get_request_filter(self): - return PrepareTransfersRequestFilter() + return PrepareTransferRequestFilter() def get_response_filter(self): pass @@ -117,9 +117,9 @@ def _execute(self, request): } -class PrepareTransfersRequestFilter(RequestFilter): +class PrepareTransferRequestFilter(RequestFilter): def __init__(self): - super(PrepareTransfersRequestFilter, self).__init__( + super(PrepareTransferRequestFilter, self).__init__( { # Required parameters. 'seed': f.Required | Trytes(result_type=Seed), diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index 5b8abd7..cf4a0b5 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -8,7 +8,7 @@ from iota import Address, Bundle, ProposedTransaction from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ RequestFilter -from iota.commands.extended.prepare_transfers import PrepareTransfersCommand +from iota.commands.extended.prepare_transfer import PrepareTransferCommand from iota.commands.extended.send_trytes import SendTrytesCommand from iota.crypto.types import Seed from iota.filters import Trytes @@ -40,7 +40,7 @@ def _execute(self, request): seed = request['seed'] # type: Seed transfers = request['transfers'] # type: List[ProposedTransaction] - prepared_trytes = PrepareTransfersCommand(self.adapter)( + prepared_trytes = PrepareTransferCommand(self.adapter)( change_address = change_address, inputs = inputs, seed = seed, diff --git a/iota/transaction.py b/iota/transaction.py index 15b7cfe..af06ab4 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -291,7 +291,7 @@ class ProposedTransaction(Transaction): """ A transaction that has not yet been attached to the Tangle. - Provide to :py:meth:`iota.api.Iota.prepare_transfers` to attach to + Provide to :py:meth:`iota.api.Iota.send_transfer` to attach to tangle and publish/store. """ MESSAGE_LEN = 2187 diff --git a/test/commands/extended/prepare_transfers_test.py b/test/commands/extended/prepare_transfer_test.py similarity index 99% rename from test/commands/extended/prepare_transfers_test.py rename to test/commands/extended/prepare_transfer_test.py index 3367e92..8e0d6a3 100644 --- a/test/commands/extended/prepare_transfers_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -11,7 +11,7 @@ from iota import Address, BadApiResponse, Iota, ProposedTransaction, Tag, \ TryteString -from iota.commands.extended.prepare_transfers import PrepareTransfersCommand +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 @@ -19,13 +19,13 @@ from test import MockAdapter -class PrepareTransfersRequestFilterTestCase(BaseFilterTestCase): - filter_type = PrepareTransfersCommand(MockAdapter()).get_request_filter +class PrepareTransferRequestFilterTestCase(BaseFilterTestCase): + filter_type = PrepareTransferCommand(MockAdapter()).get_request_filter skip_value_check = True # noinspection SpellCheckingInspection def setUp(self): - super(PrepareTransfersRequestFilterTestCase, self).setUp() + super(PrepareTransferRequestFilterTestCase, self).setUp() # Define some tryte sequences that we can reuse between tests. self.trytes1 = ( @@ -394,12 +394,12 @@ def test_fail_inputs_contents_invalid(self): # noinspection SpellCheckingInspection -class PrepareTransfersCommandTestCase(TestCase): +class PrepareTransferCommandTestCase(TestCase): def setUp(self): - super(PrepareTransfersCommandTestCase, self).setUp() + super(PrepareTransferCommandTestCase, self).setUp() self.adapter = MockAdapter() - self.command = PrepareTransfersCommand(self.adapter) + self.command = PrepareTransferCommand(self.adapter) def run(self, result=None): # Ensure that all tranactions use a predictable timestamp. @@ -412,15 +412,15 @@ def get_current_timestamp(): target = 'iota.transaction.get_current_timestamp', new = get_current_timestamp, ): - return super(PrepareTransfersCommandTestCase, self).run(result) + return super(PrepareTransferCommandTestCase, self).run(result) def test_wireup(self): """ Verify that the command is wired up correctly. """ self.assertIsInstance( - Iota(self.adapter).prepareTransfers, - PrepareTransfersCommand, + Iota(self.adapter).prepareTransfer, + PrepareTransferCommand, ) def test_pass_inputs_not_needed(self): @@ -1364,10 +1364,10 @@ def test_pass_inputs_implicit_no_change(self): change address needed. """ # To keep the unit test focused, we will mock the ``getInputs`` - # command that ``prepareTransfers`` calls internally. + # command that ``prepareTransfer`` calls internally. # # References: - # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.prepare_transfer.PrepareTransferCommand` # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` mock_get_inputs = Mock(return_value={ 'inputs': [ @@ -1824,10 +1824,10 @@ def test_pass_inputs_implicit_with_change(self): address needed. """ # To keep the unit test focused, we will mock the ``getInputs`` - # command that ``prepareTransfers`` calls internally. + # command that ``prepareTransfer`` calls internally. # # References: - # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.prepare_transfer.PrepareTransferCommand` # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` mock_get_inputs = Mock(return_value={ 'inputs': [ @@ -2154,10 +2154,10 @@ def test_fail_inputs_implicit_insufficient(self): Account's total balance is not enough to cover spend amount. """ # To keep the unit test focused, we will mock the ``getInputs`` - # command that ``prepareTransfers`` calls internally. + # command that ``prepareTransfer`` calls internally. # # References: - # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.prepare_transfer.PrepareTransferCommand` # - :py:class:`iota.commands.extended.get_inputs.GetInputsCommand` mock_get_inputs = Mock(side_effect=BadApiResponse) @@ -2188,10 +2188,10 @@ def test_pass_change_address_auto_generated(self): Preparing a bundle with an auto-generated change address. """ # To keep the unit test focused, we will mock the ``getNewAddresses`` - # command that ``prepareTransfers`` calls internally. + # command that ``prepareTransfer`` calls internally. # # References: - # - :py:class:`iota.commands.extended.prepare_transfers.PrepareTransfersCommand` + # - :py:class:`iota.commands.extended.prepare_transfer.PrepareTransferCommand` # - :py:class:`iota.commands.extended.get_new_addresses.GetNewAddressesCommand` mock_get_new_addresses_command = Mock(return_value=[ Address( From 9f048d0f3a96123e1ff83779bff5b705a42df5c4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 11:20:33 -0500 Subject: [PATCH 216/239] Made `getBundle` functionality consistent w/ docs. --- iota/api.py | 19 ++++++++++--------- iota/commands/extended/get_bundles.py | 6 +++++- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/iota/api.py b/iota/api.py index 527c925..bec0c0c 100644 --- a/iota/api.py +++ b/iota/api.py @@ -4,8 +4,8 @@ from typing import Dict, Iterable, List, Optional, Text -from iota import AdapterSpec, Address, Bundle, ProposedBundle, \ - ProposedTransaction, Tag, TransactionHash, TryteString, TrytesCompatible +from iota import AdapterSpec, Address, Bundle, ProposedTransaction, Tag, \ + TransactionHash, TryteString, TrytesCompatible from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import CustomCommand, DEFAULT_MIN_WEIGHT_MAGNITUDE, \ command_registry @@ -343,7 +343,7 @@ def broadcast_and_store(self, trytes): return self.broadcastAndStore(trytes=trytes) def get_bundles(self, transaction): - # type: (TransactionHash) -> List[Bundle] + # type: (TransactionHash) -> dict """ Returns the bundle(s) associated with the specified transaction hash. @@ -353,12 +353,13 @@ def get_bundles(self, transaction): tail). :return: - List of bundles associated with the transaction. - If there are multiple bundles (e.g., because of a replay), all - valid matching bundles will be returned. + Dict with the following structure:: - Note that this method always returns a list, even if only one - bundle was found. + { + 'bundles': List[Bundle] + List of matching bundles. Note that this value is always + a list, even if only one bundle was found. + } :raise: - :py:class:`iota.adapter.BadApiResponse` if any of the @@ -400,7 +401,7 @@ def get_inputs(self, start=None, end=None, threshold=None): reached, an exception is raised. :return: - Dict with the following keys:: + Dict with the following structure:: { 'inputs': [ diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index b1115b9..cfa6316 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -52,7 +52,11 @@ def _execute(self, request): }, ) - return bundle + return { + # Always return a list, so that we have the necessary structure + # to return multiple bundles in a future iteration. + 'bundles': [bundle], + } def _traverse_bundle(self, txn_hash, target_bundle_hash=None): # type: (TransactionHash, Optional[BundleHash]) -> List[Transaction] From 65729402cbb033d9682c82a4b893e965193e4afa Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 11:48:18 -0500 Subject: [PATCH 217/239] Added support for unicode string <-> TryteString. --- iota/types.py | 37 +++++++++++++++++++++++++++- test/types_test.py | 60 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/iota/types.py b/iota/types.py index db627ae..5627268 100644 --- a/iota/types.py +++ b/iota/types.py @@ -94,6 +94,16 @@ def from_bytes(cls, bytes_): """ return cls(encode(bytes_, 'trytes')) + @classmethod + def from_string(cls, string): + # type: (Text) -> TryteString + """ + Creates a TryteString from a Unicode string. + + Note: The string will be encoded using UTF-8. + """ + return cls.from_bytes(string.encode('utf-8')) + @classmethod def from_trytes(cls, trytes): # type: (Iterable[Iterable[int]]) -> TryteString @@ -359,13 +369,38 @@ def as_bytes(self, errors='strict'): :param errors: How to handle trytes that can't be converted: - - 'strict': raise a TrytesDecodeError. + - 'strict': raise an exception. - 'replace': replace with '?'. - 'ignore': omit the tryte from the byte string. + + :raise: + - :py:class:`iota.codecs.TrytesDecodeError` if the trytes cannot + be decoded into bytes. """ # :bc: In Python 2, `decode` does not accept keyword arguments. return decode(self._trytes, 'trytes', errors) + def as_string(self, errors='strict'): + # type: (Text) -> Text + """ + Attempts to interpret the TryteString as a UTF-8 encoded Unicode + string. + + :param errors: + How to handle trytes that can't be converted, or bytes that can't + be decoded using UTF-8: + - 'strict': raise an exception. + - 'replace': replace with a placeholder character. + - 'ignore': omit the invalid tryte/byte sequence. + + :raise: + - :py:class:`iota.codecs.TrytesDecodeError` if the trytes cannot + be decoded into bytes. + - :py:class:`UnicodeDecodeError` if the resulting bytes cannot be + decoded using UTF-8. + """ + return self.as_bytes(errors).decode('utf-8', errors) + def as_json_compatible(self): # type: () -> Text """ diff --git a/test/types_test.py b/test/types_test.py index 31c41cb..155d0d5 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -411,6 +411,57 @@ def test_as_bytes_non_ascii_errors_replace(self): b'??\xd2\x80??\xc3??', ) + def test_as_string(self): + """ + Converting a sequence of trytes into a Unicode string. + """ + trytes = TryteString(b'LH9GYEMHCF9GWHZFEELHVFOEOHNEEEWHZFUD') + + self.assertEqual(trytes.as_string(), '你好,世界!') + + def test_as_string_not_utf8_errors_strict(self): + """ + The tryte sequence does not represent a valid UTF-8 sequence, and + errors='strict'. + """ + # Chop off a couple of trytes to break up a multi-byte sequence. + trytes = TryteString.from_string('你好,世界!')[:-2] + + # Note the exception type. The trytes were decoded to bytes + # successfully; the exception occurred while trying to decode the + # bytes into Unicode code points. + with self.assertRaises(UnicodeDecodeError): + trytes.as_string('strict') + + def test_as_string_not_utf8_errors_ignore(self): + """ + The tryte sequence does not represent a valid UTF-8 sequence, and + errors='ignore'. + """ + # Chop off a couple of trytes to break up a multi-byte sequence. + trytes = TryteString.from_string('你好,世界!')[:-2] + + self.assertEqual( + trytes.as_string('ignore'), + '你好,世界', + ) + + def test_as_string_not_utf8_errors_replace(self): + """ + The tryte sequence does not represent a valid UTF-8 sequence, and + errors='replace'. + """ + # Chop off a couple of trytes to break up a multi-byte sequence. + trytes = TryteString.from_string('你好,世界!')[:-2] + + self.assertEqual( + trytes.as_string('replace'), + + # Note that the replacement character is the Unicode replacement + # character, not '?'. + '你好,世界�', + ) + def test_as_trytes_single_tryte(self): """ Converting a single-tryte TryteString into a sequence of tryte @@ -579,6 +630,15 @@ def test_from_bytes_random(self): # errors were generated. self.assertEqual(trytes.as_bytes(), bytes_) + def test_from_string(self): + """ + Converting a Unicode string into a TryteString. + """ + self.assertEqual( + binary_type(TryteString.from_string('你好,世界!')), + b'LH9GYEMHCF9GWHZFEELHVFOEOHNEEEWHZFUD', + ) + def test_from_trytes(self): """ Converting a sequence of tryte values into a TryteString. From f53cc696a12fa0a879bc24021dedcb2f24884930 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 12:29:12 -0500 Subject: [PATCH 218/239] Added support for short messages to `prepareTransfer`. --- iota/transaction.py | 13 ++- .../extended/prepare_transfer_test.py | 87 ++++++++++++++++++- 2 files changed, 95 insertions(+), 5 deletions(-) diff --git a/iota/transaction.py b/iota/transaction.py index af06ab4..1a5ee6e 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -305,13 +305,13 @@ class ProposedTransaction(Transaction): """ def __init__(self, address, value, tag=None, message=None, timestamp=None): - # type: (Address, int, Optional[Tag], Optional[TrytesCompatible], Optional[int]) -> None + # type: (Address, int, Optional[Tag], Optional[TryteString], Optional[int]) -> None if not timestamp: timestamp = get_current_timestamp() super(ProposedTransaction, self).__init__( address = address, - tag = Tag(tag or b''), + tag = Tag(b'') if tag is None else tag, timestamp = timestamp, value = value, @@ -320,7 +320,7 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): current_index = None, hash_ = None, last_index = None, - signature_message_fragment = TryteString(b'', pad=self.MESSAGE_LEN), + signature_message_fragment = None, # These values start out empty; they will be populated when the # node does PoW. @@ -329,7 +329,7 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): trunk_transaction_hash = TransactionHash(b''), ) - self.message = TryteString(message or b'', pad=self.MESSAGE_LEN) + self.message = TryteString(b'') if message is None else message def as_tryte_string(self): # type: () -> TryteString @@ -738,6 +738,11 @@ def finalize(self): for txn in self: txn.bundle_hash = self.hash + # Initialize signature/message fragment. + txn.signature_message_fragment = ( + TryteString(txn.message or b'', pad=ProposedTransaction.MESSAGE_LEN) + ) + def sign_inputs(self, key_generator): # type: (KeyGenerator) -> None """ diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index 8e0d6a3..d28b2af 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -432,12 +432,13 @@ def test_pass_inputs_not_needed(self): transfers = [ ProposedTransaction( + tag = Tag(b'PYOTA9UNIT9TESTS9'), value = 0, + address = Address( b'TESTVALUE9DONTUSEINPRODUCTION99999KJUPKN' b'RMTHKVJYWNBKBGCKOQWBTKBOBJIZZYQITTFJZKLOI' ), - tag = Tag(b'PYOTA9UNIT9TESTS9'), ), ProposedTransaction( @@ -2515,6 +2516,90 @@ def test_pass_change_address_auto_generated(self): }, ) + def test_pass_message_short(self): + """ + Adding a message to a transaction. + """ + response = self.command( + seed = Seed.random(), + + transfers = [ + ProposedTransaction( + tag = Tag(b'PYOTA9UNIT9TESTS9'), + value = 0, + + address = Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999YMSWGX' + b'VNDMLXPT9HMVAOWUUZMLSJZFWGKDVGXPSQAWAEBJN' + ), + + message = TryteString.from_string('สวัสดีชาวโลก!'), + ), + ], + ) + + self.assertDictEqual( + response, + + { + 'trytes': [ + TryteString( + # Note that the tryte sequence starts with the transaction + # message. + b'HHVFHFHHVFEFHHVFOFHHVFHFHHVFMEHHVFSFHHVFCEHHVFPFHHVFEFHHWFVDHHVF' + b'CFHHVFUDFA999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVA' + b'OWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999PYOTA9UNI' + b'T9TESTS99999999999NYBKIVD99999999999999999999D9XYVJTKVWN9RUQAPIO' + b'JUXXTOQTWNMOKRKLUURUGERIIZLUURHPQWZMSYROAKYLZJEKSAMLRCVWEDINFK99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) + + def test_pass_message_long(self): + """ + The message is too long to fit into a single transaction. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + class MockSignatureFragmentGenerator(object): """ From e1dd453ea9e1d0d5c2ad72ebed6036d15cdbaacc Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 12:50:14 -0500 Subject: [PATCH 219/239] `prepareTransfer` can handle really long messages, too. --- .../extended/prepare_transfer_test.py | 186 +++++++++++++++++- 1 file changed, 184 insertions(+), 2 deletions(-) diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index d28b2af..226637a 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -2597,8 +2597,190 @@ def test_pass_message_long(self): """ The message is too long to fit into a single transaction. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + response = self.command( + seed = Seed.random(), + + transfers = [ + ProposedTransaction( + tag = Tag(b'PYOTA9UNIT9TESTS9'), + value = 0, + + address = Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999YMSWGX' + b'VNDMLXPT9HMVAOWUUZMLSJZFWGKDVGXPSQAWAEBJN' + ), + + message = TryteString.from_string( + 'Вы не можете справиться правду! Сын, мы живем в мире, который ' + 'имеет стены. И эти стены должны быть охраняют люди с оружием. ' + 'Кто будет это делать? Вы? Вы, лейтенант Weinberg? У меня есть ' + 'большая ответственность, чем вы можете понять. Ты плачешь ' + 'Сантьяго и прокляни морских пехотинцев. У вас есть такой роскоши. ' + 'У вас есть роскошь, не зная, что я знаю: что смерть Сантьяго, в ' + 'то время как трагический, вероятно, спас жизнь. И мое ' + 'существование, в то время как гротеск и непонятными для вас, ' + 'спасает жизни ... Вы не хотите знать правду. Потому что в ' + 'глубине души, в тех местах, вы не говорите о на вечеринках, вы ' + 'хотите меня на этой стене. Вы должны меня на этой стене. Мы ' + 'используем такие слова, как честь, код, верность ... мы ' + 'используем эти слова в качестве основы к жизни провел, защищая ' + 'что-то. Вы можете использовать им, как пуанта. У меня нет ни ' + 'времени, ни желания, чтобы объясниться с человеком, который ' + 'поднимается и спит под одеялом самой свободы я обеспечиваю, то ' + 'ставит под сомнение то, каким образом я предоставить ему! Я бы ' + 'предпочел, чтобы вы просто сказал спасибо и пошел на своем пути. ' + 'В противном случае, я предлагаю вам подобрать оружие и встать ' + 'пост. В любом случае, я не наплевать, что вы думаете, что вы ' + 'имеете право!' + ), + ), + ], + ) + + self.assertDictEqual( + response, + + { + 'trytes': [ + # The message is so long that it has to be split across three + # separate transactions! + TryteString( + b'EASGBGTGTDSGNFSGPFSGAGFA9999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'99999999999TESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVA' + b'OWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999PYOTA9UNI' + b'T9TESTS99999999999NYBKIVD99B99999999B99999999YJVDLFI9FFXKNVTUKHO' + b'PTZUWZPOTRTHNZ9YZDXFRVBAUGO9APIQQWFSCLGFQMLMVCEPCTBFAVMIIXHUPG99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + TryteString( + b'FSG9GTGHEEASG9GSGNFEATGFETGVDSGAGSGWFEATGUDTGVDSGSFSG9GSGSFSAEAS' + b'GKETGDEEASGRFSGAGSGYFSGTFSG9GTGDEEASGZFSGSFSG9GTGHEEASG9GSGNFEAT' + b'GFETGVDSGAGSGWFEATGUDTGVDSGSFSG9GSGSFSAEASGUETGDEEASGVFTGUDSGBGS' + b'GAGSGYFTGEESGUFTGWDSGSFSGZFEATGVDSGNFSGXFSGVFSGSFEATGUDSGYFSGAGS' + b'GPFSGNFQAEASGXFSGNFSGXFEATG9ESGSFTGUDTGVDTGEEQAEASGXFSGAGSGRFQAE' + b'ASGPFSGSFTGTDSG9GSGAGTGUDTGVDTGEEEASASASAEASGZFTGDEEASGVFTGUDSGB' + b'GSGAGSGYFTGEESGUFTGWDSGSFSGZFEATGFETGVDSGVFEATGUDSGYFSGAGSGPFSGN' + b'FEASGPFEASGXFSGNFTG9ESGSFTGUDTGVDSGPFSGSFEASGAGTGUDSG9GSGAGSGPFT' + b'GDEEASGXFEASGTFSGVFSGUFSG9GSGVFEASGBGTGTDSGAGSGPFSGSFSGYFQAEASGU' + b'FSGNFTGBESGVFTGBESGNFTGHEEATG9ETGVDSGAGRATGVDSGAGSAEASGKETGDEEAS' + b'GZFSGAGSGTFSGSFTGVDSGSFEASGVFTGUDSGBGSGAGSGYFTGEESGUFSGAGSGPFSGN' + b'FTGVDTGEEEASGVFSGZFQAEASGXFSGNFSGXFEASGBGTGWDSGNFSG9GTGVDSGNFSAE' + b'ASGAFEASGZFSGSFSG9GTGHEEASG9GSGSFTGVDEASG9GSGVFEASGPFTGTDSGSFSGZ' + b'FSGSFSG9GSGVFQAEASG9GSGVFEASGTFSGSFSGYFSGNFSG9GSGVFTGHEQAEATG9ET' + b'GVDSGAGSGOFTGDEEASGAGSGOFTGCETGHETGUDSG9GSGVFTGVDTGEETGUDTGHEEAT' + b'GUDEATG9ESGSFSGYFSGAGSGPFSGSFSGXFSGAGSGZFQAEASGXFSGAGTGVDSGAGTGT' + b'DTGDESGWFEASGBGSGAGSGRFSG9GSGVFSGZFSGNFSGSFTGVDTGUDTGHEEASGVFEAT' + b'GUDSGBGSGVFTGVDEASGBGSGAGSGRFEASGAGSGRFSGSFTGHESGYFSGAGSGZFEATGU' + b'DSGNFSGZFSGAGSGWFEATGUDSGPFSGAGSGOFSGAGSGRFTGDEEATGHEEASGAGSGOFS' + b'GSFTGUDSGBGSGSFTG9ESGVFSGPFSGNFTGGEQAEATGVDSGAGEATGUDTGVDSGNFSGP' + b'FSGVFTGVDEASGBGSGAGSGRFEATGUDSGAGSGZFSG9GSGSFSG9GSGVFSGSFEATGVDS' + b'GAGQAEASGXFSGNFSGXFSGVFSGZFEASGAGSGOFTGTDSGNFSGUFSGAGSGZFEATGHEE' + b'ASGBGTGTDSGSFSGRFSGAGTGUDTGVDSGNFSGPFSGVFTGVDTGEEEASGSFSGZFTGWDF' + b'AEASGMFEASGOFTGDEEASGBGTGTDSGSFSGRFSGBGSGAGTG9ESGSFSGYFQAEATG9ET' + b'GVDSGAGSGOFTGDEEASGPFTGDEEASGBGTGTDSGAGTGUDTGVDSGAGEATGUDSGXFSGN' + b'FSGUFSGNFSGYFEATGUDSGBGSGNFTGUDSGVFSGOFSGAGEASGVFEASGBGSGAGTGAES' + b'GSFSGYFEASG9GSGNFEATGUDSGPFSGAGSGSFSGZFEASGBGTGWDTGVDSGVFSAEASGK' + b'EEASGBGTGTDSGAGTGVDSGVFSGPFSG9GSGAGSGZFEATGUDSGYFTGWDTG9ESGNFSGS' + b'FQAEATGHEEASGBGTGTDSGSFSGRFSGYFSGNFSGQFSGNFTGGEEASGPFSGNFSGZFEAS' + b'GBGSGAGSGRFSGAGSGOFTGTDSGNFTGVDTGEEEASGAGTGTDTGWDSGTFSGVFSGSFEAS' + b'GVFEASGPFTGUDTGVDSGNFTGVDTGEEEASGBGSGAGTGUDTGVDSAEASGKEEASGYFTGG' + b'ESGOFSGAGSGZFEATGUDSGYFTGWDTG9ESGNFSGSFQAEATGHEEASG9GSGSFEASG9GS' + b'GNFSGBGSGYFSGSFSGPFSGNFTGVDTGEEQAEATG9ETGVDSGAGEASGPFTGDEEASGRFT' + b'GWDSGZFSGNFSGSFTGVDSGSFQAEATG9ETGVDSGAGEASGPFTGDEEASGVFSGZFSGSFS' + b'GSFTGVDSGSFTESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVA' + b'OWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999PYOTA9UNI' + b'T9TESTS99999999999NYBKIVD99A99999999B99999999YJVDLFI9FFXKNVTUKHO' + b'PTZUWZPOTRTHNZ9YZDXFRVBAUGO9APIQQWFSCLGFQMLMVCEPCTBFAVMIIXHUPG99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + + TryteString( + b'SGKETGDEEASG9GSGSFEASGZFSGAGSGTFSGSFTGVDSGSFEATGUDSGBGTGTDSGNFSG' + b'PFSGVFTGVDTGEETGUDTGHEEASGBGTGTDSGNFSGPFSGRFTGWDFAEASGZETGDESG9G' + b'QAEASGZFTGDEEASGTFSGVFSGPFSGSFSGZFEASGPFEASGZFSGVFTGTDSGSFQAEASG' + b'XFSGAGTGVDSGAGTGTDTGDESGWFEASGVFSGZFSGSFSGSFTGVDEATGUDTGVDSGSFSG' + b'9GTGDESAEASGQEEATGFETGVDSGVFEATGUDTGVDSGSFSG9GTGDEEASGRFSGAGSGYF' + b'SGTFSG9GTGDEEASGOFTGDETGVDTGEEEASGAGTGYDTGTDSGNFSG9GTGHETGGETGVD' + b'EASGYFTGGESGRFSGVFEATGUDEASGAGTGTDTGWDSGTFSGVFSGSFSGZFSAEASGSETG' + b'VDSGAGEASGOFTGWDSGRFSGSFTGVDEATGFETGVDSGAGEASGRFSGSFSGYFSGNFTGVD' + b'TGEEIBEASGKETGDEIBEASGKETGDEQAEASGYFSGSFSGWFTGVDSGSFSG9GSGNFSG9G' + b'TGVDEAFCTCXCBDQCTCFDVCIBEASGAFEASGZFSGSFSG9GTGHEEASGSFTGUDTGVDTG' + b'EEEASGOFSGAGSGYFTGEETGAESGNFTGHEEASGAGTGVDSGPFSGSFTGVDTGUDTGVDSG' + b'PFSGSFSG9GSG9GSGAGTGUDTGVDTGEEQAEATG9ESGSFSGZFEASGPFTGDEEASGZFSG' + b'AGSGTFSGSFTGVDSGSFEASGBGSGAGSG9GTGHETGVDTGEESAEASG9FTGDEEASGBGSG' + b'YFSGNFTG9ESGSFTGAETGEEEASGZESGNFSG9GTGVDTGEETGHESGQFSGAGEASGVFEA' + b'SGBGTGTDSGAGSGXFSGYFTGHESG9GSGVFEASGZFSGAGTGTDTGUDSGXFSGVFTGYDEA' + b'SGBGSGSFTGYDSGAGTGVDSGVFSG9GTGZDSGSFSGPFSAEASGAFEASGPFSGNFTGUDEA' + b'SGSFTGUDTGVDTGEEEATGVDSGNFSGXFSGAGSGWFEATGTDSGAGTGUDSGXFSGAGTGAE' + b'SGVFSAEASGAFEASGPFSGNFTGUDEASGSFTGUDTGVDTGEEEATGTDSGAGTGUDSGXFSG' + b'AGTGAETGEEQAEASG9GSGSFEASGUFSG9GSGNFTGHEQAEATG9ETGVDSGAGEATGHEEA' + b'SGUFSG9GSGNFTGGEDBEATG9ETGVDSGAGEATGUDSGZFSGSFTGTDTGVDTGEEEASGZE' + b'SGNFSG9GTGVDTGEETGHESGQFSGAGQAEASGPFEATGVDSGAGEASGPFTGTDSGSFSGZF' + b'TGHEEASGXFSGNFSGXFEATGVDTGTDSGNFSGQFSGVFTG9ESGSFTGUDSGXFSGVFSGWF' + b'QAEASGPFSGSFTGTDSGAGTGHETGVDSG9GSGAGQAEATGUDSGBGSGNFTGUDEASGTFSG' + b'VFSGUFSG9GTGEESAEASGQEEASGZFSGAGSGSFEATGUDTGWDTGBESGSFTGUDTGVDSG' + b'PFSGAGSGPFSGNFSG9GSGVFSGSFQAEASGPFEATGVDSGAGEASGPFTGTDSGSFSGZFTG' + b'HEEASGXFSGNFSGXFEASGQFTGTDSGAGTGVDSGSFTGUDSGXFEASGVFEASG9GSGSFSG' + b'BGSGAGSG9GTGHETGVDSG9GTGDESGZFSGVFEASGRFSGYFTGHEEASGPFSGNFTGUDQA' + b'EATGUDSGBGSGNFTGUDSGNFSGSFTGVDEASGTFSGVFSGUFSG9GSGVFEASASASAEASG' + b'KETGDEEASG9GSGSFEATGYDSGAGTGVDSGVFTGVDSGSFEASGUFSG9GSGNFTGVDTGEE' + b'EASGBGTGTDSGNFSGPFSGRFTGWDSAEASGXESGAGTGVDSGAGSGZFTGWDEATG9ETGVD' + b'SGAGEASGPFEASGQFSGYFTGWDSGOFSGVFSG9GSGSFEASGRFTGWDTGAESGVFQAEASG' + b'PFEATGVDSGSFTGYDEASGZFSGSFTGUDTGVDSGNFTGYDQAEASGPFTGDEEASG9GSGSF' + b'EASGQFSGAGSGPFSGAGTGTDSGVFTGVDSGSFEASGAGEASG9GSGNFEASGPFSGSFTG9E' + b'SGSFTGTDSGVFSG9GSGXFSGNFTGYDQAEASGPFTGDEEATGYDSGAGTGVDSGVFTGVDSG' + b'SFEASGZFSGSTESTVALUE9DONTUSEINPRODUCTION99999YMSWGXVNDMLXPT9HMVA' + b'OWUUZMLSJZFWGKDVGXPSQAWAEBJN999999999999999999999999999PYOTA9UNI' + b'T9TESTS99999999999NYBKIVD99999999999B99999999YJVDLFI9FFXKNVTUKHO' + b'PTZUWZPOTRTHNZ9YZDXFRVBAUGO9APIQQWFSCLGFQMLMVCEPCTBFAVMIIXHUPG99' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999999999999999999' + b'9999999999999999999999999999999999999999999999999' + ), + ], + }, + ) class MockSignatureFragmentGenerator(object): From 474faba6dc4c890d0f24f2401ac1acc036944cbe Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 12:54:41 -0500 Subject: [PATCH 220/239] Improved docs for `signature_message_fragment`. --- iota/transaction.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/iota/transaction.py b/iota/transaction.py index 1a5ee6e..329c0da 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -165,11 +165,18 @@ def __init__( self.signature_message_fragment = signature_message_fragment """ - Cryptographic signature used to verify the transaction. - - Signatures are usually too long to fit into a single transaction, - so they are split out into multiple transactions in the same bundle - (hence it's called a fragment). + "Signature/Message Fragment" (note the slash): + + - For inputs, this contains a fragment of the cryptographic + signature, used to verify the transaction (the entire signature + is too large to fit into a single transaction, so it is split + across multiple transactions instead). + + - For other transactions, this contains a fragment of the message + attached to the transaction (if any). This can be pretty much + any value. Like signatures, the message may be split across + multiple transactions if it is too large to fit inside a single + transaction. """ self.is_confirmed = None # type: Optional[bool] From cefe7c1a51323c1ec6ece368a5a54d4ce5d7a1f1 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 15:53:00 -0500 Subject: [PATCH 221/239] Added support for testnet PoW. --- examples/shell.py | 60 ++++-- iota/api.py | 51 +++-- iota/commands/__init__.py | 2 - iota/commands/core/attach_to_tangle.py | 35 ++-- iota/commands/extended/replay_bundle.py | 27 +-- iota/commands/extended/send_transfer.py | 13 +- iota/commands/extended/send_trytes.py | 27 +-- iota/types.py | 5 +- test/commands/core/attach_to_tangle_test.py | 192 ++++++++++--------- test/commands/extended/replay_bundle_test.py | 73 +++---- test/commands/extended/send_transfer_test.py | 140 +++++++++----- test/commands/extended/send_trytes_test.py | 76 ++++---- 12 files changed, 398 insertions(+), 303 deletions(-) diff --git a/examples/shell.py b/examples/shell.py index 819df7a..5446d8c 100644 --- a/examples/shell.py +++ b/examples/shell.py @@ -1,45 +1,61 @@ # coding=utf-8 -"""Launches a Python shell with a configured API client ready to go.""" - +""" +Launches a Python shell with a configured API client ready to go. +""" from __future__ import absolute_import, division, print_function, \ unicode_literals from argparse import ArgumentParser +from getpass import getpass as secure_input from sys import argv -from typing import Text -from six import text_type as text +from iota import __version__ +from six import text_type -# Import all common IOTA symbols into module scope, so that the user -# doesn't have to import anything themselves. +# Import all IOTA symbols into module scope, so that it's more +# convenient for the user. from iota import * -def main(uri): - # type: (Text) -> None - iota = Iota(uri) +def main(uri, testnet): + seed = secure_input( + 'Enter seed and press return (typing will not be shown).\n' + 'If no seed is specified, a random one will be used instead.\n' + ) + + if isinstance(seed, text_type): + seed = seed.encode('ascii') + + iota = Iota(uri, seed=seed, testnet=testnet) _banner = ( - 'IOTA API client for {uri} initialized as variable `iota`. ' + 'IOTA API client for {uri} ({testnet}) initialized as variable `iota`.\n' 'Type `help(iota)` for help.'.format( - uri = uri, + testnet = 'testnet' if testnet else 'mainnet', + uri = uri, ) ) + start_shell(iota, _banner) + +def start_shell(iota, _banner): + """ + Starts the shell with limited scope. + """ try: - # noinspection PyUnresolvedReferences - import IPython + # noinspection PyUnresolvedReferences + import IPython except ImportError: - from code import InteractiveConsole - InteractiveConsole(locals={'iota': iota}).interact(_banner) + # IPython not available; use regular Python REPL. + from code import InteractiveConsole + InteractiveConsole(locals={'iota': iota}).interact(_banner) else: + # Launch IPython REPL. IPython.embed(header=_banner) if __name__ == '__main__': - from iota import __version__ - parser = ArgumentParser( description = __doc__, epilog = 'PyOTA v{version}'.format(version=__version__), @@ -47,7 +63,7 @@ def main(uri): parser.add_argument( '--uri', - type = text, + type = text_type, default = 'udp://localhost:14265/', help = @@ -55,5 +71,11 @@ def main(uri): '(defaults to udp://localhost:14265/).', ) - main(**vars(parser.parse_args(argv[1:]))) + parser.add_argument( + '--testnet', + action = 'store_true', + default = False, + help = 'If specified, use testnet settings (e.g., for PoW).', + ) + main(**vars(parser.parse_args(argv[1:]))) diff --git a/iota/api.py b/iota/api.py index bec0c0c..eab7faf 100644 --- a/iota/api.py +++ b/iota/api.py @@ -7,8 +7,7 @@ from iota import AdapterSpec, Address, Bundle, ProposedTransaction, Tag, \ TransactionHash, TryteString, TrytesCompatible from iota.adapter import BaseAdapter, resolve_adapter -from iota.commands import CustomCommand, DEFAULT_MIN_WEIGHT_MAGNITUDE, \ - command_registry +from iota.commands import CustomCommand, command_registry from iota.crypto.types import Seed __all__ = [ @@ -27,18 +26,22 @@ class StrictIota(object): References: - https://iota.readme.io/docs/getting-started """ - def __init__(self, adapter): - # type: (AdapterSpec) -> None + def __init__(self, adapter, testnet=False): + # type: (AdapterSpec, bool) -> None """ :param adapter: URI string or BaseAdapter instance. + + :param testnet: + Whether to use testnet settings for this instance. """ super(StrictIota, self).__init__() if not isinstance(adapter, BaseAdapter): adapter = resolve_adapter(adapter) - self.adapter = adapter # type: BaseAdapter + self.adapter = adapter # type: BaseAdapter + self.testnet = testnet def __getattr__(self, command): # type: (Text, dict) -> CustomCommand @@ -59,6 +62,15 @@ def __getattr__(self, command): except KeyError: return CustomCommand(self.adapter, command) + @property + def default_min_weight_magnitude(self): + # type: () -> int + """ + Returns the default ``min_weight_magnitude`` value to use for API + requests. + """ + return 13 if self.testnet else 18 + def add_neighbors(self, uris): # type: (Iterable[Text]) -> dict """ @@ -79,7 +91,7 @@ def attach_to_tangle( trunk_transaction, branch_transaction, trytes, - min_weight_magnitude = DEFAULT_MIN_WEIGHT_MAGNITUDE, + min_weight_magnitude = None, ): # type: (TransactionHash, TransactionHash, Iterable[TryteString], int) -> dict """ @@ -96,6 +108,9 @@ def attach_to_tangle( References: - https://iota.readme.io/docs/attachtotangle """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + return self.attachToTangle( trunk_transaction = trunk_transaction, branch_transaction = branch_transaction, @@ -319,8 +334,8 @@ class Iota(StrictIota): - https://iota.readme.io/docs/getting-started - https://github.com/iotaledger/wiki/blob/master/api-proposal.md """ - def __init__(self, adapter, seed=None): - # type: (AdapterSpec, Optional[TrytesCompatible]) -> None + def __init__(self, adapter, seed=None, testnet=False): + # type: (AdapterSpec, Optional[TrytesCompatible], bool) -> None """ :param seed: Seed used to generate new addresses. @@ -328,7 +343,7 @@ def __init__(self, adapter, seed=None): Note: This value is never transferred to the node/network. """ - super(Iota, self).__init__(adapter) + super(Iota, self).__init__(adapter, testnet) self.seed = Seed(seed) if seed else Seed.random() @@ -556,9 +571,9 @@ def replay_bundle( self, transaction, depth, - min_weight_magnitude = DEFAULT_MIN_WEIGHT_MAGNITUDE, + min_weight_magnitude = None, ): - # type: (TransactionHash, int, int) -> Bundle + # type: (TransactionHash, int, Optional[int]) -> Bundle """ Takes a tail transaction hash as input, gets the bundle associated with the transaction and then replays the bundle by attaching it to @@ -574,12 +589,17 @@ def replay_bundle( Min weight magnitude, used by the node to calibrate Proof of Work. + If not provided, a default value will be used. + :return: The bundle containing the replayed transfer. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#replaytransfer """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + return self.replayBundle( transaction = transaction, depth = depth, @@ -592,9 +612,9 @@ def send_transfer( transfers, inputs = None, change_address = None, - min_weight_magnitude = DEFAULT_MIN_WEIGHT_MAGNITUDE, + min_weight_magnitude = None, ): - # type: (int, Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], int) -> Bundle + # type: (int, Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], Optional[int]) -> Bundle """ Prepares a set of transfers and creates the bundle, then attaches the bundle to the Tangle, and broadcasts and stores the @@ -621,12 +641,17 @@ def send_transfer( Min weight magnitude, used by the node to calibrate Proof of Work. + If not provided, a default value will be used. + :return: The newly-attached bundle. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtransfer """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + return self.sendTransfer( seed = self.seed, depth = depth, diff --git a/iota/commands/__init__.py b/iota/commands/__init__.py index cb8b2d8..8101597 100644 --- a/iota/commands/__init__.py +++ b/iota/commands/__init__.py @@ -21,8 +21,6 @@ 'command_registry', ] -DEFAULT_MIN_WEIGHT_MAGNITUDE = 18 - command_registry = {} # type: Dict[Text, CommandMeta] """Registry of commands, indexed by command name.""" diff --git a/iota/commands/core/attach_to_tangle.py b/iota/commands/core/attach_to_tangle.py index 83cd993..928a6df 100644 --- a/iota/commands/core/attach_to_tangle.py +++ b/iota/commands/core/attach_to_tangle.py @@ -4,8 +4,7 @@ import filters as f from iota import TransactionHash -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ - RequestFilter, ResponseFilter +from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes __all__ = [ @@ -15,9 +14,9 @@ class AttachToTangleCommand(FilterCommand): """ - Executes `attachToTangle` command. + Executes ``attachToTangle`` command. - See :py:meth:`iota.api.StrictIota.attach_to_tangle`. + See :py:meth:`iota.api.StrictIota.attach_to_tangle` for more info. """ command = 'attachToTangle' @@ -30,24 +29,16 @@ def get_response_filter(self): class AttachToTangleRequestFilter(RequestFilter): def __init__(self): - super(AttachToTangleRequestFilter, self).__init__( - { - 'trunk_transaction': f.Required | Trytes(result_type=TransactionHash), - 'branch_transaction': f.Required | Trytes(result_type=TransactionHash), - - 'min_weight_magnitude': ( - f.Type(int) - | f.Min(18) - | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE) - ), - - 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), - }, - - allow_missing_keys = { - 'min_weight_magnitude', - }, - ) + super(AttachToTangleRequestFilter, self).__init__({ + 'branch_transaction': f.Required | Trytes(result_type=TransactionHash), + 'trunk_transaction': f.Required | Trytes(result_type=TransactionHash), + + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + + # Loosely-validated; testnet nodes require a different value than + # mainnet. + 'min_weight_magnitude': f.Required| f.Type(int) | f.Min(1), + }) class AttachToTangleResponseFilter(ResponseFilter): diff --git a/iota/commands/extended/replay_bundle.py b/iota/commands/extended/replay_bundle.py index 70e0125..091f685 100644 --- a/iota/commands/extended/replay_bundle.py +++ b/iota/commands/extended/replay_bundle.py @@ -7,8 +7,7 @@ import filters as f from iota import Bundle from iota import TransactionHash -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ - RequestFilter +from iota.commands import FilterCommand, RequestFilter from iota.commands.extended.get_bundles import GetBundlesCommand from iota.commands.extended.send_trytes import SendTrytesCommand from iota.filters import Trytes @@ -50,19 +49,11 @@ def _execute(self, request): class ReplayBundleRequestFilter(RequestFilter): def __init__(self): - super(ReplayBundleRequestFilter, self).__init__( - { - 'depth': f.Required | f.Type(int) | f.Min(1), - 'transaction': f.Required | Trytes(result_type=TransactionHash), - - 'min_weight_magnitude': ( - f.Type(int) - | f.Min(18) - | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE) - ), - }, - - allow_missing_keys = { - 'min_weight_magnitude', - }, - ) + super(ReplayBundleRequestFilter, self).__init__({ + 'depth': f.Required | f.Type(int) | f.Min(1), + 'transaction': f.Required | Trytes(result_type=TransactionHash), + + # Loosely-validated; testnet nodes require a different value than + # mainnet. + 'min_weight_magnitude': f.Required | f.Type(int) | f.Min(1), + }) diff --git a/iota/commands/extended/send_transfer.py b/iota/commands/extended/send_transfer.py index cf4a0b5..27a121a 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -6,8 +6,7 @@ import filters as f from iota import Address, Bundle, ProposedTransaction -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ - RequestFilter +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 @@ -64,6 +63,10 @@ def __init__(self): 'depth': f.Required | f.Type(int) | f.Min(1), 'seed': f.Required | Trytes(result_type=Seed), + # Loosely-validated; testnet nodes require a different value + # than mainnet. + 'min_weight_magnitude': f.Required | f.Type(int) | f.Min(1), + 'transfers': ( f.Required | f.Array @@ -73,11 +76,6 @@ def __init__(self): # Optional parameters. 'change_address': Trytes(result_type=Address), - 'min_weight_magnitude': ( - f.Type(int) - | f.Min(18) - | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE) - ), # Note that ``inputs`` is allowed to be an empty array. 'inputs': @@ -87,6 +85,5 @@ def __init__(self): allow_missing_keys = { 'change_address', 'inputs', - 'min_weight_magnitude', }, ) diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index 85a7ac0..049d7d9 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -6,8 +6,7 @@ import filters as f from iota import TryteString -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE, FilterCommand, \ - RequestFilter +from iota.commands import FilterCommand, RequestFilter from iota.commands.core.attach_to_tangle import AttachToTangleCommand from iota.commands.core.get_transactions_to_approve import \ GetTransactionsToApproveCommand @@ -55,18 +54,12 @@ def _execute(self, request): class SendTrytesRequestFilter(RequestFilter): def __init__(self): - super(SendTrytesRequestFilter, self).__init__( - { - # Required parameters. - 'depth': f.Required | f.Type(int) | f.Min(1), - 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), - - # Optional parameters. - 'min_weight_magnitude': - f.Type(int) | f.Min(18) | f.Optional(DEFAULT_MIN_WEIGHT_MAGNITUDE), - }, - - allow_missing_keys = { - 'min_weight_magnitude', - }, - ) + super(SendTrytesRequestFilter, self).__init__({ + 'depth': f.Required | f.Type(int) | f.Min(1), + + 'trytes': f.Required | f.Array | f.FilterRepeater(f.Required | Trytes), + + # Loosely-validated; testnet nodes require a different value than + # mainnet. + 'min_weight_magnitude': f.Required | f.Type(int) | f.Min(1), + }) diff --git a/iota/types.py b/iota/types.py index 5627268..f2c51bd 100644 --- a/iota/types.py +++ b/iota/types.py @@ -237,7 +237,10 @@ def __init__(self, trytes, pad=None): def __repr__(self): # type: () -> Text - return 'TryteString({trytes!r})'.format(trytes=binary_type(self._trytes)) + return '{cls}({trytes!r})'.format( + cls = type(self).__name__, + trytes = binary_type(self._trytes), + ) def __bytes__(self): # type: () -> binary_type diff --git a/test/commands/core/attach_to_tangle_test.py b/test/commands/core/attach_to_tangle_test.py index c077bb5..bdd83d2 100644 --- a/test/commands/core/attach_to_tangle_test.py +++ b/test/commands/core/attach_to_tangle_test.py @@ -7,7 +7,6 @@ import filters as f from filters.test import BaseFilterTestCase from iota import Iota, TransactionHash, TryteString -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.core.attach_to_tangle import AttachToTangleCommand from iota.filters import Trytes from six import binary_type, text_type @@ -34,7 +33,9 @@ def setUp(self): b'CCPCBDVC9DTCEAKDXC9D9DEARCWCPCBDVCTCEAHDWCTCEAKDCDFD9DSCSA' def test_pass_happy_path(self): - """The incoming request is valid.""" + """ + The incoming request is valid. + """ request = { 'trunk_transaction': TransactionHash(self.txn_id), 'branch_transaction': TransactionHash(self.txn_id), @@ -51,38 +52,11 @@ def test_pass_happy_path(self): self.assertFilterPasses(filter_) self.assertDictEqual(filter_.cleaned_data, request) - def test_pass_min_weight_magnitude_missing(self): - """`min_weight_magnitude` is optional.""" - request = { - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), - - 'trytes': [ - TryteString(self.trytes1) - ], - - # If not provided, this value is set to the default (18). - # 'min_weight_magnitude': 20, - } - - filter_ = self._filter(request) - - self.assertFilterPasses(filter_) - self.assertDictEqual( - filter_.cleaned_data, - - { - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), - 'trytes': [TryteString(self.trytes1)], - - 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, - }, - ) - # noinspection SpellCheckingInspection def test_pass_compatible_types(self): - """Incoming values can be converted into the expected types.""" + """ + Incoming values can be converted into the expected types. + """ filter_ = self._filter({ # Any value that can be converted into a TransactionHash is valid # here. @@ -127,14 +101,17 @@ def test_pass_compatible_types(self): ) def test_fail_empty(self): - """The incoming request is empty.""" + """ + The incoming request is empty. + """ self.assertFilterErrors( {}, { - 'trunk_transaction': [f.FilterMapper.CODE_MISSING_KEY], - 'branch_transaction': [f.FilterMapper.CODE_MISSING_KEY], - 'trytes': [f.FilterMapper.CODE_MISSING_KEY], + 'branch_transaction': [f.FilterMapper.CODE_MISSING_KEY], + 'min_weight_magnitude': [f.FilterMapper.CODE_MISSING_KEY], + 'trunk_transaction': [f.FilterMapper.CODE_MISSING_KEY], + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], }, ) @@ -142,9 +119,9 @@ def test_fail_unexpected_parameters(self): """The incoming request contains unexpected parameters.""" self.assertFilterErrors( { - 'trunk_transaction': TransactionHash(self.txn_id), 'branch_transaction': TransactionHash(self.txn_id), 'min_weight_magnitude': 20, + 'trunk_transaction': TransactionHash(self.txn_id), 'trytes': [TryteString(self.trytes1)], # Hey, how'd that get in there? @@ -157,13 +134,16 @@ def test_fail_unexpected_parameters(self): ) def test_fail_trunk_transaction_null(self): - """`trunk_transaction` is null.""" + """ + ``trunk_transaction`` is null. + """ self.assertFilterErrors( { 'trunk_transaction': None, - 'branch_transaction': TransactionHash(self.txn_id), - 'trytes': [TryteString(self.trytes1)], + 'branch_transaction': TransactionHash(self.txn_id), + 'min_weight_magnitude': 13, + 'trytes': [TryteString(self.trytes1)], }, { @@ -172,14 +152,17 @@ def test_fail_trunk_transaction_null(self): ) def test_fail_trunk_transaction_wrong_type(self): - """`trunk_transaction` can't be converted to a TryteString.""" + """ + ``trunk_transaction`` can't be converted to a TryteString. + """ self.assertFilterErrors( { # Strings are not valid tryte sequences. 'trunk_transaction': text_type(self.txn_id, 'ascii'), - 'branch_transaction': TransactionHash(self.txn_id), - 'trytes': [TryteString(self.trytes1)], + 'branch_transaction': TransactionHash(self.txn_id), + 'min_weight_magnitude': 13, + 'trytes': [TryteString(self.trytes1)], }, { @@ -188,13 +171,16 @@ def test_fail_trunk_transaction_wrong_type(self): ) def test_fail_branch_transaction_null(self): - """`branch_transaction` is null.""" + """ + ``branch_transaction`` is null. + """ self.assertFilterErrors( { 'branch_transaction': None, - 'trunk_transaction': TransactionHash(self.txn_id), - 'trytes': [TryteString(self.trytes1)], + 'min_weight_magnitude': 13, + 'trunk_transaction': TransactionHash(self.txn_id), + 'trytes': [TryteString(self.trytes1)], }, { @@ -203,34 +189,54 @@ def test_fail_branch_transaction_null(self): ) def test_fail_branch_transaction_wrong_type(self): - """`branch_transaction` can't be converted to a TryteString.""" + """ + ``branch_transaction`` can't be converted to a TryteString. + """ self.assertFilterErrors( { # Strings are not valid tryte sequences. 'branch_transaction': text_type(self.txn_id, 'ascii'), + 'min_weight_magnitude': 13, + 'trunk_transaction': TransactionHash(self.txn_id), + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'branch_transaction': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_min_weight_magnitude_null(self): + """ + ``min_weight_magnitude`` is null. + """ + self.assertFilterErrors( + { + 'min_weight_magnitude': None, + + 'branch_transaction': TransactionHash(self.txn_id), 'trunk_transaction': TransactionHash(self.txn_id), 'trytes': [TryteString(self.trytes1)], }, { - 'branch_transaction': [f.Type.CODE_WRONG_TYPE], + 'min_weight_magnitude': [f.Required.CODE_EMPTY], }, ) def test_fail_min_weight_magnitude_float(self): - """`min_weight_magnitude` is a float.""" + """ + ``min_weight_magnitude`` is a float. + """ self.assertFilterErrors( { # I don't care if the fpart is empty; it's still not an int! 'min_weight_magnitude': 20.0, - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), - - 'trytes': [ - TryteString(self.trytes1) - ], + 'branch_transaction': TransactionHash(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'trytes': [TryteString(self.trytes1)], }, { @@ -239,18 +245,17 @@ def test_fail_min_weight_magnitude_float(self): ) def test_fail_min_weight_magnitude_string(self): - """`min_weight_magnitude` is a string.""" + """ + ``min_weight_magnitude`` is a string. + """ self.assertFilterErrors( { # For want of an int cast, the transaction was lost. 'min_weight_magnitude': '20', - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), - - 'trytes': [ - TryteString(self.trytes1) - ], + 'branch_transaction': TransactionHash(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'trytes': [TryteString(self.trytes1)], }, { @@ -259,17 +264,16 @@ def test_fail_min_weight_magnitude_string(self): ) def test_fail_min_weight_magnitude_too_small(self): - """`min_weight_magnitude` is less than 18.""" + """ + ``min_weight_magnitude`` is less than 1. + """ self.assertFilterErrors( { - 'min_weight_magnitude': 17, - - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), + 'min_weight_magnitude': 0, - 'trytes': [ - TryteString(self.trytes1) - ], + 'branch_transaction': TransactionHash(self.txn_id), + 'trunk_transaction': TransactionHash(self.txn_id), + 'trytes': [TryteString(self.trytes1)], }, { @@ -278,13 +282,16 @@ def test_fail_min_weight_magnitude_too_small(self): ) def test_fail_trytes_null(self): - """`trytes` is null.""" + """ + ``trytes`` is null. + """ self.assertFilterErrors( { - 'trytes': None, + 'trytes': None, - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), + 'min_weight_magnitude': 13, + 'trunk_transaction': TransactionHash(self.txn_id), }, { @@ -293,15 +300,18 @@ def test_fail_trytes_null(self): ) def test_fail_trytes_wrong_type(self): - """`trytes` is not an array.""" + """ + ``trytes`` is not an array. + """ self.assertFilterErrors( { # You have to specify an array, even if you only want to attach - # a single tryte sequence. - 'trytes': TryteString(self.trytes1), + # a single tryte sequence. + 'trytes': TryteString(self.trytes1), - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), + 'min_weight_magnitude': 13, + 'trunk_transaction': TransactionHash(self.txn_id), }, { @@ -310,15 +320,18 @@ def test_fail_trytes_wrong_type(self): ) def test_fail_trytes_empty(self): - """`trytes` is an array, but it's empty.""" + """ + ``trytes`` is an array, but it's empty. + """ self.assertFilterErrors( { # Ok, you got the list part down, but you have to put something - # inside it. - 'trytes': [], + # inside it. + 'trytes': [], - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), + 'min_weight_magnitude': 13, + 'trunk_transaction': TransactionHash(self.txn_id), }, { @@ -327,7 +340,9 @@ def test_fail_trytes_empty(self): ) def test_fail_trytes_contents_invalid(self): - """`trytes` is an array, but it contains invalid values.""" + """ + ``trytes`` is an array, but it contains invalid values. + """ self.assertFilterErrors( { 'trytes': [ @@ -338,14 +353,15 @@ def test_fail_trytes_contents_invalid(self): b'not valid trytes', # This is actually valid; I just added it to make sure the - # filter isn't cheating! + # filter isn't cheating! TryteString(self.trytes2), 2130706433, ], - 'trunk_transaction': TransactionHash(self.txn_id), - 'branch_transaction': TransactionHash(self.txn_id), + 'branch_transaction': TransactionHash(self.txn_id), + 'min_weight_magnitude': 13, + 'trunk_transaction': TransactionHash(self.txn_id), }, { diff --git a/test/commands/extended/replay_bundle_test.py b/test/commands/extended/replay_bundle_test.py index f6e3cee..1162edd 100644 --- a/test/commands/extended/replay_bundle_test.py +++ b/test/commands/extended/replay_bundle_test.py @@ -7,7 +7,6 @@ import filters as f from filters.test import BaseFilterTestCase from iota import Iota, TransactionHash -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.extended.replay_bundle import ReplayBundleCommand from iota.filters import Trytes from six import binary_type, text_type @@ -67,26 +66,6 @@ def test_pass_compatible_types(self): }, ) - def test_pass_optional_parameters_excluded(self): - """ - Request omits optional parameters. - """ - filter_ = self._filter({ - 'depth': 100, - 'transaction': TransactionHash(self.trytes1), - }) - - self.assertFilterPasses(filter_) - self.assertDictEqual( - filter_.cleaned_data, - - { - 'depth': 100, - 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, - 'transaction': TransactionHash(self.trytes1), - }, - ) - def test_fail_empty(self): """ Request is empty. @@ -95,8 +74,9 @@ def test_fail_empty(self): {}, { - 'depth': [f.FilterMapper.CODE_MISSING_KEY], - 'transaction': [f.FilterMapper.CODE_MISSING_KEY], + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + 'min_weight_magnitude': [f.FilterMapper.CODE_MISSING_KEY], + 'transaction': [f.FilterMapper.CODE_MISSING_KEY], }, ) @@ -106,8 +86,9 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, - 'transaction': TransactionHash(self.trytes1), + 'depth': 100, + 'min_weight_magnitude': 18, + 'transaction': TransactionHash(self.trytes1), # That's a real nasty habit you got there. 'foo': 'bar', @@ -126,7 +107,8 @@ def test_fail_transaction_null(self): { 'transaction': None, - 'depth': 100, + 'depth': 100, + 'min_weight_magnitude': 18, }, { @@ -142,7 +124,8 @@ def test_fail_transaction_wrong_type(self): { 'transaction': text_type(self.trytes1, 'ascii'), - 'depth': 100, + 'depth': 100, + 'min_weight_magnitude': 18, }, { @@ -158,7 +141,8 @@ def test_fail_transaction_not_trytes(self): { 'transaction': b'not valid; must contain only uppercase and "9"', - 'depth': 100, + 'depth': 100, + 'min_weight_magnitude': 18, }, { @@ -174,7 +158,8 @@ def test_fail_depth_null(self): { 'depth': None, - 'transaction': TransactionHash(self.trytes1), + 'min_weight_magnitude': 18, + 'transaction': TransactionHash(self.trytes1), }, { @@ -191,7 +176,8 @@ def test_fail_depth_string(self): # Too ambiguous; it's gotta be an int. 'depth': '4', - 'transaction': TransactionHash(self.trytes1), + 'min_weight_magnitude': 18, + 'transaction': TransactionHash(self.trytes1), }, { @@ -208,7 +194,8 @@ def test_fail_depth_float(self): # Even with an empty fpart, float value is not valid. 'depth': 8.0, - 'transaction': TransactionHash(self.trytes1), + 'min_weight_magnitude': 18, + 'transaction': TransactionHash(self.trytes1), }, { @@ -224,7 +211,8 @@ def test_fail_depth_too_small(self): { 'depth': 0, - 'transaction': TransactionHash(self.trytes1), + 'min_weight_magnitude': 18, + 'transaction': TransactionHash(self.trytes1), }, { @@ -232,6 +220,23 @@ def test_fail_depth_too_small(self): }, ) + def test_fail_min_weight_magnitude_null(self): + """ + ``min_weight_magnitude`` is null. + """ + self.assertFilterErrors( + { + 'min_weight_magnitude': None, + + 'depth': 100, + 'transaction': TransactionHash(self.trytes1), + }, + + { + 'min_weight_magnitude': [f.Required.CODE_EMPTY], + }, + ) + def test_fail_min_weight_magnitude_string(self): """ ``min_weight_magnitude`` is a string. @@ -270,11 +275,11 @@ def test_fail_min_weight_magnitude_float(self): def test_fail_min_weight_magnitude_too_small(self): """ - ``min_weight_magnitude`` is < 18. + ``min_weight_magnitude`` is < 1. """ self.assertFilterErrors( { - 'min_weight_magnitude': 17, + 'min_weight_magnitude': 0, 'depth': 100, 'transaction': TransactionHash(self.trytes1), diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index d81c521..ea91e98 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -7,7 +7,6 @@ import filters as f from filters.test import BaseFilterTestCase from iota import Address, Iota, ProposedTransaction, TryteString -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.extended.send_transfer import SendTransferCommand from iota.crypto.types import Seed from iota.filters import Trytes @@ -144,8 +143,9 @@ def test_pass_optional_parameters_omitted(self): Request omits optional parameters. """ filter_ = self._filter({ - 'depth': 100, - 'seed': Seed(self.trytes2), + 'depth': 100, + 'min_weight_magnitude': 13, + 'seed': Seed(self.trytes2), 'transfers': [ self.transfer1, @@ -159,9 +159,10 @@ def test_pass_optional_parameters_omitted(self): { 'change_address': None, - 'depth': 100, 'inputs': None, - 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, + + 'depth': 100, + 'min_weight_magnitude': 13, 'seed': Seed(self.trytes2), 'transfers': [ @@ -179,9 +180,10 @@ def test_fail_empty(self): {}, { - 'depth': [f.FilterMapper.CODE_MISSING_KEY], - 'seed': [f.FilterMapper.CODE_MISSING_KEY], - 'transfers': [f.FilterMapper.CODE_MISSING_KEY], + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + 'min_weight_magnitude': [f.FilterMapper.CODE_MISSING_KEY], + 'seed': [f.FilterMapper.CODE_MISSING_KEY], + 'transfers': [f.FilterMapper.CODE_MISSING_KEY], }, ) @@ -191,13 +193,10 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, - 'seed': Seed(self.trytes2), - - 'transfers': [ - self.transfer1, - self.transfer2 - ], + 'depth': 100, + 'min_weight_magnitude': 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. @@ -217,8 +216,9 @@ def test_fail_seed_null(self): { 'seed': None, - 'depth': 100, - 'transfers': [self.transfer1], + 'depth': 100, + 'min_weight_magnitude': 18, + 'transfers': [self.transfer1], }, { @@ -234,8 +234,9 @@ def test_fail_seed_wrong_type(self): { 'seed': text_type(self.trytes1, 'ascii'), - 'depth': 100, - 'transfers': [self.transfer1], + 'depth': 100, + 'min_weight_magnitude': 18, + 'transfers': [self.transfer1], }, { @@ -251,8 +252,9 @@ def test_fail_seed_not_trytes(self): { 'seed': b'not valid; must contain only uppercase and "9"', - 'depth': 100, - 'transfers': [self.transfer1], + 'depth': 100, + 'min_weight_magnitude': 18, + 'transfers': [self.transfer1], }, { @@ -268,8 +270,9 @@ def test_fail_transfers_wrong_type(self): { 'transfers': self.transfer1, - 'depth': 100, - 'seed': Seed(self.trytes1), + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), }, { @@ -285,8 +288,9 @@ def test_fail_transfers_empty(self): { 'transfers': [], - 'depth': 100, - 'seed': Seed(self.trytes1), + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), }, { @@ -310,8 +314,9 @@ def test_fail_transfers_contents_invalid(self): {'address': Address(self.trytes2), 'value': 42}, ], - 'depth': 100, - 'seed': Seed(self.trytes1), + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), }, { @@ -328,9 +333,10 @@ def test_fail_change_address_wrong_type(self): { 'change_address': text_type(self.trytes3, 'ascii'), - 'depth': 100, - 'seed': Seed(self.trytes1), - 'transfers': [self.transfer1], + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], }, { @@ -346,9 +352,10 @@ def test_fail_change_address_not_trytes(self): { 'change_address': b'not valid; must contain only uppercase and "9"', - 'depth': 100, - 'seed': Seed(self.trytes1), - 'transfers': [self.transfer1], + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], }, { @@ -365,9 +372,10 @@ def test_fail_inputs_wrong_type(self): # Must be an array, even if there's only one input. 'inputs': Address(self.trytes4), - 'depth': 100, - 'seed': Seed(self.trytes1), - 'transfers': [self.transfer1], + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], }, { @@ -396,9 +404,10 @@ def test_fail_inputs_contents_invalid(self): b'9' * 82, ], - 'depth': 100, - 'seed': Seed(self.trytes1), - 'transfers': [self.transfer1], + 'depth': 100, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], }, { @@ -412,6 +421,24 @@ def test_fail_inputs_contents_invalid(self): }, ) + def test_fail_depth_null(self): + """ + ``depth`` is null. + """ + self.assertFilterErrors( + { + 'depth': None, + + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'depth': [f.Required.CODE_EMPTY], + }, + ) + def test_fail_depth_string(self): """ ``depth`` is a string. @@ -421,8 +448,9 @@ def test_fail_depth_string(self): # Too ambiguous; it must be an int. 'depth': '2', - 'seed': Seed(self.trytes1), - 'transfers': [self.transfer1], + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], }, { @@ -439,8 +467,9 @@ def test_fail_depth_float(self): # Even with an empty fpart, floats are invalid. 'depth': 100.0, - 'seed': Seed(self.trytes1), - 'transfers': [self.transfer1], + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], }, { @@ -456,12 +485,31 @@ def test_fail_depth_too_small(self): { 'depth': 0, + 'min_weight_magnitude': 18, + 'seed': Seed(self.trytes1), + 'transfers': [self.transfer1], + }, + + { + 'depth': [f.Min.CODE_TOO_SMALL], + }, + ) + + def test_fail_min_weight_magnitude_null(self): + """ + ``min_weight_magnitude`` is null. + """ + self.assertFilterErrors( + { + 'min_weight_magnitude': None, + + 'depth': 100, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, { - 'depth': [f.Min.CODE_TOO_SMALL], + 'min_weight_magnitude': [f.Required.CODE_EMPTY], }, ) @@ -505,11 +553,11 @@ def test_fail_min_weight_magnitude_float(self): def test_fail_min_weight_magnitude_too_small(self): """ - ``min_weight_magnitude`` is < 18. + ``min_weight_magnitude`` is < 1. """ self.assertFilterErrors( { - 'min_weight_magnitude': 17, + 'min_weight_magnitude': 0, 'depth': 100, 'seed': Seed(self.trytes1), diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index 2d8e946..c7b8b6f 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -7,7 +7,6 @@ import filters as f from filters.test import BaseFilterTestCase from iota import BadApiResponse, Iota, TransactionHash, TryteString -from iota.commands import DEFAULT_MIN_WEIGHT_MAGNITUDE from iota.commands.extended.send_trytes import SendTrytesCommand from iota.filters import Trytes from six import text_type, binary_type @@ -75,26 +74,6 @@ def test_pass_compatible_types(self): }, ) - def test_pass_optional_parameters_omitted(self): - """ - Request omits optional parameters. - """ - filter_ = self._filter({ - 'depth': 100, - 'trytes': [TryteString(self.trytes1)], - }) - - self.assertFilterPasses(filter_ ) - self.assertDictEqual( - filter_.cleaned_data, - - { - 'depth': 100, - 'min_weight_magnitude': DEFAULT_MIN_WEIGHT_MAGNITUDE, - 'trytes': [TryteString(self.trytes1)], - }, - ) - def test_fail_request_empty(self): """ Request is empty. @@ -103,8 +82,9 @@ def test_fail_request_empty(self): {}, { - 'depth': [f.FilterMapper.CODE_MISSING_KEY], - 'trytes': [f.FilterMapper.CODE_MISSING_KEY], + 'depth': [f.FilterMapper.CODE_MISSING_KEY], + 'min_weight_magnitude': [f.FilterMapper.CODE_MISSING_KEY], + 'trytes': [f.FilterMapper.CODE_MISSING_KEY], }, ) @@ -114,8 +94,9 @@ def test_fail_request_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, - 'trytes': [TryteString(self.trytes1)], + 'depth': 100, + 'min_weight_magnitude': 18, + 'trytes': [TryteString(self.trytes1)], # Oh, bother. 'foo': 'bar', @@ -134,7 +115,8 @@ def test_fail_depth_null(self): { 'depth': None, - 'trytes': [TryteString(self.trytes1)], + 'min_weight_magnitude': 18, + 'trytes': [TryteString(self.trytes1)], }, { @@ -151,7 +133,8 @@ def test_fail_depth_string(self): # Too ambiguous; it's gotta be an int. 'depth': '4', - 'trytes': [TryteString(self.trytes1)], + 'min_weight_magnitude': 18, + 'trytes': [TryteString(self.trytes1)], }, { @@ -168,7 +151,8 @@ def test_fail_depth_float(self): # Even with an empty fpart, float value is not valid. 'depth': 8.0, - 'trytes': [TryteString(self.trytes1)], + 'min_weight_magnitude': 18, + 'trytes': [TryteString(self.trytes1)], }, { @@ -184,7 +168,8 @@ def test_fail_depth_too_small(self): { 'depth': 0, - 'trytes': [TryteString(self.trytes1)], + 'min_weight_magnitude': 18, + 'trytes': [TryteString(self.trytes1)], }, { @@ -192,6 +177,23 @@ def test_fail_depth_too_small(self): }, ) + def test_fail_min_weight_magnitude_null(self): + """ + ``min_weight_magnitude`` is null. + """ + self.assertFilterErrors( + { + 'min_weight_magnitude': None, + + 'depth': 100, + 'trytes': [TryteString(self.trytes1)], + }, + + { + 'min_weight_magnitude': [f.Required.CODE_EMPTY], + }, + ) + def test_fail_min_weight_magnitude_string(self): """ ``min_weight_magnitude`` is a string. @@ -230,11 +232,11 @@ def test_fail_min_weight_magnitude_float(self): def test_fail_min_weight_magnitude_too_small(self): """ - ``min_weight_magnitude`` is < 18. + ``min_weight_magnitude`` is < 1. """ self.assertFilterErrors( { - 'min_weight_magnitude': 17, + 'min_weight_magnitude': 0, 'depth': 100, 'trytes': [TryteString(self.trytes1)], @@ -253,7 +255,8 @@ def test_fail_trytes_null(self): { 'trytes': None, - 'depth': 100, + 'depth': 100, + 'min_weight_magnitude': 18, }, { @@ -271,7 +274,8 @@ def test_fail_trytes_wrong_type(self): # send. 'trytes': TryteString(self.trytes1), - 'depth': 100, + 'depth': 100, + 'min_weight_magnitude': 18, }, { @@ -287,7 +291,8 @@ def test_fail_trytes_empty(self): { 'trytes': [], - 'depth': 100, + 'depth': 100, + 'min_weight_magnitude': 18, }, { @@ -315,7 +320,8 @@ def test_fail_trytes_contents_invalid(self): 2130706433, ], - 'depth': 100, + 'depth': 100, + 'min_weight_magnitude': 18, }, { From 7c5e3140a8d227435b6109a7816e2348ee1e08a7 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 17:13:32 -0500 Subject: [PATCH 222/239] Stubbed-out bundle validator. --- iota/commands/extended/get_bundles.py | 14 +- iota/transaction.py | 155 +++++++----- test/transaction_test.py | 339 +++++++++++++++++++++++++- 3 files changed, 438 insertions(+), 70 deletions(-) diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index cfa6316..d4c21a9 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -11,6 +11,7 @@ from iota.commands.core.get_trytes import GetTrytesCommand from iota.exceptions import with_context from iota.filters import Trytes +from iota.transaction import BundleValidator __all__ = [ 'GetBundlesCommand', @@ -34,21 +35,18 @@ def get_response_filter(self): def _execute(self, request): transaction_hash = request['transaction'] # type: TransactionHash - bundle = Bundle(self._traverse_bundle(transaction_hash)) + bundle = Bundle(self._traverse_bundle(transaction_hash)) + validator = BundleValidator(bundle) - try: - bundle.validate() - except ValueError as e: + if not validator.is_valid(): raise with_context( exc = BadApiResponse( - 'Bundle failed validation: {error} ' - '(``exc.context`` has more info).'.format( - error = e, - ), + 'Bundle failed validation (``exc.context`` has more info).', ), context = { 'bundle': bundle, + 'errors': validator.errors, }, ) diff --git a/iota/transaction.py b/iota/transaction.py index 329c0da..b019155 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -4,8 +4,8 @@ from calendar import timegm as unix_timestamp from datetime import datetime -from typing import Iterable, Iterator, List, MutableSequence, Optional, \ - Sequence, Tuple +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 @@ -18,6 +18,7 @@ __all__ = [ 'Bundle', 'BundleHash', + 'Fragment', 'ProposedBundle', 'ProposedTransaction', 'Transaction', @@ -54,6 +55,29 @@ class TransactionHash(Hash): pass +class Fragment(TryteString): + """ + A signature/message fragment in a transaction. + """ + LEN = 2187 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Fragment, self).__init__(trytes, pad=self.LEN) + + if len(self._trytes) > self.LEN: + raise with_context( + exc = ValueError('{cls} values must be {len} trytes long.'.format( + cls = type(self).__name__, + len = self.LEN + )), + + context = { + 'trytes': trytes, + }, + ) + + class Transaction(JsonSerializable): """ A transaction that has been attached to the Tangle. @@ -160,8 +184,24 @@ def __init__( The position of the final transaction inside the bundle. """ - self.branch_transaction_hash = branch_transaction_hash - self.trunk_transaction_hash = trunk_transaction_hash + self.trunk_transaction_hash = trunk_transaction_hash + """ + In order to add a transaction to the Tangle, you must perform PoW + to "approve" two existing transactions, called the "trunk" and + "branch" transactions. + + The trunk transaction is generally used to link transactions within + a bundle. + """ + + self.branch_transaction_hash = branch_transaction_hash + """ + In order to add a transaction to the Tangle, you must perform PoW + to "approve" two existing transactions, called the "trunk" and + "branch" transactions. + + The branch transaction generally has no significance. + """ self.signature_message_fragment = signature_message_fragment """ @@ -301,16 +341,6 @@ class ProposedTransaction(Transaction): Provide to :py:meth:`iota.api.Iota.send_transfer` to attach to tangle and publish/store. """ - MESSAGE_LEN = 2187 - """ - Max number of trytes allowed in a transaction message. - - If a transaction's message is longer than this, it will be split into - multiple transactions automatically when it is added to a bundle. - - See :py:meth:`ProposedBundle.add_transaction` for more info. - """ - def __init__(self, address, value, tag=None, message=None, timestamp=None): # type: (Address, int, Optional[Tag], Optional[TryteString], Optional[int]) -> None if not timestamp: @@ -456,44 +486,55 @@ def as_json_compatible(self): """ return [txn.as_json_compatible() for txn in self] - def validate(self): - # type: () -> None + +class BundleValidator(object): + """ + Checks a bundle and its transactions for problems. + """ + def __init__(self, bundle): + # type: (Bundle) -> None + super(BundleValidator, self).__init__() + + self.bundle = bundle + + self._errors = [] # type: Optional[List[Text]] + self._validator = self._create_validator() + + @property + def errors(self): + # type: () -> List[Text] + """ + Returns all errors found with the bundle. """ - Checks that the bundle is valid. If any issues are found, an - exception is raised. - - :raise: - - :py:class:`ValueError` if any issues are found. - """ - # balance = 0 - # sponge = Curl() - # - # # Use a counter for the loop so that we can skip ahead as we go. - # i = 0 - # while i < len(self): - # txn = self[i] - # - # if txn.current_index != i: - # raise ValueError( - # 'Transaction #{i} has invalid ``currentIndex`` ' - # '({txn.current_index}).'.format( - # i = i, - # txn = txn, - # ), - # ) - # - # balance += txn.value - # - # sponge.absorb(txn.get_signature_validation_trytes()) - # - # - # if balance: - # raise ValueError( - # 'Bundle has non-zero balance ({balance}).'.format( - # balance = balance, - # ), - # ) - pass + try: + self._errors.extend(self._validator) # type: List[Text] + except StopIteration: + pass + + return self._errors + + def is_valid(self): + # type: () -> bool + """ + Returns whether the bundle is valid. + """ + if not self._errors: + try: + # We only have to check for a single error to determine if the + # bundle is valid or not. + self._errors.append(next(self._validator)) + except StopIteration: + pass + + return not self._errors + + def _create_validator(self): + # type: () -> Generator[Text] + """ + Creates a generator that does all the work. + """ + if False: + yield '' class ProposedBundle(JsonSerializable, Sequence[ProposedTransaction]): @@ -591,7 +632,7 @@ def add_transaction(self, transaction): address = transaction.address, value = transaction.value, tag = transaction.tag, - message = transaction.message[:ProposedTransaction.MESSAGE_LEN], + message = transaction.message[:Fragment.LEN], timestamp = transaction.timestamp, )) @@ -601,17 +642,17 @@ def add_transaction(self, transaction): # If the message is too long to fit in a single transactions, # it must be split up into multiple transactions so that it will # fit. - fragment = transaction.message[ProposedTransaction.MESSAGE_LEN:] + fragment = transaction.message[Fragment.LEN:] while fragment: self._transactions.append(ProposedTransaction( address = transaction.address, value = 0, tag = transaction.tag, - message = fragment[:ProposedTransaction.MESSAGE_LEN], + message = fragment[:Fragment.LEN], timestamp = transaction.timestamp, )) - fragment = fragment[ProposedTransaction.MESSAGE_LEN:] + fragment = fragment[Fragment.LEN:] def add_inputs(self, inputs): # type: (Iterable[Address]) -> None @@ -746,9 +787,7 @@ def finalize(self): txn.bundle_hash = self.hash # Initialize signature/message fragment. - txn.signature_message_fragment = ( - TryteString(txn.message or b'', pad=ProposedTransaction.MESSAGE_LEN) - ) + txn.signature_message_fragment = Fragment(txn.message or b'') def sign_inputs(self, key_generator): # type: (KeyGenerator) -> None diff --git a/test/transaction_test.py b/test/transaction_test.py index 46f0091..ac5c913 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -4,11 +4,342 @@ from unittest import TestCase -from iota import Address, BundleHash, Hash, Tag, Transaction, \ - TransactionHash, TryteString +from iota import Address, Bundle, BundleHash, Fragment, Hash, Tag, \ + Transaction, TransactionHash +from iota.transaction import BundleValidator from six import binary_type +class BundleValidatorTestCase(TestCase): + # noinspection SpellCheckingInspection + def setUp(self): + super(BundleValidatorTestCase, self).setUp() + + # Define a valid bundle that will serve as the happy path. + # We will mangle it in various ways to trigger validation errors. + self.bundle = Bundle([ + # "Spend" transaction, Part 1 of 1 + Transaction( + hash_ = + TransactionHash( + b'LUQJUUDAZIHSTPBLCZYXWXYKXTFCOCQJ9EHXKLEB' + b'IJBPSRFSBYRBYODDAZ9NPKPYSMPVNEFXYZQ999999' + ), + + address = + Address( + b'FZXUHBBLASPIMBDIHYTDFCDFIRII9LRJPXFTQTPO' + b'VLEIFE9NWTFPPQZHDCXYUOUCXHHNRPKCIROYYTWSA' + ), + + branch_transaction_hash = + TransactionHash( + b'UKGIAYNLALFGJOVUZYJGNIOZSXBBZDXVQLUMHGQE' + b'PZJWYDMGTPJIQXS9GOKXR9WIGWFRWRSKGCJ999999' + ), + + bundle_hash = + BundleHash( + b'ZSATLX9HDENCIARERVLWYHXPQETFL9QKTNC9LUOL' + b'CDXKKW9MYTLZJDXBNOHURUXSYWMGGD9UDGLHCSZO9' + ), + + nonce = + Hash( + b'LIJVXBVTYMEEPCKJRIQTGAKWJRAMYNPJEGHEWAUL' + b'XPBBUQPCJTJPRZTISQPJRJGMSBGQLER9OXYQXFGQO' + ), + + trunk_transaction_hash = + TransactionHash( + b'KFCQSGDYENCECCPNNZDVDTBINCBRBERPTQIHFH9G' + b'YLTCSGUFMVWWSAHVZFXDVEZO9UHAUIU9LNX999999' + ), + + signature_message_fragment = Fragment(b''), + + current_index = 0, + last_index = 3, + tag = Tag(b''), + timestamp = 1483033814, + value = 1, + ), + + # Input #1, Part 1 of 2 + Transaction( + hash_ = + TransactionHash( + b'KFCQSGDYENCECCPNNZDVDTBINCBRBERPTQIHFH9G' + b'YLTCSGUFMVWWSAHVZFXDVEZO9UHAUIU9LNX999999' + ), + + address = + Address( + b'GMHOSRRGXJLDPVWRWVSRWI9BCIVLUXWKTJYZATIA' + b'RAZRUCRGTWXWP9SZPFOVAMLISUPQUKHNDMITUJKIB' + ), + + branch_transaction_hash = + TransactionHash( + b'UKGIAYNLALFGJOVUZYJGNIOZSXBBZDXVQLUMHGQE' + b'PZJWYDMGTPJIQXS9GOKXR9WIGWFRWRSKGCJ999999' + ), + + bundle_hash = + BundleHash( + b'ZSATLX9HDENCIARERVLWYHXPQETFL9QKTNC9LUOL' + b'CDXKKW9MYTLZJDXBNOHURUXSYWMGGD9UDGLHCSZO9' + ), + + nonce = + Hash( + b'VRYLDCKEWZJXPQVSWOJVYVBJSCWZQEVKPBG9KGEZ' + b'GPRQFKFSRNBPXCSVQNJINBRNEPIKAXNHOTJFIVYJO' + ), + + trunk_transaction_hash = + TransactionHash( + b'QSTUKDIBYAZIQVEOMFGKQPHAIPBHUPSDQFFKKHRA' + b'ABYMYMQDHMTEWCM9IMZHQMGXOTWNSJYHRNA999999' + ), + + signature_message_fragment = + Fragment( + b'XS9OVIXHIGGR9IYQBHGMFAHPZBWLIBNAQPFMPVYUZDOLLFDJIPZEMIOGVANQJSCU' + b'IPDNNUNAMWEL9OFXXK9NV9UTCRBYTARBJHPQYJYKNAQGMATG9EXQMHGXY9QOHPBA' + b'FEVABDYMCXORXHBMPLEWJYGYFFBWVXAUXHGLTABBKOQMZLFAYWDAKEOMJPJX9TMT' + b'GXIJXZTKRRIPAMYY9UNSPPEGFPJE9NFSJFWKYOFZRMPBXZDNQUEKLRUVPXMCTQRE' + b'ZWICSCVXN9VBLN9DRINRPAZTYJYXPGGRZJLMYXGCLUQNZ9NJGH9GFQPKKVK9N9WR' + b'IJXDNKUMLLJUVIQRGPHEVWTXQHRLRCWQJCHTPASCVLRGPNWSIUKWIBMDJJ9EUTQ9' + b'NXZZEJFWY9LCJJSOEPXWETUBKKVZNUKTLUPEPDBLUWCQGYTOXZ9NZUXHBDOUYQBP' + b'MNECVJ9HGWA9AWU9VHGETWKBU9YZEZGEQKMVTAKPLCZVWKQFXDEFBPKNUCQDSPWA' + b'LMPFTUFGRFDZH9PQHJ9WXZPCDWGMNASVVEUXEGWATM9ZIMCEEXTHCXFLYG9LQAKV' + b'UOGORP9UUWYFTWGZ9OFOGSP9KDNPDSQKEMMISEMWQDVFKCSQXSP9RUMNUQJOBACU' + b'MPIXCGBJLQQGB9GDSMUUUSYWIY9ZNIAIZBJYTAJKJKZIBFPMGDWUEPXSO9HUJRNV' + b'QE9OTVUPKBVNVUBSILVZEDPC9AMEYAIATE9JEIQQWIMGCZXMHOPXPFUTEPJEATJN' + b'QWDFZQ9OGPNBFUHLJDJZQJLXCFEUDPZKVCPEBTNBRYNIIKJHUV9EUFVMB9AHTARQ' + b'DN9TZ9JPAPCEXSYHZPAQLBJMNQ9R9KPWVZLDLFNHYCCQBFVEPMEZSXAB9GOKGOGC' + b'SOVL9XJSAQYSECY9UUNSVQSJB9BZVYRUURNUDMZVBJTMIDQUKQLMVW99XFDWTOPR' + b'UBRPKS9BGEAQPIODAMJAILNCH9UVYVWSDCZXZWLO9XJJZ9FQOL9F9ZJDNGMUGFKJ' + b'PCYURGYBGYRVKPEBKMJPZZGDKZKT9UBFSJEELREWOYDQZVUPVSGPZYIDVOJGNTXC' + b'OFGCHBGVZPQDNRKAQNVJEYKYTKHTFBJRDMKVSHEWADNYIQOAUFXYMZKNJPLXGYFX' + b'DTCVDDBUHBDPG9WLNMWPSCCCGVTIOOLEECXKNVAYNNTDLJMDGDGSKOGWO9UYXTTF' + b'FCRZEDDQNN9ZODTETGMGGUXOYECGNMHGMGXHZSPODIBMBATJJHSPQHDUCZOMWQNI' + b'CUZG9LAMBOQRQQDIPIBMIDCIAJBBBNDUAIEMFCEASHPUJPFPPXNDUVGDNNYBRRTW' + b'SPXGXMCSUXYJSTFIRUIDNEUSVKNIDKIBRQNMEDCYQOMJYTMGRZLYHBUYXCRGSAXR' + b'ZVHTZEAKNAUKJPFGPOGQLTDMSOXR9NVOIAIMCBVWOF9FXAZUKKZKHJEGHFNLUB9B' + b'TGAICGQGAYZRRHSFIDTNIJPHIHCXTHQUSKJRSVAWFUXLBYA99QKMGLHDNUHOPEW9' + b'OFNWPDXXRVZREUIQKSVSDCFIJ99TSGSZ9KU9JGE9VXDVVOLMGNMUGSHUZAOFCIMK' + b'CPEWMG9IHUZAILQCANIUUG9JNEZMT9EONSN9CWWQOTFBEPZRTTJTQFSTQTBERKGE' + b'NGFFIYMZMCFBYNIOBPOFOIYPUMYYPRXEHUJEVVELOPNXAPCYFXQ9ORMSFICDOZTS' + b'GQOMDI9FKEKRIMZTWSIWMYAUSBIN9TPFSMQZCYGVPVWKSFZXPE9BP9ALNWQOVJGM' + b'SCSJSTNUTMUAJUIQTITPPOHG9NKIFRNXSCMDAEW9LSUCTCXITSTZSBYMPOMSMTXP' + b'CEBEOAUJK9STIZRXUORRQBCYJPCNHFKEVY9YBJL9QGLVUCSZKOLHD9BDNKIVJX9T' + b'PPXQVGAXUSQQYGFDWQRZPKZKKWB9ZBFXTUGUGOAQLDTJPQXPUPHNATSGILEQCSQX' + b'X9IAGIVKUW9MVNGKTSCYDMPSVWXCGLXEHWKRPVARKJFWGRYFCATYNZDTRZDGNZAI' + b'OULYHRIPACAZLN9YHOFDSZYIRZJEGDUZBHFFWWQRNOLLWKZZENKOWQQYHGLMBMPF' + b'HE9VHDDTBZYHMKQGZNCSLACYRCGYSFFTZQJUSZGJTZKKLWAEBGCRLXQRADCSFQYZ' + b'G9CM9VLMQZA' + ), + + current_index = 1, + last_index = 3, + tag = Tag(b''), + timestamp = 1483033814, + value = -99, + ), + + # Input #1, Part 2 of 2 + Transaction( + hash_ = + TransactionHash( + b'QSTUKDIBYAZIQVEOMFGKQPHAIPBHUPSDQFFKKHRA' + b'ABYMYMQDHMTEWCM9IMZHQMGXOTWNSJYHRNA999999' + ), + + address = + Address( + b'GMHOSRRGXJLDPVWRWVSRWI9BCIVLUXWKTJYZATIA' + b'RAZRUCRGTWXWP9SZPFOVAMLISUPQUKHNDMITUJKIB' + ), + + branch_transaction_hash = + TransactionHash( + b'UKGIAYNLALFGJOVUZYJGNIOZSXBBZDXVQLUMHGQE' + b'PZJWYDMGTPJIQXS9GOKXR9WIGWFRWRSKGCJ999999' + ), + + bundle_hash = + BundleHash( + b'ZSATLX9HDENCIARERVLWYHXPQETFL9QKTNC9LUOL' + b'CDXKKW9MYTLZJDXBNOHURUXSYWMGGD9UDGLHCSZO9' + ), + + nonce = + Hash( + b'AAKVYZOEZSOXTX9LOLHZYLNAS9CXBLSWVZQAMRGW' + b'YW9GHHMVIOHWBMTXHDBXRTF9DEFFQFQESNVJORNXK' + ), + + trunk_transaction_hash = + TransactionHash( + b'ZYQGVZABMFVLJXHXXJMVAXOXHRJTTQUVDIIQOOXN' + b'NDPQGDFDRIDQMUWJGCQKKLGEUQRBFAJWZBC999999' + ), + + signature_message_fragment = + Fragment( + b'YSNEGUCUHXEEFXPQEABV9ATGQMMXEVGMZWKKAFAVOVGUECOZZJFQDNRBCSXCOTBD' + b'BRUJ9HF9CITXQI9ZQGZFKCXMFZTOYHUTCXDIBIMTBKVXMMTPNKRDRLQESLWFZSQQ' + b'9BCGKVIZAHBWYTNXG9OWOXHAMQECMOVKN9SOEVJBBARPXUXYUQVFPYXWXQQMDIVP' + b'VITRWTNNBY9CYBHXJTZUVIPJJG9WLTNMFVPXGYZCNOGSLGVMS9YXXNSV9AYPXZTA' + b'QJYUNUFBCSZBZNKWCPMVMOGFIDENTOOOCPRDJTNGQRLA9YKMLYZQRO9QQJMCSYVF' + b'YLISFIWQQYMWMHUOEZPATYCEZARLWLAMCZWYWJZVD9WWKYJURTOLITFFRXQUBKST' + b'DG9CKDBLPXTPCIMKEKRGEXJGLRL9ZST9VOLV9NOFZLIMVOZBDZJUQISUWZKOJCRN' + b'YRBRJLCTNPV9QIWQJZDQFVPSTW9BJYWHNRVQTITWJYB9HBUQBXTAGK9BZCHYWYPE' + b'IREDOXCYRW9UXVSLZBBPAFIUEJABMBYKSUPNWVVKAFQJKDAYYRDICTGOTWWDSFLG' + b'BQFZZ9NBEHZHPHVQUYEETIRUDM9V9LBXFUXTUGUMZG9HRBLXCKMMWWMK9VTKVZSA' + b'PRSMJVBLFFDHTYCPDXKBUYYLZDPW9EVXANPZOPBASQUPRNCDGHNUK9NDUQSULUZI' + b'VMIJTPUGMZPCYR9AERBAGUYNGVEHWIIADAAPPMYQOAGBQCXEDTQOGHWHHSWDFZLC' + b'DVLNPYMGDPZWOZURT9OZKDJKFECXSFIALXJDRJWMWMTNUUNVDUCJAZLDRN9ZWLHH' + b'SNXDWULUBNLVRDJZQMKCTRCKULKS9VARFZSRYZCPNH9FHXCAFWKPNGOPOFMYXJLE' + b'LTKUHSZVDQRDJIGQRGOSKYWDCU9EBJMXQDBKTBNQTIZNCALHRNTHKN99WOBQVVEV' + b'HEDTDRKFPGLIWOSPLAAELQQXDCDWPIFED9OEUPYPKHZBOHPQGQGSEKO9BFIQFYZK' + b'YEULWSIBZVSPXBGOJTTYBVIIIPAXGL9ZJNNIELFYAUOUBRDWLJJMSAXHQOYGOWDV' + b'HHPISRZFSHPDLNQDFWRHLWNAJPYM9REAJLZDIAIVLQBFAUJIQKVHJDFPXENI9ZM9' + b'SFNGSQHDFEDC9CQVXAXTQVLWYMVSLEDCOVNSQLSANLVA9TWSY9BHAJKOCGI9YLAB' + b'VROCBJRVXRWBKNUXCAXJIAYWSFRDZHIPQSNBRYNKZAFXHDUENVLHFHYIKH9IANFV' + b'FKWVFJCSEELVTDDUHBPIYNFLTJLINNORIMDEAXMN9LGNGBWVWYWQIPWKBFDKNDOX' + b'WFKGBAMZIUFYA9DXGAL9OQQTJAUUXTINWZSQUTPUKUMOZCGOBKKFBXCVR9AGTAQS' + b'SVGTUBBHSIRHFRSIR9SKSZPXQFG9AOYAHZNQR9AHSEFCKWCJHUTLREDVGBQYVBZR' + b'CZDXFG9PTSAWQOURYKNWYAZNASV9UMUYUMFCQSFDHZD99WUMCORLYTIZMRGNBAY9' + b'UJYJMMRCLJP9XVLXTAZOHNVVYSCOSDHGUOPXIRBJDXJUCJYLQKUJOTNJCPRBDOKV' + b'ZEMIGZRNJOQKFFAXQVGGY9YRJORZCOD9REIIIDDTRQ9TJWTFYRKOPLAFNUUPCHXA' + b'WVPYUQXAFFCTYAESWAFUTQQYZRQVLVZW9OWAAJMPSAEPKWXVEZVTVPQEEBVXNZJP' + b'ZU9JJSIAEPIT9HE99XNAUYOAKRIFQQJQTFIMWEOKLCH9JKCQTGZPEGWORFB9ARNS' + b'DPYKRONBONYOGEVEFXGTMQTQBEMFQWEMIDSGAVEQHVHAPSMTCJ9FMEYBWAQWWJCE' + b'ABUUMMVNDMSBORFLHVIIDOUQHHXQKXTVGRAYTLMECCSVZOZM9JKUWIGGFLMMDGBU' + b'DBIHJFUINVOKSFTOGFCZEMIBSZNGPL9HXWGTNNAKYIMDITCRMSHFR9BDSFGHXQMR' + b'ACZOVUOTSJSKMNHNYIFEOD9CVBWYVVMG9ZDNR9FOIXSZSTIO9GLOLPLMW9RPAJYB' + b'WTCKV9JMSEVGD9ZPEGKXF9XYQMUMJPWTMFZJODFIEYNLI9PWODSPPW9MVJOWZQZU' + b'CIKXCVVXDKWHXV99GOEZ9CMGUH9OWGLLISNZEPSAPEDHVRKKGFFNGBXFLDBQTTQL' + b'WVLUITJQ9JM' + ), + + current_index = 2, + last_index = 3, + tag = Tag(b''), + timestamp = 1483033814, + value = 0, + ), + + # "Change" transaction, Part 1 of 1 + Transaction( + hash_ = + TransactionHash( + b'ZYQGVZABMFVLJXHXXJMVAXOXHRJTTQUVDIIQOOXN' + b'NDPQGDFDRIDQMUWJGCQKKLGEUQRBFAJWZBC999999' + ), + + address = + Address( + b'YOTMYW9YLZQCSLHB9WRSTZDYYYGUUWLVDRHFQFEX' + b'UVOQARTQWZGLBU9DVSRDPCWYWQZHLFHY9NGLPZRAQ' + ), + + branch_transaction_hash = + TransactionHash( + b'QCHKLZZBG9XQMNGCDVXZGDRXIJMFZP9XUGAWNNVP' + b'GXBWB9NVEKEFMUWOEACULFUR9Q9XCWPBRNF999999' + ), + + bundle_hash = + BundleHash( + b'ZSATLX9HDENCIARERVLWYHXPQETFL9QKTNC9LUOL' + b'CDXKKW9MYTLZJDXBNOHURUXSYWMGGD9UDGLHCSZO9' + ), + + nonce = + Hash( + b'TPGXQFUGNEYYFFKPFWJSXKTWEUKUFTRJCQKKXLXL' + b'PSOHBZTGIBFPGLSVRIVYAC9NZMOMZLARFZYCNNRCM' + ), + + trunk_transaction_hash = + TransactionHash( + b'UKGIAYNLALFGJOVUZYJGNIOZSXBBZDXVQLUMHGQE' + b'PZJWYDMGTPJIQXS9GOKXR9WIGWFRWRSKGCJ999999' + ), + + signature_message_fragment = Fragment(b''), + + current_index = 3, + last_index = 3, + tag = Tag(b''), + timestamp = 1483033814, + value = 98, + ), + ]) + + def test_pass_happy_path(self): + """ + Bundle passes validation. + """ + validator = BundleValidator(self.bundle) + + self.assertTrue(validator.is_valid()) + self.assertListEqual(validator.errors, []) + + def test_fail_balance_positive(self): + """ + The bundle balance is > 0. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_balance_negative(self): + """ + The bundle balance is < 0. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_bundle_hash_invalid(self): + """ + One of the transactions has an invalid ``bundle_hash`` value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_current_index_invalid(self): + """ + One of the transactions has an invalid ``current_index`` value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_last_index_invalid(self): + """ + One of the transactions has an invalid ``last_index`` value. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_signature_invalid(self): + """ + One of the input signatures fails validation. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_fail_multiple_errors(self): + """ + The bundle has multiple problems. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + class ProposedBundleTestCase(TestCase): def test_add_transaction_short_message(self): """ @@ -222,7 +553,7 @@ def test_from_tryte_string(self): self.assertEqual( transaction.signature_message_fragment, - TryteString( + Fragment( b'GYPRVHBEZOOFXSHQBLCYW9ICTCISLHDBNMMVYD9JJHQMPQCTIQAQTJNNNJ9IDXLRCC' b'OYOXYPCLR9PBEY9ORZIEPPDNTI9CQWYZUOTAVBXPSBOFEQAPFLWXSWUIUSJMSJIIIZ' b'WIKIRH9GCOEVZFKNXEVCUCIIWZQCQEUVRZOCMEL9AMGXJNMLJCIA9UWGRPPHCEOPTS' @@ -340,7 +671,7 @@ def test_as_tryte_string(self): ), signature_message_fragment = - TryteString( + Fragment( b'GYPRVHBEZOOFXSHQBLCYW9ICTCISLHDBNMMVYD9JJHQMPQCTIQAQTJNNNJ9IDXLRCC' b'OYOXYPCLR9PBEY9ORZIEPPDNTI9CQWYZUOTAVBXPSBOFEQAPFLWXSWUIUSJMSJIIIZ' b'WIKIRH9GCOEVZFKNXEVCUCIIWZQCQEUVRZOCMEL9AMGXJNMLJCIA9UWGRPPHCEOPTS' From fa8362cd5dc61f52415d52074eb48491419a05d3 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 17:20:50 -0500 Subject: [PATCH 223/239] Implemented balance validation. --- iota/transaction.py | 11 +++++++++-- test/transaction_test.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 35 insertions(+), 6 deletions(-) diff --git a/iota/transaction.py b/iota/transaction.py index b019155..32f92d5 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -533,8 +533,15 @@ def _create_validator(self): """ Creates a generator that does all the work. """ - if False: - yield '' + balance = 0 + for txn in self.bundle: + balance += txn.value + + if balance != 0: + yield \ + 'Bundle has invalid balance (expected 0, actual {balance}).'.format( + balance = balance, + ) class ProposedBundle(JsonSerializable, Sequence[ProposedTransaction]): diff --git a/test/transaction_test.py b/test/transaction_test.py index ac5c913..f90aea6 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -294,15 +294,37 @@ def test_fail_balance_positive(self): """ The bundle balance is > 0. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.transactions[0].value += 1 + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Bundle has invalid balance (expected 0, actual 1).', + ], + ) def test_fail_balance_negative(self): """ The bundle balance is < 0. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.transactions[3].value -= 1 + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Bundle has invalid balance (expected 0, actual -1).', + ], + ) def test_fail_bundle_hash_invalid(self): """ From fba871b492472318e07553a40999d710ee53b13a Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 17:26:39 -0500 Subject: [PATCH 224/239] Added bundle hash check to bundle valid'n. --- iota/transaction.py | 25 ++++++++++++++++++++++++- test/transaction_test.py | 29 +++++++++++++++++++++++++++-- 2 files changed, 51 insertions(+), 3 deletions(-) diff --git a/iota/transaction.py b/iota/transaction.py index 32f92d5..e49e0a1 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -468,6 +468,22 @@ def is_confirmed(self, new_is_confirmed): for txn in self: txn.is_confirmed = new_is_confirmed + @property + def hash(self): + # type: () -> Optional[BundleHash] + """ + Returns the hash of the bundle. + + This value is determined by inspecting the bundle's tail + transaction, so in a few edge cases, it may be incorrect. + + If the bundle has no transactions, this method returns `None`. + """ + try: + return self.tail_transaction.bundle_hash + except IndexError: + return None + @property def tail_transaction(self): # type: () -> Transaction @@ -533,10 +549,17 @@ def _create_validator(self): """ Creates a generator that does all the work. """ + bundle_hash = self.bundle.hash + balance = 0 - for txn in self.bundle: + for (i, txn) in enumerate(self.bundle): # type: Tuple[int, Transaction] balance += txn.value + if txn.bundle_hash != bundle_hash: + yield 'Transaction {i} has invalid bundle hash.'.format( + i = i, + ) + if balance != 0: yield \ 'Bundle has invalid balance (expected 0, actual {balance}).'.format( diff --git a/test/transaction_test.py b/test/transaction_test.py index f90aea6..9cefaa8 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -290,6 +290,15 @@ def test_pass_happy_path(self): self.assertTrue(validator.is_valid()) self.assertListEqual(validator.errors, []) + def test_pass_empty(self): + """ + Bundle has no transactions. + """ + validator = BundleValidator(Bundle()) + + self.assertTrue(validator.is_valid()) + self.assertListEqual(validator.errors, []) + def test_fail_balance_positive(self): """ The bundle balance is > 0. @@ -330,8 +339,24 @@ def test_fail_bundle_hash_invalid(self): """ One of the transactions has an invalid ``bundle_hash`` value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + # noinspection SpellCheckingInspection + self.bundle.transactions[3].bundle_hash =\ + BundleHash( + b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' + b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' + ) + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Transaction 3 has invalid bundle hash.', + ], + ) def test_fail_current_index_invalid(self): """ From d0945641a6e4e5d75b5e3456b94d466cc0fed560 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 17:29:04 -0500 Subject: [PATCH 225/239] Added `current_index` check to bundle valid'n. --- iota/transaction.py | 12 +++++++++++- test/transaction_test.py | 16 ++++++++++++++-- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/iota/transaction.py b/iota/transaction.py index e49e0a1..c0170fc 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -560,11 +560,21 @@ def _create_validator(self): i = i, ) + if txn.current_index != i: + yield ( + 'Transaction {i} has invalid current index value ' + '(expected {i}, actual {current_index}).'.format( + current_index = txn.current_index, + i = i, + ) + ) + if balance != 0: - yield \ + yield ( 'Bundle has invalid balance (expected 0, actual {balance}).'.format( balance = balance, ) + ) class ProposedBundle(JsonSerializable, Sequence[ProposedTransaction]): diff --git a/test/transaction_test.py b/test/transaction_test.py index 9cefaa8..d9371ca 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -362,8 +362,20 @@ def test_fail_current_index_invalid(self): """ One of the transactions has an invalid ``current_index`` value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.transactions[3].current_index = 4 + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Transaction 3 has invalid current index value ' + '(expected 3, actual 4).', + ], + ) def test_fail_last_index_invalid(self): """ From 716db5d758a5eb4310d74c1937af458024e0d235 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Fri, 30 Dec 2016 17:31:39 -0500 Subject: [PATCH 226/239] Added `last_index` check to bundle vaild'n. --- iota/transaction.py | 21 ++++++++++++++++----- test/transaction_test.py | 16 ++++++++++++++-- 2 files changed, 30 insertions(+), 7 deletions(-) diff --git a/iota/transaction.py b/iota/transaction.py index c0170fc..ca18300 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -550,6 +550,7 @@ def _create_validator(self): Creates a generator that does all the work. """ bundle_hash = self.bundle.hash + last_index = len(self.bundle) - 1 balance = 0 for (i, txn) in enumerate(self.bundle): # type: Tuple[int, Transaction] @@ -563,16 +564,26 @@ def _create_validator(self): if txn.current_index != i: yield ( 'Transaction {i} has invalid current index value ' - '(expected {i}, actual {current_index}).'.format( - current_index = txn.current_index, - i = i, + '(expected {i}, actual {actual}).'.format( + actual = txn.current_index, + i = i, + ) + ) + + if txn.last_index != last_index: + yield ( + 'Transaction {i} has invalid last index value ' + '(expected {expected}, actual {actual}).'.format( + actual = txn.last_index, + expected = last_index, + i = i, ) ) if balance != 0: yield ( - 'Bundle has invalid balance (expected 0, actual {balance}).'.format( - balance = balance, + 'Bundle has invalid balance (expected 0, actual {actual}).'.format( + actual = balance, ) ) diff --git a/test/transaction_test.py b/test/transaction_test.py index d9371ca..68ee3c1 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -381,8 +381,20 @@ def test_fail_last_index_invalid(self): """ One of the transactions has an invalid ``last_index`` value. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + self.bundle.transactions[0].last_index = 2 + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Transaction 0 has invalid last index value ' + '(expected 3, actual 2).' + ], + ) def test_fail_signature_invalid(self): """ From 68c6c74b6f492be360b387ca13a7b69190900275 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 12:03:13 -0500 Subject: [PATCH 227/239] Added integrity checks to BundleValidator. --- iota/transaction.py | 36 +++++++++++++++++++++++++++++ test/transaction_test.py | 50 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 86 insertions(+) diff --git a/iota/transaction.py b/iota/transaction.py index ca18300..e62ec59 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -587,6 +587,42 @@ def _create_validator(self): ) ) + # Signature validation is only meaningful if the transactions are + # otherwise valid. + if not self._errors: + i = 0 + while i <= last_index: + txn = self.bundle[i] + + if txn.value < 0: + # The next transaction should contain the second fragment. + try: + next_txn = self.bundle[i+1] + except IndexError: + yield ( + 'Reached end of bundle while looking for ' + 'second signature fragment for transaction {i}.'.format( + i = i, + ) + ) + i += 1 + continue + + if next_txn.address != txn.address: + yield ( + 'Unable to find second signature fragment ' + 'for transaction {i}.'.format( + i = i, + ) + ) + i += 1 + continue + + i += 1 + else: + # No signature to validate; skip this transaction. + i += 1 + class ProposedBundle(JsonSerializable, Sequence[ProposedTransaction]): """ diff --git a/test/transaction_test.py b/test/transaction_test.py index 68ee3c1..e0870cc 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -2,6 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals +from typing import Tuple from unittest import TestCase from iota import Address, Bundle, BundleHash, Fragment, Hash, Tag, \ @@ -396,6 +397,55 @@ def test_fail_last_index_invalid(self): ], ) + def test_fail_missing_signature_fragment(self): + """ + One of the inputs is missing its second signature fragment. + """ + del self.bundle.transactions[2] + for (i, txn) in enumerate(self.bundle): # type: Tuple[int, Transaction] + txn.current_index = i + txn.last_index = 2 + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Unable to find second signature fragment for transaction 1.' + ], + ) + + def test_fail_missing_signature_fragment_overflow(self): + """ + The last transaction in the bundle is an input, and its second + signature fragment is missing. + """ + # Remove the last input's second signature fragment, and the change + # transaction. + del self.bundle.transactions[-2:] + for (i, txn) in enumerate(self.bundle): # type: Tuple[int, Transaction] + txn.current_index = i + txn.last_index = 1 + + # Fix bundle balance, since we removed the change transaction. + self.bundle[1].value = -self.bundle[0].value + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Reached end of bundle while looking for ' + 'second signature fragment for transaction 1.' + ], + ) + def test_fail_signature_invalid(self): """ One of the input signatures fails validation. From a8f177a1fba72c8026ab7ac16a6f6d5837eaa8ce Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 12:15:35 -0500 Subject: [PATCH 228/239] Check 2nd sMF txn amount value. --- iota/transaction.py | 23 +++++++++++---- test/transaction_test.py | 64 +++++++++++++++++++++++++++------------- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/iota/transaction.py b/iota/transaction.py index e62ec59..ab0f78b 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -595,30 +595,43 @@ def _create_validator(self): txn = self.bundle[i] if txn.value < 0: + signature_fragments = [txn.signature_message_fragment] + # The next transaction should contain the second fragment. + i += 1 try: - next_txn = self.bundle[i+1] + next_txn = self.bundle[i] except IndexError: yield ( 'Reached end of bundle while looking for ' 'second signature fragment for transaction {i}.'.format( - i = i, + i = txn.current_index, ) ) - i += 1 continue if next_txn.address != txn.address: yield ( 'Unable to find second signature fragment ' 'for transaction {i}.'.format( - i = i, + i = txn.current_index, ) ) + continue + + if next_txn.value != 0: + yield ( + 'Transaction {i} has invalid amount ' + '(expected 0, actual {actual}).'.format( + actual = next_txn.value, + i = next_txn.current_index, + ) + ) + # Skip ``next_txn`` in the next iteration. i += 1 continue - i += 1 + signature_fragments.append(next_txn.signature_message_fragment) else: # No signature to validate; skip this transaction. i += 1 diff --git a/test/transaction_test.py b/test/transaction_test.py index e0870cc..314334f 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -397,14 +397,20 @@ def test_fail_last_index_invalid(self): ], ) - def test_fail_missing_signature_fragment(self): + def test_fail_missing_signature_fragment_underflow(self): """ - One of the inputs is missing its second signature fragment. + The last transaction in the bundle is an input, and its second + signature fragment is missing. """ - del self.bundle.transactions[2] + # Remove the last input's second signature fragment, and the change + # transaction. + del self.bundle.transactions[-2:] for (i, txn) in enumerate(self.bundle): # type: Tuple[int, Transaction] txn.current_index = i - txn.last_index = 2 + txn.last_index = 1 + + # Fix bundle balance, since we removed the change transaction. + self.bundle[1].value = -self.bundle[0].value validator = BundleValidator(self.bundle) @@ -414,24 +420,41 @@ def test_fail_missing_signature_fragment(self): validator.errors, [ - 'Unable to find second signature fragment for transaction 1.' + 'Reached end of bundle while looking for ' + 'second signature fragment for transaction 1.' ], ) - def test_fail_missing_signature_fragment_overflow(self): + def test_fail_signature_fragment_address_wrong(self): """ - The last transaction in the bundle is an input, and its second - signature fragment is missing. + The second signature fragment for an input is associated with the + wrong address. """ - # Remove the last input's second signature fragment, and the change - # transaction. - del self.bundle.transactions[-2:] - for (i, txn) in enumerate(self.bundle): # type: Tuple[int, Transaction] - txn.current_index = i - txn.last_index = 1 + # noinspection SpellCheckingInspection + self.bundle[2].address =\ + Address( + b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9P' + b'GNFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' + ) - # Fix bundle balance, since we removed the change transaction. - self.bundle[1].value = -self.bundle[0].value + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Unable to find second signature fragment for transaction 1.' + ], + ) + + def test_fail_signature_fragment_value_wrong(self): + """ + The second signature fragment for an input has a nonzero balance. + """ + self.bundle[2].value = -1 + self.bundle[-1].value += 1 validator = BundleValidator(self.bundle) @@ -441,8 +464,7 @@ def test_fail_missing_signature_fragment_overflow(self): validator.errors, [ - 'Reached end of bundle while looking for ' - 'second signature fragment for transaction 1.' + 'Transaction 2 has invalid amount (expected 0, actual -1).', ], ) @@ -841,19 +863,19 @@ def test_as_tryte_string(self): current_index = 1, last_index = 1, - bundle_hash= + bundle_hash = BundleHash( b'NFDPEEZCWVYLKZGSLCQNOFUSENIXRHWWTZFBXMPS' b'QHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PG' ), - trunk_transaction_hash= + trunk_transaction_hash = TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' ), - branch_transaction_hash= + branch_transaction_hash = TransactionHash( b'TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJN' b'QZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999999' From ffdb59fcf4d3bfe09dde97e3bf7d7e15b6965e77 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 12:35:07 -0500 Subject: [PATCH 229/239] Implemented `TryteString.__setitem__`. --- iota/types.py | 22 ++++++++++++++++++++++ test/transaction_test.py | 25 +++++++++++++++++++++++-- test/types_test.py | 31 ++++++++++++++++++++++++++++++- 3 files changed, 75 insertions(+), 3 deletions(-) diff --git a/iota/types.py b/iota/types.py index f2c51bd..92da741 100644 --- a/iota/types.py +++ b/iota/types.py @@ -305,6 +305,28 @@ def __getitem__(self, item): return TryteString(new_trytes) + def __setitem__(self, item, trytes): + # type: (Union[int, slice], TrytesCompatible) -> None + new_trytes = TryteString(trytes) + + if isinstance(item, slice): + self._trytes[item] = new_trytes._trytes + elif len(new_trytes) > 1: + raise with_context( + exc = ValueError( + 'Cannot assign multiple trytes to the same index ' + '(``exc.context`` has more info).' + ), + + context = { + 'self': self, + 'index': item, + 'new_trytes': new_trytes, + }, + ) + else: + self._trytes[item] = new_trytes._trytes[0] + def __add__(self, other): # type: (TrytesCompatible) -> TryteString if isinstance(other, TryteString): diff --git a/test/transaction_test.py b/test/transaction_test.py index 314334f..59097f6 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -479,8 +479,29 @@ def test_fail_multiple_errors(self): """ The bundle has multiple problems. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + del self.bundle.transactions[2] + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Transaction 0 has invalid last index value ' + '(expected 2, actual 3).', + + 'Transaction 1 has invalid last index value ' + '(expected 2, actual 3).', + + 'Transaction 2 has invalid current index value ' + '(expected 2, actual 3).', + + 'Transaction 2 has invalid last index value ' + '(expected 2, actual 3).', + ], + ) class ProposedBundleTestCase(TestCase): diff --git a/test/types_test.py b/test/types_test.py index 155d0d5..44a2fd3 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -191,7 +191,7 @@ def test_concatenation_error_wrong_type(self): # What is this I don't even.. trytes += None - def test_slice(self): + def test_slice_accessor(self): """ Taking slices of a TryteString. """ @@ -204,6 +204,35 @@ def test_slice(self): self.assertEqual(ts[-4:], TryteString(b'KBFA')) self.assertEqual(ts[4:-4:4], TryteString(b'9CEY')) + def test_slice_mutator(self): + """ + Modifying slices of a TryteString. + """ + ts = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + + ts[4] = TryteString(b'A') + self.assertEqual(ts, TryteString(b'RBTCAD9DCDQAEASBYBCCKBFA')) + + ts[:4] = TryteString(b'BCDE') + self.assertEqual(ts, TryteString(b'BCDEAD9DCDQAEASBYBCCKBFA')) + + # The lengths do not have to be the same... + ts[:-4] = TryteString(b'EFGHIJ') + self.assertEqual(ts, TryteString(b'EFGHIJKBFA')) + + # ... unless you are trying to set a single tryte. + with self.assertRaises(ValueError): + ts[4] = TryteString(b'99') + + # Any TrytesCompatible value will work. + ts[3:-3] = b'FOOBAR' + self.assertEqual(ts, TryteString(b'EFGFOOBARBFA')) + + # I have no idea why you would ever need to do this, but I'm not + # going to judge, either. + ts[2:-2:2] = b'IOTA' + self.assertEqual(ts, TryteString(b'EFIFOOTAABFA')) + def test_iter_chunks(self): """ Iterating over a TryteString in constant-size chunks. From f64e8162dbea004187dc9adcfa6cbefe1e07d90c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 13:53:10 -0500 Subject: [PATCH 230/239] General code cleanup. - Made code more DRY. - Renamed incorrect references to "block" (s/b "fragment"). - Documented PyCurl constants. - `AddressGenerator.DIGEST_ITERATIONS` can be > 3. --- iota/crypto/__init__.py | 9 +++ iota/crypto/addresses.py | 11 ++++ iota/crypto/pycurl.py | 33 +++++++++-- iota/crypto/signing.py | 40 ++++++------- iota/crypto/types.py | 70 ++++++++--------------- iota/transaction.py | 108 ++++++++++++++++++++++++------------ iota/types.py | 2 +- test/crypto/signing_test.py | 19 ++++--- test/transaction_test.py | 21 ++++++- 9 files changed, 194 insertions(+), 119 deletions(-) diff --git a/iota/crypto/__init__.py b/iota/crypto/__init__.py index 1d325fa..63ca830 100644 --- a/iota/crypto/__init__.py +++ b/iota/crypto/__init__.py @@ -7,3 +7,12 @@ # If a compiled c extension is available, we will prefer to load that # (once implemented). from .pycurl import * + + +FRAGMENT_LENGTH = 2187 +""" +Number of trytes per fragment. + +Fragments are used to divide up really long tryte sequences into +manageable chunks (similar in concept to AES blocks). +""" diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 0467ec6..4f97643 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -28,6 +28,17 @@ class AddressGenerator(Iterable[Address]): when you use the API (: """ DIGEST_ITERATIONS = 2 + """ + Number of iterations to use when creating digests, used to create + addresses. + + Note: this also impacts a few other things like length of transaction + signatures. + + References: + - :py:meth:`iota.transaction.ProposedBundle.sign_inputs` + - :py:class:`iota.transaction.BundleValidator` + """ def __init__(self, seed): # type: (TrytesCompatible) -> None diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index 96bdbe4..d52b5de 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -11,11 +11,36 @@ 'HASH_LENGTH', ] -HASH_LENGTH = 243 -STATE_LENGTH = 3 * HASH_LENGTH -NUMBER_OF_ROUNDS = 27 -TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] +HASH_LENGTH = 243 +""" +Number of trits in a hash. + +Note: These constants are usually expressed in _trytes_ in PyOTA, but +for compatibility with other libraries, this value must be _trits_. +""" + +STATE_LENGTH = 3 * HASH_LENGTH +""" +Number of trits that a Curl sponge stores internally. +""" + +NUMBER_OF_ROUNDS = 27 +""" +Number of iterations to perform per transform operation. + +References: + - :py:meth:`Curl._transform`. +""" + +TRUTH_TABLE = [1, 0, -1, 1, -1, 0, -1, 1, 0] +""" +Lookup table, used to ensure that the result of a Curl operation is +deterministic but not reversible. + +References: + - :py:meth:`Curl._transform`. +""" class Curl(object): diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 8c8d53f..132e411 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -2,10 +2,10 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, List, MutableSequence +from typing import Generator, Iterable, List, MutableSequence, Tuple from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible, Hash -from iota.crypto import Curl, HASH_LENGTH +from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH from iota.crypto.types import PrivateKey from iota.exceptions import with_context @@ -19,11 +19,6 @@ class KeyGenerator(object): """ Generates signing keys for messages. """ - HASHES_PER_BLOCK = 27 - """ - Number of hashes that make up one block in a signing key. - """ - def __init__(self, seed): # type: (TrytesCompatible) -> None super(KeyGenerator, self).__init__() @@ -158,23 +153,25 @@ def create_generator(self, start=0, step=1, iterations=1): current = start + fragment_length = FRAGMENT_LENGTH * TRITS_PER_TRYTE + hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN + while current >= 0: sponge = self._create_sponge(current) - # Multiply by 3 to convert trytes into trits. - block_length = PrivateKey.BLOCK_LEN * TRITS_PER_TRYTE - - key = [0] * (block_length * iterations) + key = [0] * (fragment_length * iterations) buffer = [0] * HASH_LENGTH # type: MutableSequence[int] - for block_seq in range(iterations): + for fragment_seq in range(iterations): # Squeeze trits from the buffer and append them to the key, one # hash at a time. - for hash_seq in range(self.HASHES_PER_BLOCK): + for hash_seq in range(hashes_per_fragment): sponge.squeeze(buffer) - key_start = (block_seq * block_length) + (hash_seq * HASH_LENGTH) - key_stop = key_start + HASH_LENGTH + key_start =\ + (fragment_seq * fragment_length) + (hash_seq * HASH_LENGTH) + + key_stop = key_start + HASH_LENGTH key[key_start:key_stop] = buffer @@ -218,8 +215,8 @@ class SignatureFragmentGenerator(object): """ Used to generate signature fragments progressively. - Each instance can generate 1 signature per block (2187 trytes) in the - private key. + Each instance can generate 1 signature per fragment in the private + key. Note: This class behaves more like a coroutine than an iterator; you must invoke the instance's :py:meth:`send` method to generate a new @@ -229,7 +226,7 @@ def __init__(self, private_key): # type: (PrivateKey) -> None super(SignatureFragmentGenerator, self).__init__() - self._key_chunks = private_key.iter_chunks(PrivateKey.BLOCK_LEN) + self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) self._sponge = Curl() def __len__(self): @@ -256,17 +253,16 @@ def send(self, source_trytes): """ key_trytes = next(self._key_chunks) # type: TryteString - hashes_per_block = PrivateKey.BLOCK_LEN // Hash.LEN + hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN # Ensure ``source_trits`` is long enough. - # It must have at least 1 trit per hash. source_trits = source_trytes.as_trits() - source_trits += [0] * max(0, hashes_per_block - len(source_trytes)) + source_trits += [0] * max(0, hashes_per_fragment - len(source_trytes)) signature = key_trytes.as_trits() # Build the signature, one hash at a time. - for i in range(hashes_per_block): + for i in range(hashes_per_fragment): hash_start = i * HASH_LENGTH hash_end = hash_start + HASH_LENGTH diff --git a/iota/crypto/types.py b/iota/crypto/types.py index 6418ba1..7e10dca 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -4,13 +4,14 @@ from math import ceil from os import urandom -from typing import Callable, List, Optional +from typing import Callable, List, MutableSequence, Optional, Tuple -from iota import Hash, TRITS_PER_TRYTE, TryteString, TrytesCompatible -from iota.crypto import HASH_LENGTH, Curl -from iota.exceptions import with_context from six import binary_type +from iota import Hash, TryteString, TrytesCompatible +from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH +from iota.exceptions import with_context + __all__ = [ 'PrivateKey', 'Seed', @@ -52,22 +53,16 @@ class PrivateKey(TryteString): A TryteString that acts as a private key, e.g., for generating message signatures, new addresses, etc. """ - BLOCK_LEN = 2187 - """ - Similar to RSA keys, SigningKeys must have a length that is divisible - by a certain number of trytes. - """ - def __init__(self, trytes, key_index=None): # type: (TrytesCompatible, Optional[int]) -> None super(PrivateKey, self).__init__(trytes) - if len(self._trytes) % self.BLOCK_LEN: + if len(self._trytes) % FRAGMENT_LENGTH: raise with_context( exc = ValueError( 'Length of {cls} values must be a multiple of {len} trytes.'.format( cls = type(self).__name__, - len = self.BLOCK_LEN + len = FRAGMENT_LENGTH ), ), @@ -78,14 +73,6 @@ def __init__(self, trytes, key_index=None): self.key_index = key_index - @property - def block_count(self): - # type: () -> int - """ - Returns the length of this key, expressed in blocks. - """ - return len(self) // self.BLOCK_LEN - def get_digest_trits(self): # type: () -> List[int] """ @@ -98,43 +85,34 @@ def get_digest_trits(self): through a PBKDF, yielding a constant-length hash that can be used for crypto. """ - block_size = self.BLOCK_LEN * TRITS_PER_TRYTE - hashes_per_block = block_size // HASH_LENGTH - - raw_trits = self.as_trits() - - # Initialize list with the correct length to improve performance. - digest = [0] * HASH_LENGTH # type: List[int] - - for i in range(self.block_count): - block_start = i * block_size - block_end = block_start + block_size - - block_trits = raw_trits[block_start:block_end] + hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN - # Initialize ``key_fragment`` with the correct length to - # improve performance. - key_fragment = [0] * block_size # type: List[int] + digest = [0] * HASH_LENGTH - buffer = [] # type: List[int] + for (i, fragment) in enumerate(self.iter_chunks(FRAGMENT_LENGTH)): # type: Tuple[int, TryteString] + fragment_start = i * FRAGMENT_LENGTH + fragment_end = fragment_start + FRAGMENT_LENGTH + fragment_trits = fragment[fragment_start:fragment_end].as_trits() - for j in range(hashes_per_block): - hash_start = j * HASH_LENGTH - hash_end = hash_start + HASH_LENGTH + key_fragment = [0] * len(fragment_trits) + hash_trits = [] - buffer = block_trits[hash_start:hash_end] + for j in range(hashes_per_fragment): + hash_start = j * HASH_LENGTH + hash_end = hash_start + HASH_LENGTH + hash_trits = fragment_trits[hash_start:hash_end] # type: MutableSequence[int] for k in range(26): sponge = Curl() - sponge.absorb(buffer) - sponge.squeeze(buffer) + sponge.absorb(hash_trits) + sponge.squeeze(hash_trits) - key_fragment[hash_start:hash_end] = buffer + key_fragment[hash_start:hash_end] = hash_trits sponge = Curl() sponge.absorb(key_fragment) - sponge.squeeze(buffer) + sponge.squeeze(hash_trits) - digest[block_start:block_end] = buffer + digest[fragment_start:fragment_end] = hash_trits return digest diff --git a/iota/transaction.py b/iota/transaction.py index ab0f78b..7b22a6d 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -9,7 +9,7 @@ from iota import Address, Hash, Tag, TrytesCompatible, TryteString, \ int_from_trits, trits_from_int -from iota.crypto import Curl, HASH_LENGTH +from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH from iota.crypto.addresses import AddressGenerator from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator from iota.exceptions import with_context @@ -59,7 +59,7 @@ class Fragment(TryteString): """ A signature/message fragment in a transaction. """ - LEN = 2187 + LEN = FRAGMENT_LENGTH def __init__(self, trytes): # type: (TrytesCompatible) -> None @@ -597,41 +597,75 @@ def _create_validator(self): if txn.value < 0: signature_fragments = [txn.signature_message_fragment] - # The next transaction should contain the second fragment. - i += 1 - try: - next_txn = self.bundle[i] - except IndexError: - yield ( - 'Reached end of bundle while looking for ' - 'second signature fragment for transaction {i}.'.format( - i = txn.current_index, + # The following transaction(s) should contain additional + # fragments. + fragments_valid = True + j = 0 + for j in range(1, AddressGenerator.DIGEST_ITERATIONS): + i += 1 + try: + next_txn = self.bundle[i] + except IndexError: + yield ( + 'Reached end of bundle while looking for ' + 'signature fragment {j} for transaction {i}.'.format( + i = txn.current_index, + j = j+1, + ) ) - ) - continue - - if next_txn.address != txn.address: - yield ( - 'Unable to find second signature fragment ' - 'for transaction {i}.'.format( - i = txn.current_index, + fragments_valid = False + break + + if next_txn.address != txn.address: + yield ( + 'Unable to find signature fragment {j} ' + 'for transaction {i}.'.format( + i = txn.current_index, + j = j+1, + ) ) - ) - continue - - if next_txn.value != 0: - yield ( - 'Transaction {i} has invalid amount ' - '(expected 0, actual {actual}).'.format( - actual = next_txn.value, - i = next_txn.current_index, + fragments_valid = False + break + + if next_txn.value != 0: + yield ( + 'Transaction {i} has invalid amount ' + '(expected 0, actual {actual}).'.format( + actual = next_txn.value, + i = next_txn.current_index, + ) + ) + fragments_valid = False + # Keep going, just in case there's another signature + # fragment next (so that we skip it in the next iteration + # of the outer loop). + continue + + signature_fragments.append(next_txn.signature_message_fragment) + + if fragments_valid: + signature_valid = True + # signature_valid = validate_signature_fragments( + # fragments = signature_fragments, + # source_trytes = txn.bundle_hash, + # public_key = txn.address, + # ) + + if not signature_valid: + yield ( + 'Transaction {i} has invalid signature ' + '(using {fragments} fragments).'.format( + fragments = len(signature_fragments), + i = txn.current_index, + ) ) - ) - # Skip ``next_txn`` in the next iteration. - i += 1 - continue - signature_fragments.append(next_txn.signature_message_fragment) + # Skip signature fragments in the next iteration. + # Note that it's possible to have + # ``j < AddressGenerator.DIGEST_ITERATIONS`` if the bundle is + # badly malformed. + i += j + else: # No signature to validate; skip this transaction. i += 1 @@ -922,7 +956,7 @@ def sign_inputs(self, key_generator): signature_fragment_generator =\ self._create_signature_fragment_generator(key_generator, txn) - hash_fragment_iterator = self.hash.iter_chunks(9) + bundle_hash_chunks = list(self.hash.iter_chunks(9)) # We can only fit one signature fragment into each transaction, # so we have to split the entire signature among the extra @@ -930,7 +964,11 @@ def sign_inputs(self, key_generator): # :py:meth:`add_inputs`. for j in range(AddressGenerator.DIGEST_ITERATIONS): self[i+j].signature_message_fragment =\ - signature_fragment_generator.send(next(hash_fragment_iterator)) + signature_fragment_generator.send( + # If there are more than 3 iterations, we loop back + # around to the start of the bundle hash. + bundle_hash_chunks[j % len(bundle_hash_chunks)] + ) i += AddressGenerator.DIGEST_ITERATIONS else: diff --git a/iota/types.py b/iota/types.py index 92da741..4d8423d 100644 --- a/iota/types.py +++ b/iota/types.py @@ -553,7 +553,7 @@ class Hash(TryteString): A TryteString that is exactly one hash long. """ # Divide by 3 to convert trits to trytes. - LEN = HASH_LENGTH // 3 + LEN = HASH_LENGTH // TRITS_PER_TRYTE def __init__(self, trytes): # type: (TrytesCompatible) -> None diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 41f3671..5698367 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -812,9 +812,9 @@ def test_generator_with_iterations(self): # noinspection SpellCheckingInspection class SignatureFragmentGeneratorTestCase(TestCase): - def test_single_block(self): + def test_single_fragment(self): """ - Creating signature fragments from a PrivateKey exactly 1 block + Creating signature fragments from a PrivateKey exactly 1 fragment long. """ generator = SignatureFragmentGenerator( @@ -897,14 +897,15 @@ def test_single_block(self): ), ) - # A generator can only generate one signature fragment per block in - # its private key. + # A generator can only generate one signature fragment per fragment + # in its private key. with self.assertRaises(StopIteration): generator.send(TryteString(b'')) - def test_multiple_blocks(self): + def test_multiple_fragments(self): """ - Creating signature fragments from a PrivateKey longer than 1 block. + Creating signature fragments from a PrivateKey longer than 1 + fragment. """ generator = SignatureFragmentGenerator( PrivateKey( @@ -1024,7 +1025,7 @@ def test_multiple_blocks(self): # second iteration. generator.send(TryteString(b'WGXG9AGGI')), - # The generator used the second block of the private key this + # The generator used the second fragment of the private key this # time, so we get a different result. TryteString( b'QFBNTRFTTBHNIPZQNQPXOFQWQCP9KMMMVEPLHYQMVMCWSVSWS9HEEJHNRGKELXEEKI' @@ -1064,7 +1065,7 @@ def test_multiple_blocks(self): ), ) - # A generator can only generate one signature fragment per block in - # its private key. + # A generator can only generate one signature fragment per fragment + # in its private key. with self.assertRaises(StopIteration): generator.send(TryteString(b'')) diff --git a/test/transaction_test.py b/test/transaction_test.py index 59097f6..99ca471 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -421,7 +421,7 @@ def test_fail_missing_signature_fragment_underflow(self): [ 'Reached end of bundle while looking for ' - 'second signature fragment for transaction 1.' + 'signature fragment 2 for transaction 1.' ], ) @@ -445,7 +445,7 @@ def test_fail_signature_fragment_address_wrong(self): validator.errors, [ - 'Unable to find second signature fragment for transaction 1.' + 'Unable to find signature fragment 2 for transaction 1.' ], ) @@ -474,6 +474,19 @@ def test_fail_signature_invalid(self): """ # :todo: Implement test. self.skipTest('Not implemented yet.') + # self.bundle[2].signature_message_fragment[:-1] = b'9' + # + # validator = BundleValidator(self.bundle) + # + # self.assertFalse(validator.is_valid()) + # + # self.assertListEqual( + # validator.errors, + # + # [ + # 'Transaction 1 has invalid signature (using 2 fragments).', + # ], + # ) def test_fail_multiple_errors(self): """ @@ -488,6 +501,10 @@ def test_fail_multiple_errors(self): self.assertListEqual( validator.errors, + # Note that there is no error about the missing signature + # fragment for transaction 1. The bundle fails some basic + # consistency checks, so we don't even bother to validate + # signatures. [ 'Transaction 0 has invalid last index value ' '(expected 2, actual 3).', From 483309f12f7de9e7aba752906be83409f2a7cf97 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 15:03:09 -0500 Subject: [PATCH 231/239] Refactored signature generation. `SignatureFragmentGenerator` now operates like an iterator rather than a coroutine, to match how signature validation works. --- iota/crypto/signing.py | 51 ++++++++++--------- iota/transaction.py | 12 ++--- .../extended/prepare_transfer_test.py | 10 +++- test/crypto/signing_test.py | 25 +++++---- 4 files changed, 54 insertions(+), 44 deletions(-) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 132e411..76901e0 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -2,12 +2,13 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, Iterable, List, MutableSequence, Tuple +from typing import Generator, Iterator, List, MutableSequence from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible, Hash from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH from iota.crypto.types import PrivateKey from iota.exceptions import with_context +from six import PY2 __all__ = [ 'KeyGenerator', @@ -211,23 +212,25 @@ def _create_sponge(self, index): return sponge -class SignatureFragmentGenerator(object): +class SignatureFragmentGenerator(Iterator[TryteString]): """ Used to generate signature fragments progressively. Each instance can generate 1 signature per fragment in the private key. - - Note: This class behaves more like a coroutine than an iterator; you - must invoke the instance's :py:meth:`send` method to generate a new - value. """ - def __init__(self, private_key): - # type: (PrivateKey) -> None + def __init__(self, private_key, source_trytes): + # type: (PrivateKey, TryteString) -> None super(SignatureFragmentGenerator, self).__init__() - self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) - self._sponge = Curl() + self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) + self._iteration = -1 + self._source_chunks = list(source_trytes.iter_chunks(9)) # type: List[TryteString] + self._sponge = Curl() + + def __iter__(self): + # type: () -> SignatureFragmentGenerator + return self def __len__(self): # type: () -> int @@ -239,25 +242,22 @@ def __len__(self): """ return len(self._key_chunks) - def send(self, source_trytes): - # type: (TryteString) -> TryteString + def __next__(self): + # type: () -> TryteString """ - Sends a source string to the generator to create the next fragment. - - :param source_trytes: - Trytes to use to generate the signature. - - Note: should be 27 trytes long. - If too short, will be padded. - If too long, extra trytes will be ignored. + Returns the next signature fragment. """ key_trytes = next(self._key_chunks) # type: TryteString + self._iteration += 1 - hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN + # If the key is long enough, loop back around to the start of the + # source chunks. + source_trits = ( + self._source_chunks[self._iteration % len(self._source_chunks)] + .as_trits() + ) - # Ensure ``source_trits`` is long enough. - source_trits = source_trytes.as_trits() - source_trits += [0] * max(0, hashes_per_fragment - len(source_trytes)) + hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN signature = key_trytes.as_trits() @@ -278,3 +278,6 @@ def send(self, source_trytes): signature[hash_start:hash_end] = fragment return TryteString.from_trits(signature) + + if PY2: + next = __next__ diff --git a/iota/transaction.py b/iota/transaction.py index 7b22a6d..de5ffff 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -956,19 +956,13 @@ def sign_inputs(self, key_generator): signature_fragment_generator =\ self._create_signature_fragment_generator(key_generator, txn) - bundle_hash_chunks = list(self.hash.iter_chunks(9)) - # We can only fit one signature fragment into each transaction, # so we have to split the entire signature among the extra # transactions we created for this input in # :py:meth:`add_inputs`. for j in range(AddressGenerator.DIGEST_ITERATIONS): self[i+j].signature_message_fragment =\ - signature_fragment_generator.send( - # If there are more than 3 iterations, we loop back - # around to the start of the bundle hash. - bundle_hash_chunks[j % len(bundle_hash_chunks)] - ) + next(signature_fragment_generator) i += AddressGenerator.DIGEST_ITERATIONS else: @@ -986,8 +980,10 @@ def _create_signature_fragment_generator(key_generator, txn): tests. """ return SignatureFragmentGenerator( - key_generator.get_keys( + private_key = key_generator.get_keys( start = txn.address.key_index, iterations = AddressGenerator.DIGEST_ITERATIONS )[0], + + source_trytes = txn.bundle_hash, ) diff --git a/test/commands/extended/prepare_transfer_test.py b/test/commands/extended/prepare_transfer_test.py index 226637a..1e418aa 100644 --- a/test/commands/extended/prepare_transfer_test.py +++ b/test/commands/extended/prepare_transfer_test.py @@ -15,7 +15,7 @@ from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import GeneratedAddress, Trytes -from six import binary_type, text_type +from six import PY2, binary_type, text_type from test import MockAdapter @@ -2814,6 +2814,9 @@ def __init__( self.fragments = list(fragments or []) # type: List[TryteString] self.length = length + def __iter__(self): + return self + def __len__(self): return self.length @@ -2822,6 +2825,9 @@ def seed(self, fragment): self.fragments.append(fragment) # noinspection PyUnusedLocal - def send(self, source_trytes): + def __next__(self): # type: (TryteString) -> TryteString return self.fragments.pop(0) + + if PY2: + next = __next__ diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 5698367..a963be8 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -818,7 +818,7 @@ def test_single_fragment(self): long. """ generator = SignatureFragmentGenerator( - PrivateKey( + private_key = PrivateKey( b'QFBNTRFTTBHNIPZQNQPXOFQWQCP9KMMMVEPLHYQMVMCWSVSWS9HEEJHNRGKELXEEKI' b'DBREGBJTIKHBMODRBXEOKYOXTPPYGVSROOMLBHQKFALHNUYEVUAMEYPNRLFFNCDGBF' b'YZQRLGLVQEPCYGP9HRUWRVIRZYFTFJNEJKCLUMFDXULSOWKVSEMVFXHAHLXBZITIHR' @@ -854,10 +854,12 @@ def test_single_fragment(self): b'EHIPLPZSFDJCALFZNPAKSLPD9QEBZIZNCAMKBZQFWRCFSVIEBDDSVMQIYDCWLQQGPC' b'HAXQWELNH' ), + + source_trytes = TryteString(b'UDUX9OKZP') ) self.assertEqual( - generator.send(TryteString(b'UDUX9OKZP')), + next(generator), TryteString( b'CGWXNMRWQRICZ9QULUMMHLTGIC9ILGBPBLX9OXQOAFDUPFOEQMQBZPVIVDWCBOFCN9' @@ -900,7 +902,7 @@ def test_single_fragment(self): # A generator can only generate one signature fragment per fragment # in its private key. with self.assertRaises(StopIteration): - generator.send(TryteString(b'')) + next(generator) def test_multiple_fragments(self): """ @@ -908,7 +910,7 @@ def test_multiple_fragments(self): fragment. """ generator = SignatureFragmentGenerator( - PrivateKey( + private_key = PrivateKey( b'MPRSBNPSAZRHZSBXSJASXSEHLYVLBXHCUNNQUNYB9CYEVWNBJKZUODPCZGOBRJ9V9C' b'CARPTZEQRXGYDBEZISGMMXGHHHATAUDRTWUHEKJAMKUUKCK9NUUEWEFHPTVAARNLJB' b'XRTFHQDXAVAOWYGJJNLCRVWQFHXPTMLFPGDKEBZCSSOQZEPNRTUKUJ9TRHLWQKOCBZ' @@ -976,11 +978,16 @@ def test_multiple_fragments(self): b'BCQNMVIQ9EXTSCQAJJROGKIHJJTKLOABYQDHMXJFEKFN9L9FSYAMVOFTXIOSEOBBUA' b'UYSHWINHCJRQTIXWWPZBGKIGJC9TDKFAKZTIFBSDFSXVJCCICIBEAIGNUARDZGGBBH' b'RLMWQ9RSJB9TPLSDEP' - ) + ), + + # Just to be tricky, we will use the same source trytes for each + # iteration, just to make sure the generator isn't cheating. + # We should get a different result for each iteration. + source_trytes = TryteString(b'WGXG9AGGIWGXG9AGGI') ) self.assertEqual( - generator.send(TryteString(b'WGXG9AGGI')), + next(generator), TryteString( b'GNCUHGZVIYRQQBXXBUONVL9COKOYDERJAWWI9YRWBVUJLDQQCBMFOORHRTVPKUDFWK' @@ -1021,9 +1028,7 @@ def test_multiple_fragments(self): ) self.assertEqual( - # Just to be tricky, try sending the same source trytes for the - # second iteration. - generator.send(TryteString(b'WGXG9AGGI')), + next(generator), # The generator used the second fragment of the private key this # time, so we get a different result. @@ -1068,4 +1073,4 @@ def test_multiple_fragments(self): # A generator can only generate one signature fragment per fragment # in its private key. with self.assertRaises(StopIteration): - generator.send(TryteString(b'')) + next(generator) From 72b99801e6486a6716a089673d1843493a5e53fe Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 16:49:20 -0500 Subject: [PATCH 232/239] Implemented signature validation. --- iota/crypto/signing.py | 127 ++++++++++++++++++--- iota/transaction.py | 14 +-- iota/types.py | 44 +++++--- test/crypto/signing_test.py | 217 ++++++++++++++++++------------------ test/transaction_test.py | 28 +++-- 5 files changed, 270 insertions(+), 160 deletions(-) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 76901e0..979bbda 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -2,20 +2,64 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Generator, Iterator, List, MutableSequence +from typing import Generator, Iterator, List, MutableSequence, Sequence, Tuple + +from six import PY2 from iota import TRITS_PER_TRYTE, TryteString, TrytesCompatible, Hash from iota.crypto import Curl, FRAGMENT_LENGTH, HASH_LENGTH from iota.crypto.types import PrivateKey from iota.exceptions import with_context -from six import PY2 __all__ = [ 'KeyGenerator', 'SignatureFragmentGenerator', + 'normalize', + 'validate_signature_fragments', ] +def normalize(hash_): + # type: (Hash) -> List[List[int]] + """ + "Normalizes" a hash, converting it into a sequence of integers + (not trits!) suitable for use in signature generation/validation. + + The hash is divided up into 3 parts, each of which is "balanced" (sum + of all the values is equal to zero). + """ + normalized = [] + source = hash_.as_integers() + + chunk_size = 27 + + for i in range(Hash.LEN // chunk_size): + start = i * chunk_size + stop = start + chunk_size + + chunk = source[start:stop] + chunk_sum = sum(chunk) + + while chunk_sum > 0: + chunk_sum -= 1 + for j in range(chunk_size): + if chunk[j] > -13: + chunk[j] -= 1 + break + + + while chunk_sum < 0: + chunk_sum += 1 + for j in range(chunk_size): + if chunk[j] < 13: + chunk[j] += 1 + break + + normalized.append(chunk) + + return normalized + + class KeyGenerator(object): """ Generates signing keys for messages. @@ -190,7 +234,9 @@ def _create_sponge(self, index): seed = self.seed.as_trits() # type: MutableSequence[int] for i in range(index): - # Increment each tryte unless/until we overflow. + # Treat ``seed`` like a really big number and add ``index``. + # Note that addition works a little bit differently in balanced + # ternary. for j in range(len(seed)): seed[j] += 1 @@ -225,7 +271,7 @@ def __init__(self, private_key, source_trytes): self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) self._iteration = -1 - self._source_chunks = list(source_trytes.iter_chunks(9)) # type: List[TryteString] + self._source_chunks = normalize(source_trytes) self._sponge = Curl() def __iter__(self): @@ -250,34 +296,83 @@ def __next__(self): key_trytes = next(self._key_chunks) # type: TryteString self._iteration += 1 - # If the key is long enough, loop back around to the start of the - # source chunks. - source_trits = ( + source_ints = ( + # If the key is long enough, loop back around to the start of the + # source chunks. self._source_chunks[self._iteration % len(self._source_chunks)] - .as_trits() ) - hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN - - signature = key_trytes.as_trits() + buffer = key_trytes.as_trits() # Build the signature, one hash at a time. - for i in range(hashes_per_fragment): + for i in range(key_trytes.count_chunks(Hash.LEN)): hash_start = i * HASH_LENGTH hash_end = hash_start + HASH_LENGTH - fragment = signature[hash_start:hash_end] + fragment = buffer[hash_start:hash_end] # type: MutableSequence[int] # Use value from the source trits to make the signature # deterministic. - for _ in range(13 - source_trits[i]): + for _ in range(13 - source_ints[i]): self._sponge.reset() self._sponge.absorb(fragment) self._sponge.squeeze(fragment) - signature[hash_start:hash_end] = fragment + buffer[hash_start:hash_end] = fragment - return TryteString.from_trits(signature) + return TryteString.from_trits(buffer) if PY2: next = __next__ + + +def validate_signature_fragments(fragments, source_trytes, public_key): + # type: (Sequence[TryteString], Hash, TryteString) -> bool + """ + Returns whether a sequence of signature fragments is valid. + + :param fragments: + Sequence of signature fragments (usually + :py:class:`iota.transaction.Fragment` instances). + + :param source_trytes: + Plaintext value used to generate the signature fragments (usually a + :py:class:`iota.transaction.BundleHash` instance). + + :param public_key: + The public key value used to verify the signature digest (usually a + :py:class:`iota.types.Address` instance). + """ + checksum = [0] * (HASH_LENGTH * len(fragments)) + source_chunks = normalize(source_trytes) + + for (i, fragment) in enumerate(fragments): # type: Tuple[int, TryteString] + outer_sponge = Curl() + + # If there are more than 3 iterations, we loop back + # around to the start of the source trytes. + source_ints = source_chunks[i % len(source_chunks)] + + buffer = [] + for (j, hash_trytes) in enumerate(fragment.iter_chunks(Hash.LEN)): # type: Tuple[int, TryteString] + buffer = hash_trytes.as_trits() # type: MutableSequence[int] + inner_sponge = Curl() + + # Note the sign flip compared to ``SignatureFragmentGenerator``. + for _ in range(13 + source_ints[j]): + inner_sponge.reset() + inner_sponge.absorb(buffer) + inner_sponge.squeeze(buffer) + + outer_sponge.absorb(buffer) + + outer_sponge.squeeze(buffer) + checksum[i*HASH_LENGTH:(i+1)*HASH_LENGTH] = buffer + + address = [0] * HASH_LENGTH # type: MutableSequence[int] + addy_sponge = Curl() + addy_sponge.absorb(checksum) + addy_sponge.squeeze(address) + + return address == public_key.as_trits() + diff --git a/iota/transaction.py b/iota/transaction.py index de5ffff..9326ed3 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -11,7 +11,8 @@ 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 +from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator, \ + validate_signature_fragments from iota.exceptions import with_context from iota.json import JsonSerializable @@ -644,12 +645,11 @@ def _create_validator(self): signature_fragments.append(next_txn.signature_message_fragment) if fragments_valid: - signature_valid = True - # signature_valid = validate_signature_fragments( - # fragments = signature_fragments, - # source_trytes = txn.bundle_hash, - # public_key = txn.address, - # ) + signature_valid = validate_signature_fragments( + fragments = signature_fragments, + source_trytes = txn.bundle_hash, + public_key = txn.address, + ) if not signature_valid: yield ( diff --git a/iota/types.py b/iota/types.py index 4d8423d..f6ad796 100644 --- a/iota/types.py +++ b/iota/types.py @@ -376,6 +376,17 @@ def __ne__(self, other): # type: (TrytesCompatible) -> bool return not (self == other) + def count_chunks(self, chunk_size): + # type: (int) -> int + """ + Returns the number of constant-size chunks the TryteString can be + divided into (rounded up). + + :param chunk_size: + Number of trytes per chunk. + """ + return len(self.iter_chunks(chunk_size)) + def iter_chunks(self, chunk_size): # type: (int) -> ChunkIterator """ @@ -436,7 +447,20 @@ def as_json_compatible(self): """ return self._trytes.decode('ascii') + def as_integers(self): + # type: () -> List[int] + """ + Converts the TryteString into a sequence of integers. + + Each integer is a value between -13 and 13. + """ + return [ + self._normalize(TrytesCodec.index[c]) + for c in self._trytes + ] + def as_trytes(self): + # type: () -> List[List[int]] """ Converts the TryteString into a sequence of trytes. @@ -448,11 +472,12 @@ def as_trytes(self): method should not be interpreted as an integer! """ return [ - self._tryte_from_int(TrytesCodec.index[c]) - for c in self._trytes + trits_from_int(n, pad=3) + for n in self.as_integers() ] def as_trits(self): + # type: () -> List[int] """ Converts the TryteString into a sequence of trit values. @@ -468,14 +493,8 @@ def as_trits(self): return list(chain.from_iterable(self.as_trytes())) @staticmethod - def _tryte_from_int(n): - """ - Converts an integer into a single tryte. - - This method is specialized for TryteStrings: - - The value must fit inside a single tryte. - - If the value is greater than 13, it will trigger an overflow. - """ + def _normalize(n): + # type: (int) -> int if n > 26: raise ValueError('{n} cannot be represented by a single tryte.'.format( n = n, @@ -483,10 +502,7 @@ def _tryte_from_int(n): # For values greater than 13, trigger an overflow. # E.g., 14 => -13, 15 => -12, etc. - if n > 13: - n -= 27 - - return trits_from_int(n, pad=3) + return (n - 27) if n > 13 else n class ChunkIterator(Iterator[TryteString]): diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index a963be8..2810e29 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -4,7 +4,7 @@ from unittest import TestCase -from iota import TryteString +from iota import Hash, TryteString from iota.crypto.signing import KeyGenerator, SignatureFragmentGenerator from iota.crypto.types import PrivateKey @@ -855,47 +855,50 @@ def test_single_fragment(self): b'HAXQWELNH' ), - source_trytes = TryteString(b'UDUX9OKZP') + source_trytes = Hash( + b'ZHAGCBCERH9FBGEFUDXGNGCAKCCCFEBHUCYGRFNB' + b'ZFFHEHSEEDDBGBDIMFEHYCGDBFPGVAIEH9A9VBNEJ' + ), ) self.assertEqual( next(generator), TryteString( - b'CGWXNMRWQRICZ9QULUMMHLTGIC9ILGBPBLX9OXQOAFDUPFOEQMQBZPVIVDWCBOFCN9' - b'KEYSWXIXTUFTJKTFRCEXTVBWZXCTXHCLXRWVFUEGYPBFWAASUQVGOHDYCWZAQVALEZ' - b'GQEGJJYDFSKSRGJAJ9NMMXQIEEUVYBUAELKYVEQARXTOEKCBH9VKKSYMOYBONNJNIL' - b'9KOU9RRIBIHGQJQFMGUONGWMGYJYLJFPUIZQATQBDTZLC9XRPUODTHNU9XFFC9EIGE' - b'VHHTSUQBGWWNSTACF9RTFDSYCVJARDKZYXBYZYNWETUTKDWGXAKFOEHUEHHEXOEVEA' - b'IOBEBORQDVEBZWNMGBWN9AP9WZSTA9EBCBYBCQYKSMGEXRZTLLPLKMG9AS9DESQFOZ' - b'9LF9SWXMENMELCPUSLDDYKG9GYT99LKYIFIWEGJMNXGWOGVVH9LEZCLQSRFZTAKRPR' - b'ZXIOPVBJBBPCBWWDNVVAESOMOOOIE9VTGB9BRNEKGUGSMOYOBRDLVAXCZKXPOSWALF' - b'MMKKVOWROOTSCC9LFOEUNSKMBMTIIPEKFZQOGCJKMJKMCULHLWOYFBCOCIIZCWTEVH' - b'PLJIVTMKO9AVDCJPYMTYLQCSKWTUGZAFMMJRBWUQNCUWGVCLMZHHTQZFZWWQILFJJU' - b'SAKHGNU9KOKVVOBOVGVCZOQMELMYXHCMJXRGDDDXTWC9ATJFAZENRSIVGHDZPCIHEW' - b'NX9MYPJIJEMXQEV9QW9ZFENKFWCNURXPSEAPBJHSNVKSPEYMRJIOKFIRPUFFNCHQWU' - b'JNOZFENUSBIYKOUFWIJNMLNFYEADOT9DXVIRGGBQJPCQXSEGOSQVFSBUYNXAURWQJL' - b'ERIG9GSBREQGEJTSLPNNUFMFKRLCJKVLFANPNKLS9XSRPPZNIYOBMKDKRQILMVWIMT' - b'BKB9FSCZJPHK9L9XNCCMNANBLANGJYAQJXKKBUSRGJXGRMQBBAQVEHVLK9ASZBIVWQ' - b'MYDXVDWPBZPHE9FTMMPEOGDSYMMTSZODGFKUYJQK9SYPXXAPYPEBTQYVDIQFMZJWRG' - b'KQRZQBAQCRDFKZSEGZVDYSPQIKSJWPBKLKIEIICCSAMVICGYSGSKE9KERRDDYCHJ9H' - b'GNTCCMYQHXBHDHEZPKVYGTJJOUWSHNQQCAKLQHLTTBLHPTMQWHUXXQFIAIYGYCLAEB' - b'NMCXLJVPIZOKHGEPVPDIAXNBEXBPF9XCNKFBVXXZYZMDYDQQORPKKIBJCRSQGMZWQU' - b'KZDHQPXPSVSQRDRMYXQQEFXQE9BPAVAVMJNSXMEINAKUJNPXTMMWKAOWQFPGTPYJUI' - b'BCMAZYBKNMMJMIKDWIN9HQFSCV9BCO9ZCJLAV9HHSMWSZWKESZUMQVALTFERWP9U9C' - b'SGBMBQWBDULGEGSYBVUERZKPBEBMVGZSZAGFTZODSSKBYGQNPPAANDNIRNXCOZPBFD' - b'DEONIKKXBNMUI9JFDRHJLYCZZTEPCPBZEOZMPYYSVOOUNDS9IEKNQMHO9TUOGJ9QBT' - b'9HJ9UGMCCZW9UMWJSWPHFRSOKA9PNPTDDRVAYIJDEZRM9XILJMB9QTFCWKQDZGBGWG' - b'KQCNZVEQLGYYDKSQUJFWC9KDJJWEPNBNYIRLXDIJRKFLNAHAUNKWXHCKBXVZTJKD9D' - b'KTLXJ9XFPLM9MZBYNFFDEOCNKAIGSDMORBXQZMKWWOIANNUQEKAGYVSFWXHNIBTSIT' - b'REGQACPJXCIXJIGXCUKBTOAWXEAUSSRERIH9MYHOSZDAFXZWSHXUJ9ELEZEROSIUSL' - b'TYVVNAXAAXQXPQGSPUV9EUSLPZQSQZGFTZHEMPLALLFKQSYCNNADAMSTJAWDXXSMSE' - b'AG9T99RF9LFHG9FEPUVQLBOUUFTVUXTDGAQQEMJWMZWFWHJHBTVONFREZUBZXNFITC' - b'YPKUV9TJJOTDQPRFO9HABDWYYWP9OVFPPKXMFOHDYOGGR9XEORCSZHQZSEDIMQZSNO' - b'Q9BC9IUHSWCLJYLOUTXGWIUH9HEHDRPYTI9KRRNLEYGPCPEDSLKDNPGJXQPKHVYGPT' - b'FFBVUVAEQEKEJJVAZCTCICKAPCN9RSMJSXBOKRXUZPZWFRULCPLANWMJNKEVRIKECA' - b'DFHLDDRHJMOFOXYPOQGSQXLMQAMYFPTTKVFYYWNHPP9CLOROZWBNVHCFGSCIGCPSOA' - b'YBFLZDHBO' + b'XJASWJN9LHHCZFUBPFXNWZFCPCLYQRLCMEATXQUEUCVDIRFOVGXXCGTAMRXNGCQZEB' + b'VQ9QUFGTBWRBYWCQFWUDNGNZLVRE9EQYTTTQAVZCSSNPTFIRWMNRHEXIUZTM9TTRFY' + b'GYQQBSQQQEAVFMKSAWLQVDPRGJ9NFBKRYAACGXBKDCPCEVEO9SCDJXDDKDWIJFVEPD' + b'EXLEAWLIRZHRJGIUXS99CEDNEVDB9EVEDFOP9PGTDUVZXRJVSLARUBRFHNRQQTXNSO' + b'WUSSZABBZGPDNVNK9UUQZNSKFWJAJZVUGKKUIW9JSRIEQHUFSIIGOBX9IUFNBNBKYG' + b'JXVAG9GZZXETMH9LCSHWEVCMPWIHTUOPQCFFJSXQWSNHVYT9GEPTSAECAITTXREMLR' + b'HMFBMCESO9YAIRGJOELUVNPMUMNJMAYGLNL9AWIZWCSIQEEQT9I9CBWURNICTELPYL' + b'RWFRGCIWBUGXFPVRVBOFSSDSQDBUHNPDOYRDHJKZEKRLBYCBQMCSIUNRPIBAYZSJNW' + b'LHOVZXSRAHKHTXEXZGTHQLKVZZQURRWPVYLXK9DYTDNXIEJBZSAICUPYNSFKPZWQOA' + b'PHQTXXPCNQGBYYZBEGHD9MJBLNVREJAFXCKWRXHSPSZPZVLMHEEGEPQ9JEYCKJRUSQ' + b'MVCDGQQVUCUWTOYYPOKWHGKPEWLUQCTRCKAOEEORR9WKKVFCAVMQLRZHHJDMYABBMK' + b'TTCTKZFLTLDBJ9AXDLCANUOBABM9YVOUSPCDFFKBUFPZDJWENLHTSNYJGNBFCOMI9Z' + b'AWPRUCGDHXGSUERBUXNLOSQDCQLUIBM9UCRBPNEB9WZGZKWBNNJYODUWORKO9ZWURY' + b'DXXHELMBRXKHXGIGLJV9MLEKCGCSFROTM9QJDKZPHGHXOLGZHUNXTQEUTGKIIQ9OSA' + b'ENUOSRYCFKMLOPAJLJIEFZDCHILJFEXQCRGRJLTOHLKDLPUKYHDJJCYXJHHCYIHVTU' + b'LQHEKEIHMQNCYDTKCHAAOOMPXGPJUTXNOAMUFVIABPDKWMSSFNEZKTQJIVZISPVZAE' + b'K9BPSVWNCWYPCQDNLFUWFYRFTZMVRPJJBSMWWERIPSGCNJJCOBIGWNHV9NVUIJXFVZ' + b'UKCD9WBVZDYGVGKXCDXUHGCDYMUNWLUPSUVXABHHDMACCNVGSJAHHSFJ9IOEPPURVZ' + b'NLDFOSNRASVRIWLINGEOZPXRMZMFMLWALBSMILWNHGRGYPAHSMPPMPZTMHMEJWHESL' + b'MU9NZTWRAATVGSJQMS99WYCXPURWOSSZISEJZKW9AJJAKNDAYBZOLUNAAZOUGSWQAH' + b'OYKBDGBXBWRMTEJXMOAVSSRVYBIHWCJZSTZ9EFWQZANHVIKSMBTQRNJOAYFNCGLIUL' + b'TWCDQBJVFKIBXSJOTCWMHRVQAOHFQFGFGPMDMIRHIGY9XYSHPGIIMT9SPDHDRDVSJV' + b'MLILZQWDJCRGNCRSGJWLUKNTTGIH9SNJGYDZAFQVMHTTRWKVZFIHPVIXDSTOANVTHB' + b'HDHOWE9RTE9SPJXVVPCJFEPZRQERUTY9PDADTKLXIFGKGRQFZTTTNPMNTPSJQYPPUP' + b'PJTWVJC9EJFFGIXMZGHTNKPGCZALIPTCRR9DWXZEBMZRFKHNYBBPOVISABQYMLLVSA' + b'PJKFDVCRHAMNMEQSWPWQYMXNPZRBDFUWOOLAIZLFQXGWNCQN9KGQHEIQPNFOKKBUJU' + b'WMXCQAXYYSHZNDOWWMGHBMEYAAPMBYXTHZTQASRGXDJFKGBSXGVWIXFBFBWIKBDENB' + b'THWHEHJHHALBSVAEGYFXVDRHFMPQPIWPGLXGOGFYEMZK9FN9MYSXYCMMUXMUSLKWZP' + b'9BNQKOJMONDFNZWIYQEJNCBSLGWSHYKLQCUK9ERBLQDSZ9XUTAXXIUCWHTHBBRBQRX' + b'QAWNRSHAKKJLBFNEDGWBA9AKPQYHTOZHVHRUNUXXMPPWQRXGPA9XFFTQFVPMXHWJMK' + b'TPHUHLWHCODHWTLEIOENJLUSD9URLOLCQOKYSDAGDOJDXIFLSUKGVWYKHVQN9UIFWK' + b'RHECRUMGJJYIPFMAAJXBBBBVGFXXRFOAFXEGHRFCSRENWNT9EXGKMA9WD9REIUZUJJ' + b'KAVBSMAEJRBGPKCHQTN9CPZBDKZYYYRNQCQZTUKDSRMZDZLABGSLJU9VBRTGVIZC9F' + b'BPEKYDOEB' ), ) @@ -980,93 +983,91 @@ def test_multiple_fragments(self): b'RLMWQ9RSJB9TPLSDEP' ), - # Just to be tricky, we will use the same source trytes for each - # iteration, just to make sure the generator isn't cheating. - # We should get a different result for each iteration. - source_trytes = TryteString(b'WGXG9AGGIWGXG9AGGI') + source_trytes = Hash( + b'DDSHZCQGA9XFGHPFY9NEG9AGO9HEJEZHPEZDK9FA' + b'BBUEI9PBJIBELHTBTCBAWGIAYCQ9ACLDTHFCVFWGI' + ), ) self.assertEqual( next(generator), TryteString( - b'GNCUHGZVIYRQQBXXBUONVL9COKOYDERJAWWI9YRWBVUJLDQQCBMFOORHRTVPKUDFWK' - b'YPOOCNVDQOCTJNVCFOENDWEGCXOXZK9BNFGAUTSLGPIWCIWFQAGQTVECGNXLFCUTFQ' - b'UJRUQKPJEDUFZTABRPTDXCIE99ZSGZFQCJYRZRSSUEENDBMDRZPLI9KCTVAYTFVRQB' - b'HDFLQZJBHIFFDVJPIJFCMOYNKOXQNQHSNDBWXRHLVBXBRLICTY9KJKWK9TZBSNIHKM' - b'ADAWFIK9YZVZFZWSZANYEIGQKOVRDGGTGFYFLGKESJKLUJMCKGGAJZHFQJXWIJ9HFI' + b'IIMLJOGFKELWFPWQZCMKWMUBCXYLMPETDI9CELEEBVVPSCRGXFLTMKWWD9SPYJGYXF' + b'OUCKCMGBGTIJUXGCFOENDWEGCXOXZK9BNFGAUTSLGPIWCIWFQAGQTVECGNXLFCUTFQ' + b'UJRUQKPJEDUFZTABRPTDXCIE99ZSGZCZYBGEGYJ9FGJJAUPJBQUA9KVSAWIQDES9OR' + b'TIEWZJPFAUOHCDRPTCPKLWXIQADMKWUMUQPLIAOUWHENILVOWHOJEDNTCYYSIL9SLJ' + b'QVRSFYQTHJ9FDISYRCKRPONEVSPRCEMCGNPK9LMANIRTOYJOVGLGENCWABUFIJ9HFI' b'URAXKZKTNULKABTUDTNVQEJRMVUXGOOWAUMSQCUBAOLEFO9PTVPCIFMFJGYAPNFIDW' - b'T9TASFHSPXTRVEBRNJOVJIPGMNEWHGZZTQRRHEG9CXBLUJJSCENYKRXKRFPHAOEY9G' - b'RNLWUCLXXOBQOSSOVEXZGHBMJKQ9YDQPRGSSNXOFZMUOGKWENWIVIVMDDLPACSKPRS' - b'99PNRADWBWSVTXIGGW9THJEQNWEGCWOVSLVWITMUWIUQGSGBYXNKSKUCLFEHWLOOID' - b'OKTIOJGPUZQBLHYUKCH9IKPJCRWXBNXGSHIZXYCSMOOQZROSDMNGEMHSKKDOYPKXAI' - b'DS9UUINN9RSVSDJUODJ99VEJMLLQRJFPYAZDKTNXPHZYBV9AERYOUJKBXACGZTWWGV' - b'NVIDNQPRK9HNEGOVOKQBXOXHZLHFNXDSFJECQVRJFJ9VHSCZACCHRKLWEGE9LAFEQY' - b'TFYAGUSYZGFXOIUVATAZCTDMOMVOVMWTDFXRNLHPSYMQKVGRRINYVOBVUXMPZPXBMM' - b'OFPNNGDCZHBPQBGMZJKOHOIKTXYZAAFKZEZPTESTUQH9HPNUQKUD9GJSSVDIJQGFRI' - b'FULCYSKXTASWYBMXSNJWFFJA9IQOBJYVZVYDXZYMWIYJLXIPGGKULHDBEXYCLQQFXA' - b'DJEEVJW9DFWDJQTS9AJWQQZLOYOSQW9DEHHENQVVOKFSBNHSJQHTPGIBLNTCXATNOH' - b'TIOQFDIQEZFWWHCUEGNAYAPMUGSNCZQIWGKCYAWZWGVLEZVSZIJKD9EHINYFUVZLPR' - b'NIVUNZBVUGMQAUKEDPQWZNKWAJZDVHIFLSQWBGVOXIVBZTYRDBFNXUFFNSSJHJLYJA' - b'9MYYLKRFEWMQNYJWYTDNIVNJFCEHBDE99SZTKKGZUQO9AMVGYCIUOCLECKEUBSWBNB' - b'9QYSWEUAVROPHIFQPAQUIIPGGTGYSWDFPYYCRFBHBHAUDUX9OKZPIZEOFEIMVHVAKR' - b'QJ9HXVC99RNOEOAZVPPIJCWGKCAJOOWRFXSWLAPBNIQGNNKXSBLMNFJCUBYDMPKS9V' + b'T9TASFHSPX9ZHCMDPXSINJTMHGQNVOBBYCCI9CNGWRNRDZTPSWNJFRCIFQP9DGPAAK' + b'XGIRMUJKYOJMXJZGDIXYHAIMQQWTUMBJMOPE9YJYGFQBBGHSGKADRELYQPCBALYVIN' + b'CXXKMLZZFYYJYXHKMTKLQMVIKF9WXXWMYZSOE9HJANLNVQVAHQKEUKOPNNEQHRSREA' + b'FOWSEKTWOAA9ACCUUYGZZXXLDOCEVQFFOXQAVFTMBLSUVYOOYBIGC9AFTSBBMWZKNL' + b'U9WUTHBFDUUJJGAUT9VKGGCDNMUQTTZSHGEUJBX9SMBIG9XFSSDZOLYHLH9IAHZLQK' + b'E9IKUSLYCDZMDPVEVDUUEEFHXCGZHTEDIO9GHQWIXKGDTSBQWASTQGHWKUHJVUSVDA' + b'GZUYIOJBMSFFWFEMMSCZSVEVHUHPYF9JWGSDKTNORRFEGYJTDJFRYHLUFFHMYIURYE' + b'FJXPXP9BTHGPGI9LSWDYZBNNPP9PRFMNGHFOEDXAGXCNTNUDQCCGKHIWGICNJETSUZ' + b'TRDMEAOSGQHFVQERRRCGGRICYFAZUEHE9DWFICGESVXYTGFPESVPFMZQFBJ9CCOPCS' + b'BQBTFFNKWTPHDDCUDSSUWYEQTDOXYXOYM9CBEIONNL9LXNLPTSTUGRBCWZVLSOYOIX' + b'YJYSFSFHBYQXSMIJUCNDUMHLBVUFOXALSNFRFRJAZHOZ9HJESVBHIQGCNDLFVWOMTQ' + b'MLCORHIZOJAMY9RMHOKKXFNGVFNFAGVKPIPTNGCQZPCOPBLZ9VGKHKSHOPYRVDECWG' + b'DGVJWDEUKOXLUR9XZKCPTXBKSNIQVLUCBVZQYCRSRDHFFZVXCRTXLDKASADFICFV9C' + b'ZEUJBXRBJRJCYIQAPRBPOJXDUGOANQGAOYGFB9SMHZMTYFYZBBQDKGTLYBKYZCXCDS' + b'XZOWPRGJLFFNFWCFKCBCLMKJD9SUZFMDIRGRDGYTMLUTQQHUIWGWPEBCKBYDMPKS9V' b'ASYWPE9WPMZLKSMOFQBPULYKIVBTHTUUNGGRPOOMSSSNUCP9ASWMGONSIANVRPEYLF' - b'OPJNSBCXKQSVPECWUWEHIOSVRAKAUTDCPGGMPQXDJFQAAFJQTIDUUFGDMGVZDJCNOS' - b'YOEAFJSKXUPHNGDJH9TSKJZGPUETDIFCUQUVARHETQFSRLSBCJCLBR9BJNDTNTWH9P' - b'HU99TPAMMYYWJSISCKTBGMROVKWVZTOOIUTOERQKHGPTOZAWKKRGRYLZTJPPUURSMF' - b'H9H9EMWIWKJYMWBLNQPFHBUGFKDJHEHORXXDDSFXAAQBSKIORGQNHZYRSUSRNLIBID' - b'AVDPZKOLLPOWTCPPUJFTOLMGSPASTNIXBHIJJSBRMREVBCZIPRVBQVPQ9AYYPOVK9L' - b'BOQQCGIXAYOXYIC9SYFMGAGZJOIHUZCATPJJOHVAZXLPUVLDJKBXQNPRTWHKH9ZHGC' - b'ILKVHYOXNVVWQFMXYDKEAQMXZLNZTFKWRTBTCZLTRCFVQSYG9ZSSZTQJQSHMBOMIQS' - b'KNWRG9MEUHWPGAJPJBDKPQOCXDYAIYMUKBOCMTVVZXLPIYQ9XZOJWMICKLECKCRJUX' - b'LEYRZCIQNFOIHKJPHTKFTS9YQMBXLCWZBZOWPEWMHLDEHBADWNCSGUYYYNGDGPMJGK' - b'MUQ9QCPVPJIOQAZOXMIP9POREGZIDEXJOWXHYTOILXMQBXRGBBNAOIJLRANGZGZSOM' - b'XQUEEUWXS9HSNOTUATDNMNIOLERHGBLZN9REGCRXIMGZWTHIGGL9VFGZZFURDCLVCT' - b'VKMUTYC9C' + b'OPJNSBXW9ZMTDGRJRIWRNNSXQXSR9TPAOLL9PUSCFYPMFYKZRCLCQL9UUQPTOWEZHX' + b'ABZETWGPJEV9WLZIOVBJTVSOPGX9ONJVKLFFNZDKKSWSFATOGDWMATGVC9HNYNMKEX' + b'RASPVWGLDQSHKRJGVGB99LCTNGATKUCWCHQNCYRZAX9KBBQZEBXCMEOZKSUXCGVQBL' + b'HPYXYZOSOOPATETTKYP9KRSEZY99GYHAQYTGLKYMGNQWTYJJ9CVNFVQWMXDZCLHBMT' + b'LUGNNFYFZATBGOSJJ9TWFHGRXYFRSCZAFLIDXPHZMOQHCPEDUDOLWIZXMKNGVEOHRZ' + b'NZ9AUUMUGNYASBVKZMEUWQBADUZDOFRKARFQTXYPJWCHWETNWET9JNYRHMZAVGPBMN' + b'SDKVQ9NKPTSGX9NFRERSOAUIPYLPMSATQKYGCZ9PNRNOEUMZFTHTLMIGIEMRSEBKGF' + b'EXEBIAQPPEBQCVNSGOKCJSPOOP9RUQZHYHTZJH9ZJSEHZKZBNBRKAIFTJBOZPUTACH' + b'XZHRIVIN9DZLZJDUAGOPWZDYJAWBKOKAVRPBIQWSTBIPCBADWNCSGUYYYNGDGPMJGK' + b'MUQ9QCPVPJIOQAZOXMIP9POREGZIDEXJOWXHYTOILXMQBXRGBBNAOIJLRANGPHFWKI' + b'UFYPFUBYQLTBBWCNOQWETHFHAYNJYGIOCSNYWN9OVEPQARCFRZISNSQNNGXZ9WBEZU' + b'WSCVNWKOR' ), ) self.assertEqual( next(generator), - # The generator used the second fragment of the private key this - # time, so we get a different result. TryteString( - b'QFBNTRFTTBHNIPZQNQPXOFQWQCP9KMMMVEPLHYQMVMCWSVSWS9HEEJHNRGKELXEEKI' - b'DBREGBJTIKHBMODRBXEOKYOXTPPYGVSROOMLBHQKFALHNUYEVUAMEYPNRLFFNCDGBF' - b'YZQRLGLVQEPCYGP9HRUWRVIRZYFTFJNEJKCLUMFDXULSOWKVSEMVFXHAHLXBZITIHR' - b'BEDHBAOG9YYKCQDMZZJRDEYTTGVCDPMYUBEAIQUUV9KWPJBNJFFVXTKFJOWXD9RRJF' - b'HBPDMVVQMDTVCRTNFNHVCOLHOLXHIWKJW9ICHUBHCSMDKHVXGEQKFRI9DROBXBPWYV' - b'PDUOEOKOKSYEHKQSDLQIPT9JLKRSZIAGJPBTAMQOADXCZDRFMLTUB9UDYQXCIIYDDX' - b'OMIKLEJKZU9KGJHQLYAWFPPLRADBQDOAEZNENKTPJUQFEWPERZWJNYVSSVNQOBWZAT' - b'WMZOVPPRKBAISZLIDVBZECDULBGKAINVFPAI9QGAGMSQFMSPSNFSKXKNCNRHUMSQPA' - b'CPAPMNSDXAPHQXLFJZKLFGLWNZKUSACXTYKAPKLCGDMRIVGGZSAGLXSYDAKXVEHVPV' - b'VVLGUNACNOVJGOFGZTNILHJTRGSHCLBPJPPRYZMRLKC9EWINJRREXTOIIPJ9MQKHNM' - b'SKCNGLILLJZFZKXSLSLCBBCYSFIJPFPTCB9ALKC9VAZHG9URISFCWTVVUJSMDRZVKP' - b'9AIZJTEUPFEXFHCFLDL9NEVZH9QDC9WXDBXXAWIHQWIZQVXIHL9C9LMGCAGPJZKESY' - b'KADPDLYUHAFQVUJEDFQOSATEZTPIVRQTEMCHTJEODWAJHH9QQAFAUMISXHOLXRWOIZ' - b'GLSDWIOUYZA9SQWFUKEUGGUQUYWXYVKPINSCEPQXQETIOHXLQPHSPNHRBYPOQMWHQA' - b'PXAHZMAPPGCYZIHFQLOIYVOYKRXKRCPYHZMITNYQHVELAUUOELPWGVQNLPIDYNSCMN' - b'QWPSSOGIRHJMEONAIU9ZYRK9HWCKGINSEYVEKLBWCTRUZJFAVBLIWASYZYJFONCRVE' - b'GIAAJFFFOXTRLITRSJSSFGPNJPKNEBJZLQKIV9QPDSMZQIAVZJSKQMBAYIUCKTJMWP' - b'UJULPOCPMMNJ9OYSPPSYRQSRDLVNDDXLWYDEXDGXJOOLXYKBJWBOWSTQVBVHR9OXCA' - b'OOKIW9JCSKHGGSESTAUQIRCIMUPJAMMOEEQYHCHOSTKFVAHYNZ9NJSVWZHGWBRPNQK' - b'ZTHSV9MPBSZHEWCXFXUZLJFOPTPGTXUWPPXXJQMCPOAI9CYTNYCLS9O9CFLXPVCFID' - b'TSL9YT9SNSROTNSYCTKTICYZYAJTTPI9YILQSXZQXPTRTPOGALQMHZIRHPRTRXLT99' - b'9ICOCEUZDXBENFISVLBMUYMVQHQMPLEDCOSAFPVLLORRVJJPLBCZDSKQJXHKXHVVOE' - b'DG9OSKILVSWRHZL9WDKFR9HDEUTDTZQDPEWQQMXCRQSAXPNHHYDWGXHJCPEKJF9SAA' - b'FLENDPJFHKSYBDZKZXXBKUKKAEBBYWKEYSSURNWTQRAYJ9TZ9FFXNA9LXNZAQVCRWQ' - b'KNNAYGPIKUTCJZAMCMJNTRBIQNBMJVTPVMOTTNSGYABZECIOBBIDCKLRSJDESXGVNY' - b'HACSAVTMFICFHLBEBGDMWKEBEICRUFYGMXWXJJ9MHMCRLTAOM9GPOYDNWLYLOQCVKR' - b'HXZFRZKBRFCF9LOGAIGQLCZR9OIRL9HYSAMMJQEXJZMMKMCGGTYOKXVXFBDXDQOQKK' - b'IZG9TMO9CAPGRQSUWE9WKHAIERQRJRZFKS9AHYNNGDNVLXPLLARQHWCFZKATLJNEGG' - b'JAYVDGKSVQDYCIUEELIQCSVTCCFWVTYOMYIHSMN9JMTFSUBOQGCPBZYAFMCQMMQUTN' - b'OOPVKCGRVOEJMIPZKSOLGNBXHTKLWHMTOZRCKHRAIBOAPPSMCLBGXWHBYSTYPGXETK' - b'FHALEOMABXFV9MLUMZBVOFRI9UASHROBDN9EFCTUFZFYLBRUOVZWJENH9QHUJTX9EL' - b'NBKYYTDZSEHJCPORZDIHYABRLZRQZPECYFFNRVCRSNENPFCRJWZPMAQKYUXVPDFFVX' - b'EHIPLPZSFDJCALFZNPAKSLPD9QEBZIZNCAMKBZQFWRCFSVIEBDDSVMQIYDCWLQQGPC' - b'HAXQWELNH' + b'QMPEPOWGTOPFHJYWAGNCFUJOXQMQPKCFI9XHFINAX9QDRPTYPFXXQKMDDBWGFOLPJE' + b'KGLLDWNJJNIPYHXFRCEXTVBWZXCTXHCLXRWVFUEGYPBFWAASUQVGOHDYCWZAQVALEZ' + b'GQEGJJYDFSKSRGJAJ9NMMXQIEEUVYBATTYJIKZIG9QZFARFCMKKVZ99GRYAAGGJONT' + b'KUMPSUDCKRTBQZP9EAGGYBTQFSDMHXZVPANPKJVWTEXHMJJDDBNTWJDXVIPTSIZI9L' + b'HJMIAEWOQVWBPOCFGLCAIVUZVKJSRMGZ9FLSABTTQDHZ9YRFWGCLINRCBSXVPGRWZM' + b'UWDQAVFKAFBYPZSIJQPELNICVUJBKJQZCZAHNMNXTNEZTLSZA9QGHXUTOLCMRBWKGK' + b'ABEWKMPEKCBGWFGIUWEIZQOTUNBGIMKXBUUBRNFOYVNJZKCDLABAEQDARGAIXCVQGI' + b'CBCGEKO9CRPMHXWYLTJRTCNLCAKQWALTTFJKDFEOPBVXMJOSUQULYX9ZVCKTHERGEP' + b'JNHHIUCVIZCTYSLXHOXMCPPNUKEYVDECBXUKVVJCGDMRIVGGZSAGLXSYDAKXVEHVPV' + b'VVLGUNACNOVJGOFGZTNILHJTRGSHCLBPJPPRYZMRLKC9EWINJRREXTRZHHGLNNS99F' + b'PMRIBJWNQJOAWBLZLPQMUGPPMQEZUUCKTTDENOORZIWRNVGDBFXOCUSL9YGNZDQKYI' + b'YNUIE9SUBKYPDWVDHXXJQKZSHPPCEYJVITNPCHICTSVIBZXJPP9IRQWHOJUBJOABAS' + b'SHZRGIMVUPHZXRBQYNLNLVZACFHUXFTGQVJSVRMA9FEMQJDZKCTCSDAMNXHYV9CSHF' + b'YMGTCZSMNGMWMCPNIDBFKRCGIEPECPINVVZCOHPXIEHEJAIHFKUBZEID9SCGWAMNFV' + b'QZZEBRNMCRGDEJNQWTAZZNXCARRNCRHXWHTGNOOQQNGQQPEBMBSMKGWSUHEPSERDQH' + b'KVETGCWXOAIEIIIMU9GLYYNLROOTTLHMVWXZKJFSNEQAXMWJNSSUBNUZAHCMMFPZV9' + b'IDAEJORNDLLJEWZRTUCVHJYWJZZZZSVWZAMKJQFAV9UUPP9RFPKBQIEQGALVUWHWKN' + b'EVXHMNOMKMMWWZLDJOWY9PZLZZE9FOUQYGGQMUDOSZGF9FVFDOVFDVLAHBSYWKNHVG' + b'WKJECMDTSPYQ99QNZSODIFAOZB9FMLWALBSMILWNHGRGYPAHSMPPMPZTMHMEJWHESL' + b'MU9NZTWRAATVGSJQMS99WYCXPURWOSSZISEJZKW9AJGQRYHBXRUJLGDGKJUOJAR9KH' + b'KSGASZNKVOKEPOKPUCKITRTZFPYIZCBACIFYK9K9C9P9HTGSKGNMMDRCINSJVCOJET' + b'VLARADRJAOICANTEJOZAFV9VMWMBNBNPKXDUPJELBV9DDONZHLQYEDADLOAMICIUNB' + b'YJHQAAUBBUUYEXITTBIEDZFKLRNGVATFGLDRSRMSXXSBU9WOQSLTDQMIPZQYPAKRJB' + b'XANOTMDYR9DTDYZPUZH9NNY9SKHHXQKEOHQEOLACAAC9SRMOMNKTFVL9UVGIO9KQ9L' + b'Z9XEWQGDNHPRSVHHCULTAKJSLSXXIXCKSRYYNKELSWMACWRJXVZCNYCLIM9MUFXEEW' + b'MLTHNXHZWMIPVDRCIPRXDDKDNVSOELISCCSHCGDFEEBTRTOMMDRKACRKHLADGXNRFC' + b'AOYVACYGTYBFMEKLRNFSXAOYSTXISSNUM9XPCHQDR9KRNROSNGRUEVZRROZOLSRAWI' + b'VMOTPEDECEPJUDXAWETHKOMJIOXEZZYVHBDNYVDI9CLRXZ9HRUYGIXBEFVZJLQWC9R' + b'ZWDIEPVDJXQWCPZSTAGWSWOSCMKLMNUBZNPZH9BMRXRWMZ9ZLVTOIDQMPGGCQGICFH' + b'RYHTJVX9XXTMZLLV9LNDQLFIXSRXFAJVDIHCINQHVCQEYCIEWB9IHHZTWRN9YLEIHG' + b'XKIICRTABEMSLCXNOCZKLBQPHFWLJWP9TFIAGZIWEGDJLEPYSGHNTOFJWRWIHGKMZR' + b'FQENAASELQKWNLJYPGPKBKUHDS9ZBWVMQWBNMBULNGYMUSEBAAHYEBFCSIUAIVWYGU' + b'D9TRNFSB9YBYCF9CLDRGODKVTJVIETTQHLVOJEP9YKJIM9LILRKCFXMQNSFKCF9YOR' + b'YSYSZFD9I' ), ) diff --git a/test/transaction_test.py b/test/transaction_test.py index 99ca471..ee27ce4 100644 --- a/test/transaction_test.py +++ b/test/transaction_test.py @@ -472,21 +472,19 @@ def test_fail_signature_invalid(self): """ One of the input signatures fails validation. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') - # self.bundle[2].signature_message_fragment[:-1] = b'9' - # - # validator = BundleValidator(self.bundle) - # - # self.assertFalse(validator.is_valid()) - # - # self.assertListEqual( - # validator.errors, - # - # [ - # 'Transaction 1 has invalid signature (using 2 fragments).', - # ], - # ) + self.bundle[2].signature_message_fragment[:-1] = b'9' + + validator = BundleValidator(self.bundle) + + self.assertFalse(validator.is_valid()) + + self.assertListEqual( + validator.errors, + + [ + 'Transaction 1 has invalid signature (using 2 fragments).', + ], + ) def test_fail_multiple_errors(self): """ From 7dfd3977395974b12d8611e48d212913ef78014b Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 17:08:21 -0500 Subject: [PATCH 233/239] Minor code cleanup. Renamed ambiguous/confusing symbols, fixed incorrect/unclear documentation. --- iota/crypto/signing.py | 59 +++++++++++++++++-------------------- iota/transaction.py | 8 ++--- test/crypto/signing_test.py | 4 +-- 3 files changed, 33 insertions(+), 38 deletions(-) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index 979bbda..c4c6247 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -265,14 +265,14 @@ class SignatureFragmentGenerator(Iterator[TryteString]): Each instance can generate 1 signature per fragment in the private key. """ - def __init__(self, private_key, source_trytes): + def __init__(self, private_key, hash_): # type: (PrivateKey, TryteString) -> None super(SignatureFragmentGenerator, self).__init__() - self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) - self._iteration = -1 - self._source_chunks = normalize(source_trytes) - self._sponge = Curl() + self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) + self._iteration = -1 + self._normalized_hash = normalize(hash_) + self._sponge = Curl() def __iter__(self): # type: () -> SignatureFragmentGenerator @@ -296,37 +296,33 @@ def __next__(self): key_trytes = next(self._key_chunks) # type: TryteString self._iteration += 1 - source_ints = ( - # If the key is long enough, loop back around to the start of the - # source chunks. - self._source_chunks[self._iteration % len(self._source_chunks)] - ) + # If the key is long enough, loop back around to the start. + normalized_chunk =\ + self._normalized_hash[self._iteration % len(self._normalized_hash)] - buffer = key_trytes.as_trits() + signature_fragment = key_trytes.as_trits() # Build the signature, one hash at a time. for i in range(key_trytes.count_chunks(Hash.LEN)): hash_start = i * HASH_LENGTH hash_end = hash_start + HASH_LENGTH - fragment = buffer[hash_start:hash_end] # type: MutableSequence[int] + buffer = signature_fragment[hash_start:hash_end] # type: MutableSequence[int] - # Use value from the source trits to make the signature - # deterministic. - for _ in range(13 - source_ints[i]): + for _ in range(13 - normalized_chunk[i]): self._sponge.reset() - self._sponge.absorb(fragment) - self._sponge.squeeze(fragment) + self._sponge.absorb(buffer) + self._sponge.squeeze(buffer) - buffer[hash_start:hash_end] = fragment + signature_fragment[hash_start:hash_end] = buffer - return TryteString.from_trits(buffer) + return TryteString.from_trits(signature_fragment) if PY2: next = __next__ -def validate_signature_fragments(fragments, source_trytes, public_key): +def validate_signature_fragments(fragments, hash_, public_key): # type: (Sequence[TryteString], Hash, TryteString) -> bool """ Returns whether a sequence of signature fragments is valid. @@ -335,23 +331,23 @@ def validate_signature_fragments(fragments, source_trytes, public_key): Sequence of signature fragments (usually :py:class:`iota.transaction.Fragment` instances). - :param source_trytes: - Plaintext value used to generate the signature fragments (usually a + :param hash_: + Hash used to generate the signature fragments (usually a :py:class:`iota.transaction.BundleHash` instance). :param public_key: The public key value used to verify the signature digest (usually a :py:class:`iota.types.Address` instance). """ - checksum = [0] * (HASH_LENGTH * len(fragments)) - source_chunks = normalize(source_trytes) + checksum = [0] * (HASH_LENGTH * len(fragments)) + normalized_hash = normalize(hash_) for (i, fragment) in enumerate(fragments): # type: Tuple[int, TryteString] outer_sponge = Curl() - # If there are more than 3 iterations, we loop back - # around to the start of the source trytes. - source_ints = source_chunks[i % len(source_chunks)] + # If there are more than 3 iterations, loop back around to the + # start. + normalized_chunk = normalized_hash[i % len(normalized_hash)] buffer = [] for (j, hash_trytes) in enumerate(fragment.iter_chunks(Hash.LEN)): # type: Tuple[int, TryteString] @@ -359,7 +355,7 @@ def validate_signature_fragments(fragments, source_trytes, public_key): inner_sponge = Curl() # Note the sign flip compared to ``SignatureFragmentGenerator``. - for _ in range(13 + source_ints[j]): + for _ in range(13 + normalized_chunk[j]): inner_sponge.reset() inner_sponge.absorb(buffer) inner_sponge.squeeze(buffer) @@ -369,10 +365,9 @@ def validate_signature_fragments(fragments, source_trytes, public_key): outer_sponge.squeeze(buffer) checksum[i*HASH_LENGTH:(i+1)*HASH_LENGTH] = buffer - address = [0] * HASH_LENGTH # type: MutableSequence[int] + actual_public_key = [0] * HASH_LENGTH # type: MutableSequence[int] addy_sponge = Curl() addy_sponge.absorb(checksum) - addy_sponge.squeeze(address) - - return address == public_key.as_trits() + addy_sponge.squeeze(actual_public_key) + return actual_public_key == public_key.as_trits() diff --git a/iota/transaction.py b/iota/transaction.py index 9326ed3..5e2e920 100644 --- a/iota/transaction.py +++ b/iota/transaction.py @@ -646,9 +646,9 @@ def _create_validator(self): if fragments_valid: signature_valid = validate_signature_fragments( - fragments = signature_fragments, - source_trytes = txn.bundle_hash, - public_key = txn.address, + fragments = signature_fragments, + hash_ = txn.bundle_hash, + public_key = txn.address, ) if not signature_valid: @@ -985,5 +985,5 @@ def _create_signature_fragment_generator(key_generator, txn): iterations = AddressGenerator.DIGEST_ITERATIONS )[0], - source_trytes = txn.bundle_hash, + hash_= txn.bundle_hash, ) diff --git a/test/crypto/signing_test.py b/test/crypto/signing_test.py index 2810e29..c20d6b8 100644 --- a/test/crypto/signing_test.py +++ b/test/crypto/signing_test.py @@ -855,7 +855,7 @@ def test_single_fragment(self): b'HAXQWELNH' ), - source_trytes = Hash( + hash_ = Hash( b'ZHAGCBCERH9FBGEFUDXGNGCAKCCCFEBHUCYGRFNB' b'ZFFHEHSEEDDBGBDIMFEHYCGDBFPGVAIEH9A9VBNEJ' ), @@ -983,7 +983,7 @@ def test_multiple_fragments(self): b'RLMWQ9RSJB9TPLSDEP' ), - source_trytes = Hash( + hash_ = Hash( b'DDSHZCQGA9XFGHPFY9NEG9AGO9HEJEZHPEZDK9FA' b'BBUEI9PBJIBELHTBTCBAWGIAYCQ9ACLDTHFCVFWGI' ), From ea72a7fd6509c96764a86bab15c44db7b818611d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 18:30:31 -0500 Subject: [PATCH 234/239] Stubbed-out unit tests for `getTransfers`. --- iota/commands/extended/get_transfers.py | 22 ++++++------ test/commands/extended/get_transfers_test.py | 38 ++++++++++++++++++-- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index 681bc50..3db9e2f 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -36,13 +36,10 @@ def get_response_filter(self): pass def _execute(self, request): - # Optional parameters. - end = request.get('end') # type: Optional[int] - inclusion_states = request.get('inclusion_states', False) # type: bool - - # Required parameters. - start = request['start'] # type: int - seed = request['seed'] # type: Seed + end = request['end'] # type: Optional[int] + inclusion_states = request['inclusion_states'] # type: bool + seed = request['seed'] # type: Seed + start = request['start'] # type: int generator = AddressGenerator(seed) ft_command = FindTransactionsCommand(self.adapter) @@ -100,7 +97,8 @@ def _execute(self, request): # Find the bundles for each transaction. for txn in transactions: - txn_bundles = GetBundlesCommand(self.adapter)(transactions=txn.hash) # type: List[Bundle] + gb_response = GetBundlesCommand(self.adapter)(transactions=txn.hash) + txn_bundles = gb_response['bundles'] # type: List[Bundle] if inclusion_states: for bundle in txn_bundles: @@ -150,14 +148,14 @@ class GetTransfersRequestFilter(RequestFilter): def __init__(self): super(GetTransfersRequestFilter, self).__init__( { - # These arguments are optional. + # Required parameters. + 'seed': f.Required | Trytes(result_type=Seed), + + # Optional parameters. 'end': f.Type(int) | f.Min(0), 'start': f.Type(int) | f.Min(0) | f.Optional(0), 'inclusion_states': f.Type(bool) | f.Optional(False), - - # These arguments are required. - 'seed': f.Required | Trytes(result_type=Seed), }, allow_missing_keys = { diff --git a/test/commands/extended/get_transfers_test.py b/test/commands/extended/get_transfers_test.py index acb1453..549bea2 100644 --- a/test/commands/extended/get_transfers_test.py +++ b/test/commands/extended/get_transfers_test.py @@ -6,7 +6,7 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Iota +from iota import Address, Iota from iota.commands.extended.get_transfers import GetTransfersCommand, \ GetTransfersRequestFilter from iota.crypto.types import Seed @@ -314,6 +314,7 @@ def test_fail_inclusion_states_wrong_type(self): ) +# noinspection SpellCheckingInspection class GetTransfersCommandTestCase(TestCase): def setUp(self): super(GetTransfersCommandTestCase, self).setUp() @@ -330,4 +331,37 @@ def test_wireup(self): GetTransfersCommand, ) - # :todo: Unit tests. + def test_full_scan(self): + """ + Scanning the Tangle for all transfers. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_start(self): + """ + Scanning the Tangle for all transfers, with start index. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_end(self): + """ + Scanning the Tangle for all transfers, with end index. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_start_and_end(self): + """ + Scanning the Tangle for all transfers, with start and end indices. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') + + def test_get_inclusion_states(self): + """ + Fetching inclusion states with transactions. + """ + # :todo: Implement test. + self.skipTest('Not implemented yet.') From 0ed3711a6762b1a195d4dde0a2e1601a5e1f849d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 18:31:35 -0500 Subject: [PATCH 235/239] Cleanup up imports. --- test/commands/extended/get_transfers_test.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/commands/extended/get_transfers_test.py b/test/commands/extended/get_transfers_test.py index 549bea2..b1995fa 100644 --- a/test/commands/extended/get_transfers_test.py +++ b/test/commands/extended/get_transfers_test.py @@ -6,12 +6,13 @@ import filters as f from filters.test import BaseFilterTestCase -from iota import Address, Iota +from six import binary_type, text_type + +from iota import Iota from iota.commands.extended.get_transfers import GetTransfersCommand, \ GetTransfersRequestFilter from iota.crypto.types import Seed from iota.filters import Trytes -from six import binary_type, text_type from test import MockAdapter From 6909515761f84fe27defde9dac2a601032e3d3cb Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 18:33:49 -0500 Subject: [PATCH 236/239] Fixed an issue when calling `getTransfers`. --- iota/commands/extended/get_transfers.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index 3db9e2f..636a26e 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -58,6 +58,9 @@ def _execute(self, request): hashes += ft_response['hashes'] else: break + + # Reset the command so that we can call it again. + ft_command.reset() else: ft_response =\ ft_command(addresses=generator.get_addresses(start, end - start)) From 69ac8bb784d9803897d1471466f608c94e69f3e3 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 19:09:02 -0500 Subject: [PATCH 237/239] Fixed some issues with `getTransfers`. --- iota/api.py | 9 +- iota/commands/extended/get_transfers.py | 20 ++-- iota/types.py | 4 + test/__init__.py | 17 ++- test/commands/extended/get_transfers_test.py | 103 ++++++++++++++++++- 5 files changed, 139 insertions(+), 14 deletions(-) diff --git a/iota/api.py b/iota/api.py index eab7faf..db2b540 100644 --- a/iota/api.py +++ b/iota/api.py @@ -489,7 +489,7 @@ def get_new_addresses(self, index=None, count=1): return self.getNewAddresses(seed=self.seed, index=index, count=count) def get_transfers(self, start=0, end=None, inclusion_states=False): - # type: (int, Optional[int], bool) -> List[Bundle] + # type: (int, Optional[int], bool) -> dict """ Returns all transfers associated with the seed. @@ -512,7 +512,12 @@ def get_transfers(self, start=0, end=None, inclusion_states=False): disabled by default. :return: - List of bundles. + Dict containing the following values:: + + { + 'bundles': List[Bundle] + Matching bundles, sorted by tail transaction timestamp. + } References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index 636a26e..5ca63a3 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -71,7 +71,11 @@ def _execute(self, request): tails = set() non_tails = set() - transactions = self._find_transactions(hashes=hashes) + gt_response = GetTrytesCommand(self.adapter)(hashes=hashes) + transactions = list(map( + Transaction.from_tryte_string, + gt_response['trytes'], + )) for txn in transactions: if txn.is_tail: @@ -100,7 +104,7 @@ def _execute(self, request): # Find the bundles for each transaction. for txn in transactions: - gb_response = GetBundlesCommand(self.adapter)(transactions=txn.hash) + gb_response = GetBundlesCommand(self.adapter)(transaction=txn.hash) txn_bundles = gb_response['bundles'] # type: List[Bundle] if inclusion_states: @@ -109,11 +113,13 @@ def _execute(self, request): all_bundles.extend(txn_bundles) - # Sort bundles by tail transaction timestamp. - return list(sorted( - all_bundles, - key = lambda bundle_: bundle_.tail_transaction.timestamp, - )) + return { + # Sort bundles by tail transaction timestamp. + 'bundles': list(sorted( + all_bundles, + key = lambda bundle_: bundle_.tail_transaction.timestamp, + )), + } def _find_transactions(self, **kwargs): diff --git a/iota/types.py b/iota/types.py index f6ad796..91cade0 100644 --- a/iota/types.py +++ b/iota/types.py @@ -235,6 +235,10 @@ def __init__(self, trytes, pad=None): self._trytes = trytes # type: bytearray + def __hash__(self): + # type: () -> int + return hash(binary_type(self._trytes)) + def __repr__(self): # type: () -> Text return '{cls}({trytes!r})'.format( diff --git a/test/__init__.py b/test/__init__.py index dd71723..74570e8 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -61,15 +61,28 @@ def send_request(self, payload, **kwargs): try: response = self.responses[command].pop(0) - except (KeyError, IndexError): + except KeyError: raise with_context( exc = BadApiResponse( - 'Unknown request {command!r} (expected one of: {seeds!r}).'.format( + '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, }, diff --git a/test/commands/extended/get_transfers_test.py b/test/commands/extended/get_transfers_test.py index b1995fa..bb8c02e 100644 --- a/test/commands/extended/get_transfers_test.py +++ b/test/commands/extended/get_transfers_test.py @@ -6,9 +6,10 @@ import filters as f from filters.test import BaseFilterTestCase +from mock import Mock, patch from six import binary_type, text_type -from iota import Iota +from iota import Address, Iota, Bundle, Tag, Transaction from iota.commands.extended.get_transfers import GetTransfersCommand, \ GetTransfersRequestFilter from iota.crypto.types import Seed @@ -336,8 +337,104 @@ def test_full_scan(self): """ Scanning the Tangle for all transfers. """ - # :todo: Implement test. - self.skipTest('Not implemented yet.') + addy1 =\ + Address( + b'TESTVALUEONE9DONTUSEINPRODUCTION99999YDZ' + b'E9TAFAJGJA9CECKDAEPHBICDR9LHFCOFRBQDHC9IG' + ) + + addy2 =\ + Address( + b'TESTVALUETWO9DONTUSEINPRODUCTION99999TES' + b'GINEIDLEEHRAOGEBMDLENFDAFCHEIHZ9EBZDD9YHL' + ) + + # To speed up the test, we will mock the address generator. + # :py:class:`iota.crypto.addresses.AddressGenerator` already has + # its own test case, so this does not impact the stability of the + # codebase. + # noinspection PyUnusedLocal + def create_generator(ag, start, step=1): + for addy in [addy1, addy2][start::step]: + yield addy + + # The first address received IOTA. + self.adapter.seed_response( + 'findTransactions', + + { + 'duration': 42, + + 'hashes': [ + 'TESTVALUEFIVE9DONTUSEINPRODUCTION99999VH' + 'YHRHJETGYCAFZGABTEUBWCWAS9WF99UHBHRHLIOFJ', + ], + }, + ) + + # The second address is unused. + self.adapter.seed_response( + 'findTransactions', + + { + 'duration': 1, + 'hashes': [], + }, + ) + + self.adapter.seed_response( + 'getTrytes', + + { + 'duration': 99, + + # Thankfully, we do not have to seed a realistic response for + # ``getTrytes``, as we will be mocking the ``getBundles`` + # command that uses on it. + 'trytes': [''], + }, + ) + + bundle = Bundle([ + Transaction( + address = addy1, + timestamp = 1483033814, + + # These values are not relevant to the test. + hash_ = None, + signature_message_fragment = None, + value = 42, + tag = Tag(b''), + current_index = 0, + last_index = 0, + bundle_hash = None, + trunk_transaction_hash = None, + branch_transaction_hash = None, + nonce = None, + ) + ]) + + mock_get_bundles = Mock(return_value={ + 'bundles': [bundle], + }) + + with patch( + 'iota.crypto.addresses.AddressGenerator.create_generator', + create_generator, + ): + with patch( + 'iota.commands.extended.get_bundles.GetBundlesCommand._execute', + mock_get_bundles, + ): + response = self.command(seed=Seed.random()) + + self.assertDictEqual( + response, + + { + 'bundles': [bundle], + }, + ) def test_start(self): """ From 0b73ec417f7053f47b61fbf3a7e44f5bcbf0cdf4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 19:30:44 -0500 Subject: [PATCH 238/239] Enhanced unit tests. --- test/__init__.py | 6 ++++-- test/types_test.py | 17 +++++++++++++++++ 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/test/__init__.py b/test/__init__.py index 74570e8..7bce6aa 100644 --- a/test/__init__.py +++ b/test/__init__.py @@ -2,7 +2,6 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from collections import defaultdict from typing import Dict, List, Optional, Text from iota import BadApiResponse @@ -25,7 +24,7 @@ def __init__(self): # type: (Optional[dict]) -> None super(MockAdapter, self).__init__() - self.responses = defaultdict(list) # type: Dict[Text, List[dict]] + self.responses = {} # type: Dict[Text, List[dict]] self.requests = [] # type: List[dict] def seed_response(self, command, response): @@ -49,6 +48,9 @@ def seed_response(self, command, response): adapter.send_request({'command': 'sayHello'}) # {'message': 'Hello!'} """ + if command not in self.responses: + self.responses[command] = [] + self.responses[command].append(response) return self diff --git a/test/types_test.py b/test/types_test.py index 44a2fd3..8cc9c65 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -204,6 +204,15 @@ def test_slice_accessor(self): self.assertEqual(ts[-4:], TryteString(b'KBFA')) self.assertEqual(ts[4:-4:4], TryteString(b'9CEY')) + with self.assertRaises(IndexError): + # noinspection PyStatementEffect + ts[42] + + # To match the behavior of built-in types, TryteString will allow + # you to access a slice that occurs after the end of the sequence. + # There's nothing in it, of course, but you can access it. + self.assertEqual(ts[42:43], TryteString(b'')) + def test_slice_mutator(self): """ Modifying slices of a TryteString. @@ -233,6 +242,14 @@ def test_slice_mutator(self): ts[2:-2:2] = b'IOTA' self.assertEqual(ts, TryteString(b'EFIFOOTAABFA')) + with self.assertRaises(IndexError): + ts[42] = b'9' + + # To match the behavior of built-in types, TryteString will allow + # you to modify a slice that occurs after the end of the sequence. + ts[42:43] = TryteString(b'9') + self.assertEqual(ts, TryteString(b'EFIFOOTAABFA9')) + def test_iter_chunks(self): """ Iterating over a TryteString in constant-size chunks. From 5a561d98d3d9ce89aafa14be3200b6615d952c31 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 31 Dec 2016 19:43:01 -0500 Subject: [PATCH 239/239] Cleaned up project metadata. --- README.rst | 7 +++++++ setup.py | 5 +++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/README.rst b/README.rst index 9a92a37..304536e 100644 --- a/README.rst +++ b/README.rst @@ -9,6 +9,12 @@ 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, @@ -59,4 +65,5 @@ For the full documentation of this library, please refer to the .. _Slack: http://slack.iotatoken.com/ .. _dedicated forum: http://forum.iotatoken.com/ .. _official API: https://iota.readme.io/ +.. _PyOTA Bug Tracker: https://github.com/iotaledger/iota.lib.py/issues .. _tox: https://tox.readthedocs.io/ diff --git a/setup.py b/setup.py index 8419087..ac54f6c 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ name = 'PyOTA', description = 'IOTA API library for Python', url = 'https://github.com/iotaledger/pyota', - version = '1.0.0', + version = '1.0.0-beta1', packages = ['iota'], @@ -61,13 +61,14 @@ license = 'MIT', classifiers = [ - 'Development Status :: 3 - Alpha', + 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Programming Language :: Python :: 2', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3', 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', 'Topic :: Software Development :: Libraries :: Python Modules', ],