From f6015963b929c5db1abdf19b67fb35e41c1bf3d2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 11 Jun 2018 07:56:04 +1200 Subject: [PATCH 01/32] Progressive PEP-8 migration. --- iota/adapter/sandbox.py | 515 ++++++++++++++++++++-------------------- 1 file changed, 254 insertions(+), 261 deletions(-) diff --git a/iota/adapter/sandbox.py b/iota/adapter/sandbox.py index 18dab10..0b46231 100644 --- a/iota/adapter/sandbox.py +++ b/iota/adapter/sandbox.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from time import sleep from typing import Container, Optional, Text, Union @@ -12,285 +12,278 @@ from iota.exceptions import with_context __all__ = [ - 'SandboxAdapter', + 'SandboxAdapter', ] - -STATUS_ABORTED = 'ABORTED' -STATUS_FAILED = 'FAILED' +STATUS_ABORTED = 'ABORTED' +STATUS_FAILED = 'FAILED' STATUS_FINISHED = 'FINISHED' -STATUS_QUEUED = 'QUEUED' -STATUS_RUNNING = 'RUNNING' +STATUS_QUEUED = 'QUEUED' +STATUS_RUNNING = 'RUNNING' + class SandboxAdapter(HttpAdapter): - """ - HTTP adapter that sends requests to remote nodes operating in - "sandbox" mode. - - In sandbox mode, the node will only accept authenticated requests - from clients, and certain jobs are completed asynchronously. - - Note: for compatibility with Iota APIs, SandboxAdapter still operates - synchronously; it blocks until it determines that a job has completed - successfully. - - References: - - https://github.com/iotaledger/iota.lib.py/issues/19 - - https://github.com/iotaledger/documentation/blob/sandbox/source/index.html.md - """ - DEFAULT_POLL_INTERVAL = 15 - """ - Number of seconds to wait between requests to check job status. - """ - - DEFAULT_MAX_POLLS = 8 - """ - Maximum number of times to poll for job status before giving up. - """ - - def __init__( - self, - uri, - auth_token, - poll_interval = DEFAULT_POLL_INTERVAL, - max_polls = DEFAULT_MAX_POLLS, - ): - # type: (Union[Text, SplitResult], Optional[Text], int, int) -> None """ - :param uri: - URI of the node to connect to. - ``https://` URIs are recommended! + HTTP adapter that sends requests to remote nodes operating in + "sandbox" mode. - Note: Make sure the URI specifies the correct path! + In sandbox mode, the node will only accept authenticated requests + from clients, and certain jobs are completed asynchronously. - Example: + Note: for compatibility with Iota APIs, SandboxAdapter still + operates synchronously; it blocks until it determines that a job has + completed successfully. - - Incorrect: ``https://sandbox.iota:14265`` - - Correct: ``https://sandbox.iota:14265/api/v1/`` + References: + - https://github.com/iotaledger/iota.lib.py/issues/19 + - https://github.com/iotaledger/documentation/blob/sandbox/source/index.html.md + """ + DEFAULT_POLL_INTERVAL = 15 + """ + Number of seconds to wait between requests to check job status. + """ - :param auth_token: - Authorization token used to authenticate requests. + DEFAULT_MAX_POLLS = 8 + """ + Maximum number of times to poll for job status before giving up. + """ - Contact the node's maintainer to obtain a token. + def __init__( + self, + uri, + auth_token, + poll_interval=DEFAULT_POLL_INTERVAL, + max_polls=DEFAULT_MAX_POLLS, + ): + # type: (Union[Text, SplitResult], Optional[Text], int, int) -> None + """ + :param uri: + URI of the node to connect to. + ``https://` URIs are recommended! + + Note: Make sure the URI specifies the correct path! + + Example: + + - Incorrect: ``https://sandbox.iota:14265`` + - Correct: ``https://sandbox.iota:14265/api/v1/`` + + :param auth_token: + Authorization token used to authenticate requests. + + Contact the node's maintainer to obtain a token. + + If ``None``, the adapter will not include authorization + metadata with requests. + + :param poll_interval: + Number of seconds to wait between requests to check job + status. Must be a positive integer. + + Smaller values will cause the adapter to return a result + sooner (once the node completes the job), but it increases + traffic to the node (which may trip a rate limiter and/or + incur additional costs). + + :param max_polls: + Max number of times to poll for job status before giving up. + Must be a positive integer. + + This is effectively a timeout setting for asynchronous jobs; + multiply by ``poll_interval`` to get the timeout duration. + """ + super(SandboxAdapter, self).__init__(uri) + + if not (isinstance(auth_token, text_type) or (auth_token is None)): + raise with_context( + exc=TypeError( + '``auth_token`` must be a unicode string or ``None`` ' + '(``exc.context`` has more info).' + ), - If ``None``, the adapter will not include authorization metadata - with requests. + context={ + 'auth_token': auth_token, + }, + ) - :param poll_interval: - Number of seconds to wait between requests to check job status. - Must be a positive integer. + if auth_token == '': + raise with_context( + exc=ValueError( + 'Set ``auth_token=None`` if requests do not require ' + 'authorization (``exc.context`` has more info).', + ), - Smaller values will cause the adapter to return a result sooner - (once the node completes the job), but it increases traffic to - the node (which may trip a rate limiter and/or incur additional - costs). + context={ + 'auth_token': auth_token, + }, + ) - :param max_polls: - Max number of times to poll for job status before giving up. - Must be a positive integer. + if not isinstance(poll_interval, int): + raise with_context( + exc=TypeError( + '``poll_interval`` must be an int ' + '(``exc.context`` has more info).', + ), - This is effectively a timeout setting for asynchronous jobs; - multiply by ``poll_interval`` to get the timeout duration. - """ - super(SandboxAdapter, self).__init__(uri) - - if not (isinstance(auth_token, text_type) or (auth_token is None)): - raise with_context( - exc = - TypeError( - '``auth_token`` must be a unicode string or ``None`` ' - '(``exc.context`` has more info).' - ), - - context = { - 'auth_token': auth_token, - }, - ) - - if auth_token == '': - raise with_context( - exc = - ValueError( - 'Set ``auth_token=None`` if requests do not require authorization ' - '(``exc.context`` has more info).', - ), - - context = { - 'auth_token': auth_token, - }, - ) - - if not isinstance(poll_interval, int): - raise with_context( - exc = - TypeError( - '``poll_interval`` must be an int ' - '(``exc.context`` has more info).', - ), - - context = { - 'poll_interval': poll_interval, - }, - ) - - if poll_interval < 1: - raise with_context( - exc = - ValueError( - '``poll_interval`` must be > 0 ' - '(``exc.context`` has more info).', - ), - - context = { - 'poll_interval': poll_interval, - }, - ) - - if not isinstance(max_polls, int): - raise with_context( - exc = - TypeError( - '``max_polls`` must be an int ' - '(``exc.context`` has more info).', - ), - - context = { - 'max_polls': max_polls, - }, - ) - - if max_polls < 1: - raise with_context( - exc = - ValueError( - '``max_polls`` must be > 0 ' - '(``exc.context`` has more info).', - ), - - context = { - 'max_polls': max_polls, - }, - ) - - self.auth_token = auth_token # type: Optional[Text] - self.poll_interval = poll_interval # type: int - self.max_polls = max_polls # type: int - - @property - def node_url(self): - return compat.urllib_parse.urlunsplit(( - self.uri.scheme, - self.uri.netloc, - self.uri.path.rstrip('/') + '/commands', - self.uri.query, - self.uri.fragment, - )) - - @property - def authorization_header(self): - # type: () -> Text - """ - Returns the value to use for the ``Authorization`` header. - """ - return 'token {auth_token}'.format(auth_token=self.auth_token) + context={ + 'poll_interval': poll_interval, + }, + ) - def get_jobs_url(self, job_id): - # type: (Text) -> Text - """ - Returns the URL to check job status. + if poll_interval < 1: + raise with_context( + exc=ValueError( + '``poll_interval`` must be > 0 ' + '(``exc.context`` has more info).', + ), - :param job_id: - The ID of the job to check. - """ - return compat.urllib_parse.urlunsplit(( - self.uri.scheme, - self.uri.netloc, - self.uri.path.rstrip('/') + '/jobs/' + job_id, - self.uri.query, - self.uri.fragment, - )) - - def send_request(self, payload, **kwargs): - # type: (dict, dict) -> dict - if self.auth_token: - kwargs.setdefault('headers', {}) - kwargs['headers']['Authorization'] = self.authorization_header - - return super(SandboxAdapter, self).send_request(payload, **kwargs) - - def _interpret_response(self, response, payload, expected_status): - # type: (Response, dict, Container[int], bool) -> dict - decoded =\ - super(SandboxAdapter, self)._interpret_response( - response = response, - payload = payload, - expected_status = {codes['ok'], codes['accepted']}, - ) - - # Check to see if the request was queued for asynchronous - # execution. - if response.status_code == codes['accepted']: - poll_count = 0 - while decoded['status'] in (STATUS_QUEUED, STATUS_RUNNING): - if poll_count >= self.max_polls: - raise with_context( - exc = - BadApiResponse( - '``{command}`` job timed out after {duration} seconds ' - '(``exc.context`` has more info).'.format( - command = decoded['command'], - duration = self.poll_interval * self.max_polls, + context={ + 'poll_interval': poll_interval, + }, + ) + + if not isinstance(max_polls, int): + raise with_context( + exc=TypeError( + '``max_polls`` must be an int ' + '(``exc.context`` has more info).', ), - ), - - context = { - 'request': payload, - 'response': decoded, - }, - ) - - self._wait_to_poll() - poll_count += 1 - - poll_response = self._send_http_request( - headers = {'Authorization': self.authorization_header}, - method = 'get', - payload = None, - url = self.get_jobs_url(decoded['id']), + + context={ + 'max_polls': max_polls, + }, + ) + + if max_polls < 1: + raise with_context( + exc=ValueError( + '``max_polls`` must be > 0 ' + '(``exc.context`` has more info).', + ), + + context={ + 'max_polls': max_polls, + }, + ) + + self.auth_token = auth_token # type: Optional[Text] + self.poll_interval = poll_interval # type: int + self.max_polls = max_polls # type: int + + @property + def node_url(self): + return compat.urllib_parse.urlunsplit(( + self.uri.scheme, + self.uri.netloc, + self.uri.path.rstrip('/') + '/commands', + self.uri.query, + self.uri.fragment, + )) + + @property + def authorization_header(self): + # type: () -> Text + """ + Returns the value to use for the ``Authorization`` header. + """ + return 'token {auth_token}'.format(auth_token=self.auth_token) + + def get_jobs_url(self, job_id): + # type: (Text) -> Text + """ + Returns the URL to check job status. + + :param job_id: + The ID of the job to check. + """ + return compat.urllib_parse.urlunsplit(( + self.uri.scheme, + self.uri.netloc, + self.uri.path.rstrip('/') + '/jobs/' + job_id, + self.uri.query, + self.uri.fragment, + )) + + def send_request(self, payload, **kwargs): + # type: (dict, dict) -> dict + if self.auth_token: + kwargs.setdefault('headers', {}) + kwargs['headers']['Authorization'] = self.authorization_header + + return super(SandboxAdapter, self).send_request(payload, **kwargs) + + def _interpret_response(self, response, payload, expected_status): + # type: (Response, dict, Container[int]) -> dict + decoded = super(SandboxAdapter, self)._interpret_response( + response=response, + payload=payload, + expected_status={codes['ok'], codes['accepted']}, ) - decoded =\ - super(SandboxAdapter, self)._interpret_response( - response = poll_response, - payload = payload, - expected_status = {codes['ok']}, - ) - - if decoded['status'] == STATUS_FINISHED: - return decoded['{command}Response'.format(command=decoded['command'])] - - raise with_context( - exc = BadApiResponse( - decoded.get('error', {}).get('message') - or 'Command {status}: {decoded}'.format( - decoded = decoded, - status = decoded['status'].lower(), - ), - ), - - context = { - 'request': payload, - 'response': decoded, - }, - ) - - return decoded - - def _wait_to_poll(self): - """ - Waits for 1 interval (according to :py:attr:`poll_interval`). + # Check to see if the request was queued for asynchronous + # execution. + if response.status_code == codes['accepted']: + poll_count = 0 + while decoded['status'] in (STATUS_QUEUED, STATUS_RUNNING): + if poll_count >= self.max_polls: + raise with_context( + exc=BadApiResponse( + '``{command}`` job timed out after ' + '{duration} seconds ' + '(``exc.context`` has more info).'.format( + command=decoded['command'], + duration=self.poll_interval * self.max_polls, + ), + ), + + context={ + 'request': payload, + 'response': decoded, + }, + ) + + self._wait_to_poll() + poll_count += 1 + + poll_response = self._send_http_request( + headers={'Authorization': self.authorization_header}, + method='get', + payload=None, + url=self.get_jobs_url(decoded['id']), + ) + + decoded = super(SandboxAdapter, self)._interpret_response( + response=poll_response, + payload=payload, + expected_status={codes['ok']}, + ) + + if decoded['status'] == STATUS_FINISHED: + return decoded[ + '{command}Response'.format(command=decoded['command'])] + + raise with_context( + exc=BadApiResponse( + decoded.get('error', {}).get('message') + or 'Command {status}: {decoded}'.format( + decoded=decoded, + status=decoded['status'].lower(), + ), + ), - Implemented as a separate method so that it can be mocked during - unit tests ("Do you bite your thumb at us, sir?"). - """ - sleep(self.poll_interval) + context={ + 'request': payload, + 'response': decoded, + }, + ) + + return decoded + + def _wait_to_poll(self): + """ + Waits for 1 interval (according to :py:attr:`poll_interval`). + + Implemented as a separate method so that it can be mocked during + unit tests ("Do you bite your thumb at us, sir?"). + """ + sleep(self.poll_interval) From 4ad721911d9aca4d2fae90b607463936ca8515da Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 11 Jun 2018 07:57:36 +1200 Subject: [PATCH 02/32] Progressive PEP-8 migration. --- iota/adapter/wrappers.py | 163 ++++++++++++++++++++------------------- 1 file changed, 85 insertions(+), 78 deletions(-) diff --git a/iota/adapter/wrappers.py b/iota/adapter/wrappers.py index a798a88..f815298 100644 --- a/iota/adapter/wrappers.py +++ b/iota/adapter/wrappers.py @@ -1,105 +1,112 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from abc import ABCMeta, abstractmethod as abstract_method from typing import Dict, Text -from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter from six import with_metaclass +from iota.adapter import AdapterSpec, BaseAdapter, resolve_adapter + __all__ = [ - 'RoutingWrapper', + 'RoutingWrapper', ] class BaseWrapper(with_metaclass(ABCMeta, BaseAdapter)): - """ - Base functionality for "adapter wrappers", used to extend the - functionality of IOTA adapters. - """ - def __init__(self, adapter): - # type: (AdapterSpec) -> None - super(BaseWrapper, self).__init__() + """ + Base functionality for "adapter wrappers", used to extend the + functionality of IOTA adapters. + """ - if not isinstance(adapter, BaseAdapter): - adapter = resolve_adapter(adapter) + def __init__(self, adapter): + # type: (AdapterSpec) -> None + super(BaseWrapper, self).__init__() - self.adapter = adapter # type: BaseAdapter + if not isinstance(adapter, BaseAdapter): + adapter = resolve_adapter(adapter) - def get_uri(self): - # type: () -> Text - return self.adapter.get_uri() + self.adapter = adapter # type: BaseAdapter - @abstract_method - def send_request(self, payload, **kwargs): - # type: (dict, dict) -> dict - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) + def get_uri(self): + # type: () -> Text + return self.adapter.get_uri() + @abstract_method + def send_request(self, payload, **kwargs): + # type: (dict, dict) -> dict + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) -class RoutingWrapper(BaseWrapper): - """ - Routes commands to different nodes. - - This allows you to, for example, send POW requests to a local node, - while routing all other requests to a remote one. - - Example:: - - # Route POW to localhost, everything else to 12.34.56.78. - iota = Iota( - RoutingWrapper('http://12.34.56.78:14265') - .add_route('attachToTangle', 'http://localhost:14265') - .add_route('interruptAttachingToTangle', 'http://localhost:14265') - ), - ) - """ - def __init__(self, default_adapter): - # type: (AdapterSpec) -> None - """ - :param default_adapter: - Adapter to use for any routes not listed in ``routes``. - """ - super(RoutingWrapper, self).__init__(default_adapter) - # Try to limit the number of distinct adapter instances we create - # when resolving URIs. - self.adapter_aliases = {} # type: Dict[AdapterSpec, BaseAdapter] - - self.routes = {} # type: Dict[Text, BaseAdapter] - - def add_route(self, command, adapter): - # type: (Text, AdapterSpec) -> RoutingWrapper +class RoutingWrapper(BaseWrapper): """ - Adds a route to the wrapper. + Routes commands to different nodes. - :param command: - The name of the command to route (e.g., "attachToTangle"). + This allows you to, for example, send POW requests to a local node, + while routing all other requests to a remote one. - :param adapter: - The adapter object or URI to route requests to. - """ - if not isinstance(adapter, BaseAdapter): - try: - adapter = self.adapter_aliases[adapter] - except KeyError: - self.adapter_aliases[adapter] = adapter = resolve_adapter(adapter) + Example: - self.routes[command] = adapter + .. code-block:: python - return self - - def get_adapter(self, command): - # type: (Text) -> BaseAdapter - """ - Return the adapter for the specified command. + # Route POW to localhost, everything else to 12.34.56.78. + iota = Iota( + RoutingWrapper('http://12.34.56.78:14265') + .add_route('attachToTangle', 'http://localhost:14265') + .add_route('interruptAttachingToTangle', 'http://localhost:14265') + ), + ) """ - return self.routes.get(command, self.adapter) - - def send_request(self, payload, **kwargs): - # type: (dict, dict) -> dict - command = payload.get('command') - return self.get_adapter(command).send_request(payload, **kwargs) + def __init__(self, default_adapter): + # type: (AdapterSpec) -> None + """ + :param default_adapter: + Adapter to use for any routes not listed in ``routes``. + """ + super(RoutingWrapper, self).__init__(default_adapter) + + # Try to limit the number of distinct adapter instances we create + # when resolving URIs. + self.adapter_aliases = {} # type: Dict[AdapterSpec, BaseAdapter] + + self.routes = {} # type: Dict[Text, BaseAdapter] + + def add_route(self, command, adapter): + # type: (Text, AdapterSpec) -> RoutingWrapper + """ + Adds a route to the wrapper. + + :param command: + The name of the command to route (e.g., "attachToTangle"). + + :param adapter: + The adapter object or URI to route requests to. + """ + if not isinstance(adapter, BaseAdapter): + try: + adapter = self.adapter_aliases[adapter] + except KeyError: + self.adapter_aliases[adapter] = adapter = resolve_adapter( + adapter + ) + + self.routes[command] = adapter + + return self + + def get_adapter(self, command): + # type: (Text) -> BaseAdapter + """ + Return the adapter for the specified command. + """ + return self.routes.get(command, self.adapter) + + def send_request(self, payload, **kwargs): + # type: (dict, dict) -> dict + command = payload.get('command') + + return self.get_adapter(command).send_request(payload, **kwargs) From 232e2fe15c17e98805172a730e7978c39a717bf8 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 11 Jun 2018 08:01:06 +1200 Subject: [PATCH 03/32] Progressive PEP-8 migration. --- iota/bin/__init__.py | 303 ++++++++++++++++++++++--------------------- 1 file changed, 152 insertions(+), 151 deletions(-) diff --git a/iota/bin/__init__.py b/iota/bin/__init__.py index 2a35967..d597daa 100644 --- a/iota/bin/__init__.py +++ b/iota/bin/__init__.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import sys from abc import ABCMeta, abstractmethod as abstract_method @@ -16,162 +16,163 @@ from iota.crypto.types import Seed __all__ = [ - 'IotaCommandLineApp', + 'IotaCommandLineApp', ] class IotaCommandLineApp(with_metaclass(ABCMeta)): - """ - Base functionality for a PyOTA-powered command-line application. - """ - requires_seed = True - """ - Whether the command requires the user to provide a seed. - """ - - def __init__(self, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): - # type: (StringIO, StringIO, StringIO) -> None - super(IotaCommandLineApp, self).__init__() - - self.stdout = stdout - self.stderr = stderr - self.stdin = stdin - - @abstract_method - def execute(self, api, **arguments): - # type: (Iota, **Any) -> Optional[int] """ - Executes the command and (optionally) returns an exit code (used by - the shell to determine if the application exited cleanly). - - :param api: - The API object used to communicate with the node. - - :param arguments: - Command-line arguments parsed by the argument parser. - """ - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) - - def main(self): - """ - Executes the command from :py:data:`sys.argv` and exits. - """ - exit(self.run_from_argv()) - - def run_from_argv(self, argv=None): - # type: (Optional[tuple]) -> int + Base functionality for a PyOTA-powered command-line application. """ - Executes the command from a collection of arguments (e.g., - :py:data`sys.argv`) and returns the exit code. - - :param argv: - Arguments to pass to the argument parser. - If ``None``, defaults to ``sys.argv[1:]``. + requires_seed = True """ - exit_code = self.execute(**self.parse_argv(argv)) - - if exit_code is None: - exit_code = 0 - - return exit_code - - def parse_argv(self, argv=None): - # type: (Optional[tuple]) -> dict + Whether the command requires the user to provide a seed. """ - Parses arguments for the command. - - :param argv: - Arguments to pass to the argument parser. - If ``None``, defaults to ``sys.argv[1:]``. - """ - arguments = vars(self.create_argument_parser().parse_args(argv)) - - seed = None - if self.requires_seed: - seed_filepath = arguments.pop('seed_file') - - seed = ( - self.seed_from_filepath(seed_filepath) - if seed_filepath - else self.prompt_for_seed() - ) - - arguments['api'] =\ - Iota( - adapter = arguments.pop('uri'), - seed = seed, - testnet = arguments.pop('testnet'), - ) - - return arguments - - def create_argument_parser(self): - # type: () -> ArgumentParser - """ - Returns the argument parser that will be used to interpret - arguments and options from argv. - """ - parser = ArgumentParser( - description = self.__doc__, - epilog = 'PyOTA v{version}'.format(version=__version__), - ) - - parser.add_argument( - '--uri', - type = text_type, - default = 'http://localhost:14265/', - - help = - 'URI of the node to connect to ' - '(defaults to http://localhost:14265/).', - ) - - if self.requires_seed: - parser.add_argument( - '--seed-file', - type = text_type, - dest = 'seed_file', - - help = - 'Path to a file containing your seed in cleartext. ' - 'If not provided, you will be prompted to enter your seed ' - 'via stdin.', - ) - - parser.add_argument( - '--testnet', - action = 'store_true', - default = False, - help = 'If set, use testnet settings (e.g., for PoW).', - ) - - return parser - - @staticmethod - def seed_from_filepath(filepath): - # type: (Text) -> Seed - """ - Reads a seed from the first line of a text file. - - Any lines after the first are ignored. - """ - with open(filepath, 'rb') as f_: - return Seed(f_.readline().strip()) - - @staticmethod - def prompt_for_seed(): - # type: () -> Seed - """ - Prompts the user to enter their seed via stdin. - """ - 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') - return Seed(seed) if seed else Seed.random() + def __init__(self, stdout=sys.stdout, stderr=sys.stderr, stdin=sys.stdin): + # type: (StringIO, StringIO, StringIO) -> None + super(IotaCommandLineApp, self).__init__() + + self.stdout = stdout + self.stderr = stderr + self.stdin = stdin + + @abstract_method + def execute(self, api, **arguments): + # type: (Iota, **Any) -> Optional[int] + """ + Executes the command and (optionally) returns an exit code (used by + the shell to determine if the application exited cleanly). + + :param api: + The API object used to communicate with the node. + + :param arguments: + Command-line arguments parsed by the argument parser. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + def main(self): + """ + Executes the command from :py:data:`sys.argv` and exits. + """ + exit(self.run_from_argv()) + + def run_from_argv(self, argv=None): + # type: (Optional[tuple]) -> int + """ + Executes the command from a collection of arguments (e.g., + :py:data`sys.argv`) and returns the exit code. + + :param argv: + Arguments to pass to the argument parser. + If ``None``, defaults to ``sys.argv[1:]``. + """ + exit_code = self.execute(**self.parse_argv(argv)) + + if exit_code is None: + exit_code = 0 + + return exit_code + + def parse_argv(self, argv=None): + # type: (Optional[tuple]) -> dict + """ + Parses arguments for the command. + + :param argv: + Arguments to pass to the argument parser. + If ``None``, defaults to ``sys.argv[1:]``. + """ + arguments = vars(self.create_argument_parser().parse_args(argv)) + + seed = None + if self.requires_seed: + seed_filepath = arguments.pop('seed_file') + + seed = ( + self.seed_from_filepath(seed_filepath) + if seed_filepath + else self.prompt_for_seed() + ) + + arguments['api'] = Iota( + adapter=arguments.pop('uri'), + seed=seed, + testnet=arguments.pop('testnet'), + ) + + return arguments + + def create_argument_parser(self): + # type: () -> ArgumentParser + """ + Returns the argument parser that will be used to interpret + arguments and options from argv. + """ + parser = ArgumentParser( + description=self.__doc__, + epilog='PyOTA v{version}'.format(version=__version__), + ) + + parser.add_argument( + '--uri', + type=text_type, + default='http://localhost:14265/', + + help=( + 'URI of the node to connect to ' + '(defaults to http://localhost:14265/).' + ), + ) + + if self.requires_seed: + parser.add_argument( + '--seed-file', + type=text_type, + dest='seed_file', + + help=( + 'Path to a file containing your seed in cleartext. ' + 'If not provided, you will be prompted to enter ' + 'your seed via stdin.' + ), + ) + + parser.add_argument( + '--testnet', + action='store_true', + default=False, + help='If set, use testnet settings (e.g., for PoW).', + ) + + return parser + + @staticmethod + def seed_from_filepath(filepath): + # type: (Text) -> Seed + """ + Reads a seed from the first line of a text file. + + Any lines after the first are ignored. + """ + with open(filepath, 'rb') as f_: + return Seed(f_.readline().strip()) + + @staticmethod + def prompt_for_seed(): + # type: () -> Seed + """ + Prompts the user to enter their seed via stdin. + """ + 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') + + return Seed(seed) if seed else Seed.random() From 850d31d9b840c75399d6940684cb2d208db7912d Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 11 Jun 2018 08:05:04 +1200 Subject: [PATCH 04/32] Progressive PEP-8 migration. --- iota/bin/repl.py | 184 +++++++++++++++++++++++------------------------ 1 file changed, 92 insertions(+), 92 deletions(-) diff --git a/iota/bin/repl.py b/iota/bin/repl.py index dcab831..bfae94b 100755 --- a/iota/bin/repl.py +++ b/iota/bin/repl.py @@ -4,10 +4,10 @@ Launches a Python shell with a configured API client ready to go. """ from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from argparse import ArgumentParser -from logging import DEBUG, basicConfig, getLogger +from logging import basicConfig, getLogger, DEBUG from sys import stderr from six import text_type @@ -16,107 +16,107 @@ # Import all IOTA symbols into module scope, so that it's more # convenient for the user. from iota import * - from iota.adapter import resolve_adapter from iota.adapter.wrappers import RoutingWrapper from iota.bin import IotaCommandLineApp class IotaReplCommandLineApp(IotaCommandLineApp): - """ - Creates an IOTA API instance and drops the user into a REPL. - """ - def execute(self, api, **arguments): - # type: (Iota, ...) -> int - debug_requests = arguments['debug_requests'] - pow_uri = arguments['pow_uri'] - - # If ``pow_uri`` is specified, route POW requests to a separate - # node. - if pow_uri: - pow_adapter = resolve_adapter(pow_uri) - - api.adapter =\ - RoutingWrapper(api.adapter)\ - .add_route('attachToTangle', pow_adapter)\ - .add_route('interruptAttachingToTangle', pow_adapter) - - # If ``debug_requests`` is specified, log HTTP requests/responses. - if debug_requests: - # Inject a logger into the IOTA HTTP adapter. - basicConfig(level=DEBUG, stream=stderr) - - logger = getLogger(__name__) - logger.setLevel(DEBUG) - - api.adapter.set_logger(logger) - - # Turn on debugging for the underlying HTTP library. - http_client.HTTPConnection.debuglevel = 1 - - try: - self._start_repl(api) - except KeyboardInterrupt: - pass - - return 0 - - def create_argument_parser(self): - # type: () -> ArgumentParser - parser = super(IotaReplCommandLineApp, self).create_argument_parser() - - parser.add_argument( - '--pow-uri', - type = text_type, - default = None, - dest = 'pow_uri', - help = 'URI of node to send POW requests to.' - ) - - parser.add_argument( - '--debug', - action = 'store_true', - default = False, - dest = 'debug_requests', - help = 'If set, log HTTP requests to stderr.' - ) - - return parser - - @staticmethod - def _start_repl(api): - # type: (Iota) -> None """ - Starts the REPL. + Creates an IOTA API instance and drops the user into a REPL. """ - banner = ( - 'IOTA API client for {uri} ({testnet}) initialized as variable `api`.\n' - 'Type `help(api)` for list of API commands.'.format( - testnet = 'testnet' if api.testnet else 'mainnet', - uri = api.adapter.get_uri(), - ) - ) - - scope_vars = {'api': api} - - try: - # noinspection PyUnresolvedReferences - import IPython - except ImportError: - # IPython not available; use regular Python REPL. - from code import InteractiveConsole - InteractiveConsole(locals=scope_vars).interact(banner, '') - else: - print(banner) - IPython.start_ipython(argv=[], user_ns=scope_vars) + + def execute(self, api, **arguments): + # type: (Iota, ...) -> int + debug_requests = arguments['debug_requests'] + pow_uri = arguments['pow_uri'] + + # If ``pow_uri`` is specified, route POW requests to a separate + # node. + if pow_uri: + pow_adapter = resolve_adapter(pow_uri) + + api.adapter = RoutingWrapper(api.adapter) + api.adapter.add_route('attachToTangle', pow_adapter) + api.adapter.add_route('interruptAttachingToTangle', pow_adapter) + + # If ``debug_requests`` is specified, log HTTP requests/responses. + if debug_requests: + # Inject a logger into the IOTA HTTP adapter. + basicConfig(level=DEBUG, stream=stderr) + + logger = getLogger(__name__) + logger.setLevel(DEBUG) + + api.adapter.set_logger(logger) + + # Turn on debugging for the underlying HTTP library. + http_client.HTTPConnection.debuglevel = 1 + + try: + self._start_repl(api) + except KeyboardInterrupt: + pass + + return 0 + + def create_argument_parser(self): + # type: () -> ArgumentParser + parser = super(IotaReplCommandLineApp, self).create_argument_parser() + + parser.add_argument( + '--pow-uri', + type=text_type, + default=None, + dest='pow_uri', + help='URI of node to send POW requests to.' + ) + + parser.add_argument( + '--debug', + action='store_true', + default=False, + dest='debug_requests', + help='If set, log HTTP requests to stderr.' + ) + + return parser + + @staticmethod + def _start_repl(api): + # type: (Iota) -> None + """ + Starts the REPL. + """ + banner = ( + 'IOTA API client for {uri} ({testnet}) ' + 'initialized as variable `api`.\n' + 'Type `help(api)` for list of API commands.'.format( + testnet='testnet' if api.testnet else 'mainnet', + uri=api.adapter.get_uri(), + ) + ) + + scope_vars = {'api': api} + + try: + # noinspection PyUnresolvedReferences + import IPython + except ImportError: + # IPython not available; use regular Python REPL. + from code import InteractiveConsole + InteractiveConsole(locals=scope_vars).interact(banner, '') + else: + print(banner) + IPython.start_ipython(argv=[], user_ns=scope_vars) def main(): - """ - Entry point for ``setup.py``. - """ - IotaReplCommandLineApp().main() + """ + Entry point for ``setup.py``. + """ + IotaReplCommandLineApp().main() if __name__ == '__main__': - main() + main() From cca655fca361b4c2fdce80c29f22b906e5d4e998 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 11 Jun 2018 08:06:21 +1200 Subject: [PATCH 05/32] Progressive PEP-8 migration. --- iota/commands/core/__init__.py | 6 +++--- iota/commands/extended/__init__.py | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/iota/commands/core/__init__.py b/iota/commands/core/__init__.py index 6c16305..81f120a 100644 --- a/iota/commands/core/__init__.py +++ b/iota/commands/core/__init__.py @@ -3,12 +3,12 @@ Core commands are defined by the node API. References: - - https://iota.readme.io/docs/getting-started + +- https://iota.readme.io/docs/getting-started """ from __future__ import absolute_import, division, print_function, \ - unicode_literals - + unicode_literals from .add_neighbors import * from .attach_to_tangle import * diff --git a/iota/commands/extended/__init__.py b/iota/commands/extended/__init__.py index 2d2da9f..71d766f 100644 --- a/iota/commands/extended/__init__.py +++ b/iota/commands/extended/__init__.py @@ -4,12 +4,12 @@ additional functionality such as address generation and signatures. References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md + +- ht§tps://github.com/iotaledger/wiki/blob/master/api-proposal.md """ from __future__ import absolute_import, division, print_function, \ - unicode_literals - + unicode_literals from .broadcast_and_store import * from .get_account_data import * From e5f3f8259af7e9aba11c1ad2686005ba2d52b00a Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 11 Jun 2018 08:10:41 +1200 Subject: [PATCH 06/32] Cleaned up code file, added extra docs. --- iota/commands/extended/helpers.py | 36 ++++++++++++++++++++----------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/iota/commands/extended/helpers.py b/iota/commands/extended/helpers.py index 992f892..edb2c0f 100644 --- a/iota/commands/extended/helpers.py +++ b/iota/commands/extended/helpers.py @@ -1,18 +1,28 @@ -class Helpers(object): - """ - Adds additional helper functions that aren't part of the core or extended - API. - """ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from iota.transaction.types import TransactionHash - def __init__(self, api): - self.api = api - def is_promotable(self, tail): - # type: (TransactionHash) -> bool +class Helpers(object): """ - Determines if a tail transaction is promotable. + Adds additional helper functions that aren't part of the core or + extended API. - :param tail: - Transaction hash. Must be a tail transaction. + See https://github.com/iotaledger/iota.lib.py/pull/124 for more + context. """ - return self.api.check_consistency(tails=[tail])['state'] + + def __init__(self, api): + self.api = api + + def is_promotable(self, tail): + # type: (TransactionHash) -> bool + """ + Determines if a tail transaction is promotable. + + :param tail: + Transaction hash. Must be a tail transaction. + """ + return self.api.check_consistency(tails=[tail])['state'] From 0d57510471e7b333227c2ec69132a0bdf72f9391 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 12 Jun 2018 08:24:29 +1200 Subject: [PATCH 07/32] [#145] Reformat core commands for PEP-8. --- iota/commands/core/add_neighbors.py | 31 +-- iota/commands/core/attach_to_tangle.py | 65 ++++--- iota/commands/core/broadcast_transactions.py | 44 ++--- iota/commands/core/check_consistency.py | 40 ++-- iota/commands/core/find_transactions.py | 178 +++++++++--------- iota/commands/core/get_balances.py | 80 ++++---- iota/commands/core/get_inclusion_states.py | 77 ++++---- iota/commands/core/get_neighbors.py | 30 +-- iota/commands/core/get_node_info.py | 47 ++--- iota/commands/core/get_tips.py | 47 +++-- .../core/get_transactions_to_approve.py | 77 ++++---- iota/commands/core/get_trytes.py | 58 +++--- .../core/interrupt_attaching_to_tangle.py | 30 +-- iota/commands/core/remove_neighbors.py | 33 ++-- iota/commands/core/store_transactions.py | 43 +++-- .../core/were_addresses_spent_from.py | 45 ++--- 16 files changed, 453 insertions(+), 472 deletions(-) diff --git a/iota/commands/core/add_neighbors.py b/iota/commands/core/add_neighbors.py index 5943b4e..85e1ff1 100644 --- a/iota/commands/core/add_neighbors.py +++ b/iota/commands/core/add_neighbors.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -8,27 +8,28 @@ from iota.filters import NodeUri __all__ = [ - 'AddNeighborsCommand', + 'AddNeighborsCommand', ] class AddNeighborsCommand(FilterCommand): - """ - Executes `addNeighbors` command. + """ + Executes `addNeighbors` command. - See :py:meth:`iota.api.StrictIota.add_neighbors`. - """ - command = 'addNeighbors' + See :py:meth:`iota.api.StrictIota.add_neighbors`. + """ + command = 'addNeighbors' - def get_request_filter(self): - return AddNeighborsRequestFilter() + def get_request_filter(self): + return AddNeighborsRequestFilter() - def get_response_filter(self): - pass + 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), - }) + def __init__(self): + super(AddNeighborsRequestFilter, self).__init__({ + 'uris': + f.Required | f.Array | f.FilterRepeater(f.Required | NodeUri), + }) diff --git a/iota/commands/core/attach_to_tangle.py b/iota/commands/core/attach_to_tangle.py index e0ad1b0..daefec5 100644 --- a/iota/commands/core/attach_to_tangle.py +++ b/iota/commands/core/attach_to_tangle.py @@ -1,55 +1,58 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f + from iota import TransactionHash, TransactionTrytes from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes __all__ = [ - 'AttachToTangleCommand', + 'AttachToTangleCommand', ] class AttachToTangleCommand(FilterCommand): - """ - Executes ``attachToTangle`` command. + """ + Executes ``attachToTangle`` command. - See :py:meth:`iota.api.StrictIota.attach_to_tangle` for more info. - """ - command = 'attachToTangle' + See :py:meth:`iota.api.StrictIota.attach_to_tangle` for more info. + """ + command = 'attachToTangle' - def get_request_filter(self): - return AttachToTangleRequestFilter() + def get_request_filter(self): + return AttachToTangleRequestFilter() - def get_response_filter(self): - return AttachToTangleResponseFilter() + def get_response_filter(self): + return AttachToTangleResponseFilter() class AttachToTangleRequestFilter(RequestFilter): - def __init__(self): - super(AttachToTangleRequestFilter, self).__init__({ - 'branchTransaction': f.Required | Trytes(result_type=TransactionHash), - 'trunkTransaction': f.Required | Trytes(result_type=TransactionHash), + def __init__(self): + super(AttachToTangleRequestFilter, self).__init__({ + 'branchTransaction': f.Required | Trytes(TransactionHash), + 'trunkTransaction': f.Required | Trytes(TransactionHash), - 'trytes': - f.Required - | f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionTrytes)), + 'trytes': + f.Required | + f.Array | + f.FilterRepeater( + f.Required | Trytes(result_type=TransactionTrytes), + ), - # Loosely-validated; testnet nodes require a different value than - # mainnet. - 'minWeightMagnitude': f.Required| f.Type(int) | f.Min(1), - }) + # Loosely-validated; testnet nodes require a different value + # than mainnet. + 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), + }) class AttachToTangleResponseFilter(ResponseFilter): - def __init__(self): - super(AttachToTangleResponseFilter, self).__init__({ - 'trytes': - f.FilterRepeater( - f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionTrytes) - ), - }) + def __init__(self): + super(AttachToTangleResponseFilter, self).__init__({ + 'trytes': + f.FilterRepeater( + f.ByteString(encoding='ascii') | + Trytes(TransactionTrytes), + ), + }) diff --git a/iota/commands/core/broadcast_transactions.py b/iota/commands/core/broadcast_transactions.py index 70da1c5..77625c7 100644 --- a/iota/commands/core/broadcast_transactions.py +++ b/iota/commands/core/broadcast_transactions.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -9,34 +9,34 @@ from iota.filters import Trytes __all__ = [ - 'BroadcastTransactionsCommand', + 'BroadcastTransactionsCommand', ] class BroadcastTransactionsCommand(FilterCommand): - """ - Executes `broadcastTransactions` command. + """ + Executes `broadcastTransactions` command. - See :py:meth:`iota.api.StrictIota.broadcast_transactions`. - """ - command = 'broadcastTransactions' + See :py:meth:`iota.api.StrictIota.broadcast_transactions`. + """ + command = 'broadcastTransactions' - def get_request_filter(self): - return BroadcastTransactionsRequestFilter() + def get_request_filter(self): + return BroadcastTransactionsRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass class BroadcastTransactionsRequestFilter(RequestFilter): - def __init__(self): - super(BroadcastTransactionsRequestFilter, self).__init__({ - 'trytes': - f.Required - | f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=TransactionTrytes) - | f.Unicode(encoding='ascii', normalize=False) - ), - }) + def __init__(self): + super(BroadcastTransactionsRequestFilter, self).__init__({ + 'trytes': + f.Required | + f.Array | + f.FilterRepeater( + f.Required | + Trytes(TransactionTrytes) | + f.Unicode(encoding='ascii', normalize=False), + ), + }) diff --git a/iota/commands/core/check_consistency.py b/iota/commands/core/check_consistency.py index 09c3f41..598322e 100644 --- a/iota/commands/core/check_consistency.py +++ b/iota/commands/core/check_consistency.py @@ -1,38 +1,38 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f -from iota import Transaction, TransactionHash + +from iota import TransactionHash from iota.commands import FilterCommand, RequestFilter from iota.filters import Trytes __all__ = [ - 'CheckConsistencyCommand', + 'CheckConsistencyCommand', ] class CheckConsistencyCommand(FilterCommand): - """ - Executes ``checkConsistency`` extended API command. + """ + Executes ``checkConsistency`` extended API command. - See :py:meth:`iota.api.Iota.check_consistency` for more info. - """ - command = 'checkConsistency' + See :py:meth:`iota.api.Iota.check_consistency` for more info. + """ + command = 'checkConsistency' - def get_request_filter(self): - return CheckConsistencyRequestFilter() + def get_request_filter(self): + return CheckConsistencyRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass class CheckConsistencyRequestFilter(RequestFilter): - def __init__(self): - super(CheckConsistencyRequestFilter, self).__init__({ - 'tails': ( - f.Required - | f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionHash)) - ), - }) + def __init__(self): + super(CheckConsistencyRequestFilter, self).__init__({ + 'tails': + f.Required | + f.Array | + f.FilterRepeater(f.Required | Trytes(TransactionHash)), + }) diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index 0071586..a8411fb 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -1,117 +1,111 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f from six import iteritems -from iota import Address, Tag, TransactionHash +from iota import Tag, TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import AddressNoChecksum, Trytes __all__ = [ - 'FindTransactionsCommand', + 'FindTransactionsCommand', ] class FindTransactionsCommand(FilterCommand): - """ - Executes `findTransactions` command. + """ + Executes `findTransactions` command. - See :py:meth:`iota.api.StrictIota.find_transactions`. - """ - command = 'findTransactions' + See :py:meth:`iota.api.StrictIota.find_transactions`. + """ + command = 'findTransactions' - def get_request_filter(self): - return FindTransactionsRequestFilter() + def get_request_filter(self): + return FindTransactionsRequestFilter() - def get_response_filter(self): - return FindTransactionsResponseFilter() + def get_response_filter(self): + return FindTransactionsResponseFilter() 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__( - { - 'addresses': ( - f.Array - | f.FilterRepeater( - f.Required - | AddressNoChecksum() - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - - 'approvees': ( - f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=TransactionHash) - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - - 'bundles': ( - f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=TransactionHash) - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - - 'tags': ( - f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=Tag) - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - }, - - # 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, - ) - - def _apply(self, value): - value = super(FindTransactionsRequestFilter, self)._apply(value) # type: dict - - if self._has_errors: - return value - - # Remove null search terms. - # Note: We will assume that empty lists are intentional. - # https://github.com/iotaledger/iota.lib.py/issues/96 - search_terms = { - term: query - for term, query in iteritems(value) - if query is not None - } + CODE_NO_SEARCH_VALUES = 'no_search_values' - # At least one search term is required. - if not search_terms: - # Include unfiltered ``value`` in filter error context. - return self._invalid_value(value, self.CODE_NO_SEARCH_VALUES) + templates = { + CODE_NO_SEARCH_VALUES: 'No search values specified.', + } - return search_terms + def __init__(self): + super(FindTransactionsRequestFilter, self).__init__( + { + 'addresses': + f.Array | f.FilterRepeater( + f.Required | + AddressNoChecksum() | + f.Unicode(encoding='ascii', normalize=False), + ), + + 'approvees': + f.Array | f.FilterRepeater( + f.Required | + Trytes(TransactionHash) | + f.Unicode(encoding='ascii', normalize=False), + ), + + 'bundles': + f.Array | f.FilterRepeater( + f.Required | + Trytes(TransactionHash) | + f.Unicode(encoding='ascii', normalize=False), + ), + + 'tags': + f.Array | f.FilterRepeater( + f.Required | + Trytes(Tag) | + f.Unicode(encoding='ascii', normalize=False), + ), + }, + + # 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, + ) + + def _apply(self, value): + value = super(FindTransactionsRequestFilter, self)._apply( + value + ) # type: dict + + if self._has_errors: + return value + + # Remove null search terms. + # Note: We will assume that empty lists are intentional. + # https://github.com/iotaledger/iota.lib.py/issues/96 + search_terms = { + term: query + for term, query in iteritems(value) + if query is not None + } + + # At least one search term is required. + if not search_terms: + # Include unfiltered ``value`` in filter error context. + return self._invalid_value(value, self.CODE_NO_SEARCH_VALUES) + + return search_terms class FindTransactionsResponseFilter(ResponseFilter): - def __init__(self): - super(FindTransactionsResponseFilter, self).__init__({ - 'hashes': - f.FilterRepeater( - f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionHash) - ) - | f.Optional(default=[]), - }) + def __init__(self): + super(FindTransactionsResponseFilter, self).__init__({ + 'hashes': + f.FilterRepeater( + f.ByteString(encoding='ascii') | + Trytes(TransactionHash) + ) | + f.Optional(default=[]), + }) diff --git a/iota/commands/core/get_balances.py b/iota/commands/core/get_balances.py index a109b58..423e9ca 100644 --- a/iota/commands/core/get_balances.py +++ b/iota/commands/core/get_balances.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -9,58 +9,54 @@ from iota.filters import AddressNoChecksum, Trytes __all__ = [ - 'GetBalancesCommand', + 'GetBalancesCommand', ] class GetBalancesCommand(FilterCommand): - """ - Executes `getBalances` command. + """ + Executes `getBalances` command. - See :py:meth:`iota.api.StrictIota.get_balances`. - """ - command = 'getBalances' + See :py:meth:`iota.api.StrictIota.get_balances`. + """ + command = 'getBalances' - def get_request_filter(self): - return GetBalancesRequestFilter() + def get_request_filter(self): + return GetBalancesRequestFilter() - def get_response_filter(self): - return GetBalancesResponseFilter() + 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 - | AddressNoChecksum() - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - - 'threshold': ( - f.Type(int) - | f.Min(0) - | f.Max(100) - | f.Optional(default=100) - ), - }, - - allow_missing_keys = { - 'threshold', - }, - ) + def __init__(self): + super(GetBalancesRequestFilter, self).__init__( + { + 'addresses': + f.Required | f.Array | f.FilterRepeater( + f.Required | + AddressNoChecksum() | + f.Unicode(encoding='ascii', normalize=False), + ), + + 'threshold': + f.Type(int) | + f.Min(0) | + f.Max(100) | + f.Optional(default=100), + }, + + allow_missing_keys={ + 'threshold', + }, + ) class GetBalancesResponseFilter(ResponseFilter): - def __init__(self): - super(GetBalancesResponseFilter, self).__init__({ - 'balances': f.Array | f.FilterRepeater(f.Int), + def __init__(self): + super(GetBalancesResponseFilter, self).__init__({ + 'balances': f.Array | f.FilterRepeater(f.Int), - 'milestone': - f.ByteString(encoding='ascii') | Trytes(result_type=Address), - }) + 'milestone': + f.ByteString(encoding='ascii') | Trytes(Address), + }) diff --git a/iota/commands/core/get_inclusion_states.py b/iota/commands/core/get_inclusion_states.py index f5309c8..bc3066e 100644 --- a/iota/commands/core/get_inclusion_states.py +++ b/iota/commands/core/get_inclusion_states.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -9,53 +9,48 @@ from iota.filters import Trytes __all__ = [ - 'GetInclusionStatesCommand', + 'GetInclusionStatesCommand', ] class GetInclusionStatesCommand(FilterCommand): - """ - Executes ``getInclusionStates`` command. + """ + Executes ``getInclusionStates`` command. - See :py:meth:`iota.api.StrictIota.get_inclusion_states`. - """ - command = 'getInclusionStates' + See :py:meth:`iota.api.StrictIota.get_inclusion_states`. + """ + command = 'getInclusionStates' - def get_request_filter(self): - return GetInclusionStatesRequestFilter() + def get_request_filter(self): + return GetInclusionStatesRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass class GetInclusionStatesRequestFilter(RequestFilter): - def __init__(self): - super(GetInclusionStatesRequestFilter, self).__init__( - { - # Required parameters. - 'transactions': ( - f.Required - | f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=TransactionHash) - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - - # Optional parameters. - 'tips': ( - f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=TransactionHash) - | f.Unicode(encoding='ascii', normalize=False) - ) - | f.Optional(default=[]) - ), - }, - - allow_missing_keys = { - 'tips', - }, - ) + def __init__(self): + super(GetInclusionStatesRequestFilter, self).__init__( + { + # Required parameters. + 'transactions': + f.Required | f.Array | f.FilterRepeater( + f.Required | + Trytes(TransactionHash) | + f.Unicode(encoding='ascii', normalize=False), + ), + + # Optional parameters. + 'tips': + f.Array | f.FilterRepeater( + f.Required | + Trytes(TransactionHash) | + f.Unicode(encoding='ascii', normalize=False), + ) | + f.Optional(default=[]), + }, + + allow_missing_keys={ + 'tips', + }, + ) diff --git a/iota/commands/core/get_neighbors.py b/iota/commands/core/get_neighbors.py index d717bb9..9d074f4 100644 --- a/iota/commands/core/get_neighbors.py +++ b/iota/commands/core/get_neighbors.py @@ -1,31 +1,31 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from iota.commands import FilterCommand, RequestFilter __all__ = [ - 'GetNeighborsCommand', + 'GetNeighborsCommand', ] class GetNeighborsCommand(FilterCommand): - """ - Executes ``getNeighbors`` command. + """ + Executes ``getNeighbors`` command. - See :py:meth:`iota.api.StrictIota.get_neighbors`. - """ - command = 'getNeighbors' + See :py:meth:`iota.api.StrictIota.get_neighbors`. + """ + command = 'getNeighbors' - def get_request_filter(self): - return GetNeighborsRequestFilter() + def get_request_filter(self): + return GetNeighborsRequestFilter() - def get_response_filter(self): - pass + 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__({}) + 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/iota/commands/core/get_node_info.py b/iota/commands/core/get_node_info.py index b642b93..85d4386 100644 --- a/iota/commands/core/get_node_info.py +++ b/iota/commands/core/get_node_info.py @@ -1,45 +1,46 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f + from iota import TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import Trytes __all__ = [ - 'GetNodeInfoCommand', + 'GetNodeInfoCommand', ] class GetNodeInfoCommand(FilterCommand): - """ - Executes `getNodeInfo` command. + """ + Executes `getNodeInfo` command. - See :py:meth:`iota.api.StrictIota.get_node_info`. - """ - command = 'getNodeInfo' + See :py:meth:`iota.api.StrictIota.get_node_info`. + """ + command = 'getNodeInfo' - def get_request_filter(self): - return GetNodeInfoRequestFilter() + def get_request_filter(self): + return GetNodeInfoRequestFilter() - def get_response_filter(self): - return GetNodeInfoResponseFilter() + 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. - super(GetNodeInfoRequestFilter, self).__init__({}) + def __init__(self): + # ``getNodeInfo`` does not accept any parameters. + # Using a filter here just to enforce that the request is empty. + super(GetNodeInfoRequestFilter, self).__init__({}) class GetNodeInfoResponseFilter(ResponseFilter): - def __init__(self): - super(GetNodeInfoResponseFilter, self).__init__({ - 'latestMilestone': - f.ByteString(encoding='ascii') | Trytes(result_type=TransactionHash), - - 'latestSolidSubtangleMilestone': - f.ByteString(encoding='ascii') | Trytes(result_type=TransactionHash), - }) + def __init__(self): + super(GetNodeInfoResponseFilter, self).__init__({ + 'latestMilestone': + f.ByteString(encoding='ascii') | Trytes(TransactionHash), + + 'latestSolidSubtangleMilestone': + f.ByteString(encoding='ascii') | Trytes(TransactionHash), + }) diff --git a/iota/commands/core/get_tips.py b/iota/commands/core/get_tips.py index b97e9f8..f4ab15a 100644 --- a/iota/commands/core/get_tips.py +++ b/iota/commands/core/get_tips.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -9,40 +9,37 @@ from iota.transaction.types import TransactionHash __all__ = [ - 'GetTipsCommand', + 'GetTipsCommand', ] class GetTipsCommand(FilterCommand): - """ - Executes ``getTips`` command. + """ + Executes ``getTips`` command. - See :py:meth:`iota.api.StrictIota.get_tips`. - """ - command = 'getTips' + See :py:meth:`iota.api.StrictIota.get_tips`. + """ + command = 'getTips' - def get_request_filter(self): - return GetTipsRequestFilter() + def get_request_filter(self): + return GetTipsRequestFilter() - def get_response_filter(self): - return GetTipsResponseFilter() + 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__({}) + 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__({ - 'hashes': ( - f.Array - | f.FilterRepeater( - f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionHash) - ) - ), - }) + def __init__(self): + super(GetTipsResponseFilter, self).__init__({ + 'hashes': + f.Array | f.FilterRepeater( + f.ByteString(encoding='ascii') | Trytes(TransactionHash), + ), + }) diff --git a/iota/commands/core/get_transactions_to_approve.py b/iota/commands/core/get_transactions_to_approve.py index 7ab0b86..d9f9ac5 100644 --- a/iota/commands/core/get_transactions_to_approve.py +++ b/iota/commands/core/get_transactions_to_approve.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -9,60 +9,59 @@ from iota.filters import Trytes __all__ = [ - 'GetTransactionsToApproveCommand', + 'GetTransactionsToApproveCommand', ] class GetTransactionsToApproveCommand(FilterCommand): - """ - Executes ``getTransactionsToApprove`` command. + """ + Executes ``getTransactionsToApprove`` command. - See :py:meth:`iota.api.StrictIota.get_transactions_to_approve`. - """ - command = 'getTransactionsToApprove' + See :py:meth:`iota.api.StrictIota.get_transactions_to_approve`. + """ + command = 'getTransactionsToApprove' - def get_request_filter(self): - return GetTransactionsToApproveRequestFilter() + def get_request_filter(self): + return GetTransactionsToApproveRequestFilter() - def get_response_filter(self): - return GetTransactionsToApproveResponseFilter() + def get_response_filter(self): + return GetTransactionsToApproveResponseFilter() class GetTransactionsToApproveRequestFilter(RequestFilter): - def __init__(self): - super(GetTransactionsToApproveRequestFilter, self).__init__({ - 'depth': f.Required | f.Type(int) | f.Min(1), + def __init__(self): + super(GetTransactionsToApproveRequestFilter, self).__init__( + { + 'depth': f.Required | f.Type(int) | f.Min(1), - 'reference': Trytes(result_type=TransactionHash), - }, + 'reference': Trytes(result_type=TransactionHash), + }, - allow_missing_keys = { - 'reference', - }) + allow_missing_keys={ + 'reference', + }) - def _apply(self, value): - value = super(GetTransactionsToApproveRequestFilter, self)._apply(value) # type: dict + def _apply(self, value): + value = super(GetTransactionsToApproveRequestFilter, self)._apply( + value, + ) # type: dict - if self._has_errors: - return value + if self._has_errors: + return value - # Remove reference if null. - if value['reference'] is None: - del value['reference'] + # Remove reference if null. + if value['reference'] is None: + del value['reference'] - return value + return value class GetTransactionsToApproveResponseFilter(ResponseFilter): - def __init__(self): - super(GetTransactionsToApproveResponseFilter, self).__init__({ - 'branchTransaction': ( - f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionHash) - ), - - 'trunkTransaction': ( - f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionHash) - ), - }) + def __init__(self): + super(GetTransactionsToApproveResponseFilter, self).__init__({ + 'branchTransaction': + f.ByteString(encoding='ascii') | Trytes(TransactionHash), + + 'trunkTransaction': + f.ByteString(encoding='ascii') | Trytes(TransactionHash), + }) diff --git a/iota/commands/core/get_trytes.py b/iota/commands/core/get_trytes.py index e6aabde..3915b59 100644 --- a/iota/commands/core/get_trytes.py +++ b/iota/commands/core/get_trytes.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -9,45 +9,43 @@ from iota.filters import Trytes __all__ = [ - 'GetTrytesCommand', + 'GetTrytesCommand', ] class GetTrytesCommand(FilterCommand): - """ - Executes ``getTrytes`` command. + """ + Executes ``getTrytes`` command. - See :py:meth:`iota.api.StrictIota.get_trytes`. - """ - command = 'getTrytes' + See :py:meth:`iota.api.StrictIota.get_trytes`. + """ + command = 'getTrytes' - def get_request_filter(self): - return GetTrytesRequestFilter() + def get_request_filter(self): + return GetTrytesRequestFilter() - def get_response_filter(self): - return GetTrytesResponseFilter() + def get_response_filter(self): + return GetTrytesResponseFilter() class GetTrytesRequestFilter(RequestFilter): - def __init__(self): - super(GetTrytesRequestFilter, self).__init__({ - 'hashes': ( - f.Required - | f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=TransactionHash) - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - }) + def __init__(self): + super(GetTrytesRequestFilter, self).__init__({ + 'hashes': + f.Required | f.Array | f.FilterRepeater( + f.Required | + Trytes(TransactionHash) | + f.Unicode(encoding='ascii', normalize=False), + ), + }) class GetTrytesResponseFilter(ResponseFilter): - def __init__(self): - super(GetTrytesResponseFilter, self).__init__({ - 'trytes': ( - f.Array - | f.FilterRepeater(f.ByteString(encoding='ascii') | Trytes) - ), - }) + def __init__(self): + super(GetTrytesResponseFilter, self).__init__({ + 'trytes': + f.Array | f.FilterRepeater( + f.ByteString(encoding='ascii') | + Trytes + ), + }) diff --git a/iota/commands/core/interrupt_attaching_to_tangle.py b/iota/commands/core/interrupt_attaching_to_tangle.py index d1fffe3..837e87e 100644 --- a/iota/commands/core/interrupt_attaching_to_tangle.py +++ b/iota/commands/core/interrupt_attaching_to_tangle.py @@ -1,31 +1,31 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from iota.commands import FilterCommand, RequestFilter __all__ = [ - 'InterruptAttachingToTangleCommand', + 'InterruptAttachingToTangleCommand', ] class InterruptAttachingToTangleCommand(FilterCommand): - """ - Executes ``interruptAttachingToTangle`` command. + """ + Executes ``interruptAttachingToTangle`` command. - See :py:meth:`iota.api.StrictIota.interrupt_attaching_to_tangle`. - """ - command = 'interruptAttachingToTangle' + See :py:meth:`iota.api.StrictIota.interrupt_attaching_to_tangle`. + """ + command = 'interruptAttachingToTangle' - def get_request_filter(self): - return InterruptAttachingToTangleRequestFilter() + def get_request_filter(self): + return InterruptAttachingToTangleRequestFilter() - def get_response_filter(self): - pass + 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__({}) + 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/iota/commands/core/remove_neighbors.py b/iota/commands/core/remove_neighbors.py index 057db1c..6e0896c 100644 --- a/iota/commands/core/remove_neighbors.py +++ b/iota/commands/core/remove_neighbors.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -8,27 +8,30 @@ from iota.filters import NodeUri __all__ = [ - 'RemoveNeighborsCommand', + 'RemoveNeighborsCommand', ] class RemoveNeighborsCommand(FilterCommand): - """ - Executes ``removeNeighbors`` command. + """ + Executes ``removeNeighbors`` command. - See :py:meth:`iota.api.StrictIota.remove_neighbors`. - """ - command = 'removeNeighbors' + See :py:meth:`iota.api.StrictIota.remove_neighbors`. + """ + command = 'removeNeighbors' - def get_request_filter(self): - return RemoveNeighborsRequestFilter() + def get_request_filter(self): + return RemoveNeighborsRequestFilter() - def get_response_filter(self): - pass + 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), - }) + def __init__(self): + super(RemoveNeighborsRequestFilter, self).__init__({ + 'uris': f.Required | f.Array | f.FilterRepeater( + f.Required | + NodeUri, + ), + }) diff --git a/iota/commands/core/store_transactions.py b/iota/commands/core/store_transactions.py index 3a6f7c4..0412eec 100644 --- a/iota/commands/core/store_transactions.py +++ b/iota/commands/core/store_transactions.py @@ -1,41 +1,40 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f + from iota import TransactionTrytes from iota.commands import FilterCommand, RequestFilter from iota.filters import Trytes __all__ = [ - 'StoreTransactionsCommand', + 'StoreTransactionsCommand', ] class StoreTransactionsCommand(FilterCommand): - """ - Executes ``storeTransactions`` command. + """ + Executes ``storeTransactions`` command. - See :py:meth:`iota.api.StrictIota.store_transactions`. - """ - command = 'storeTransactions' + See :py:meth:`iota.api.StrictIota.store_transactions`. + """ + command = 'storeTransactions' - def get_request_filter(self): - return StoreTransactionsRequestFilter() + def get_request_filter(self): + return StoreTransactionsRequestFilter() - def get_response_filter(self): - pass + 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(result_type=TransactionTrytes) - | f.Unicode(encoding='ascii', normalize=False) - ), - }) + def __init__(self): + super(StoreTransactionsRequestFilter, self).__init__({ + 'trytes': + f.Required | f.Array | f.FilterRepeater( + f.Required | + Trytes(TransactionTrytes) | + f.Unicode(encoding='ascii', normalize=False), + ), + }) diff --git a/iota/commands/core/were_addresses_spent_from.py b/iota/commands/core/were_addresses_spent_from.py index 317ee99..0d41e17 100644 --- a/iota/commands/core/were_addresses_spent_from.py +++ b/iota/commands/core/were_addresses_spent_from.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f @@ -8,37 +8,32 @@ from iota.filters import AddressNoChecksum __all__ = [ - 'WereAddressesSpentFromCommand', + 'WereAddressesSpentFromCommand', ] class WereAddressesSpentFromCommand(FilterCommand): - """ - Executes `wereAddressesSpentFrom` command. + """ + Executes `wereAddressesSpentFrom` command. - See :py:meth:`iota.api.StrictIota.were_addresses_spent_from`. - """ - command = 'wereAddressesSpentFrom' + See :py:meth:`iota.api.StrictIota.were_addresses_spent_from`. + """ + command = 'wereAddressesSpentFrom' - def get_request_filter(self): - return WereAddressesSpentFromRequestFilter() + def get_request_filter(self): + return WereAddressesSpentFromRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass class WereAddressesSpentFromRequestFilter(RequestFilter): - def __init__(self): - super(WereAddressesSpentFromRequestFilter, self).__init__( - { - 'addresses': ( - f.Required - | f.Array - | f.FilterRepeater( - f.Required - | AddressNoChecksum() - | f.Unicode(encoding='ascii', normalize=False) - ) - ), - } - ) + def __init__(self): + super(WereAddressesSpentFromRequestFilter, self).__init__({ + 'addresses': + f.Required | f.Array | f.FilterRepeater( + f.Required | + AddressNoChecksum() | + f.Unicode(encoding='ascii', normalize=False), + ), + }) From d4662f51538fa4b844fa780bbbdc9e4507ca3ad6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 13 Jun 2018 08:57:21 +1200 Subject: [PATCH 08/32] [#145][#184] Reformat for PEP-8, remove dead code. --- iota/commands/extended/broadcast_and_store.py | 36 +-- iota/commands/extended/get_account_data.py | 246 ++++++++------- iota/commands/extended/get_bundles.py | 198 ++++++------ iota/commands/extended/get_inputs.py | 293 +++++++++--------- .../commands/extended/get_latest_inclusion.py | 58 ++-- iota/commands/extended/get_new_addresses.py | 146 +++++---- iota/commands/extended/get_transfers.py | 218 ++++++------- iota/commands/extended/is_reattachable.py | 103 +++--- iota/commands/extended/prepare_transfer.py | 239 +++++++------- iota/commands/extended/promote_transaction.py | 86 ++--- iota/commands/extended/replay_bundle.py | 74 +++-- iota/commands/extended/send_transfer.py | 155 +++++---- iota/commands/extended/send_trytes.py | 103 +++--- iota/commands/extended/utils.py | 193 ++++++------ 14 files changed, 1095 insertions(+), 1053 deletions(-) diff --git a/iota/commands/extended/broadcast_and_store.py b/iota/commands/extended/broadcast_and_store.py index 4c3007e..c28526b 100644 --- a/iota/commands/extended/broadcast_and_store.py +++ b/iota/commands/extended/broadcast_and_store.py @@ -1,34 +1,34 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from iota.commands import FilterCommand from iota.commands.core.broadcast_transactions import \ - BroadcastTransactionsCommand + BroadcastTransactionsCommand from iota.commands.core.store_transactions import StoreTransactionsCommand __all__ = [ - 'BroadcastAndStoreCommand', + 'BroadcastAndStoreCommand', ] class BroadcastAndStoreCommand(FilterCommand): - """ - Executes ``broadcastAndStore`` extended API command. + """ + Executes ``broadcastAndStore`` extended API command. - See :py:meth:`iota.api.Iota.broadcast_and_store` for more info. - """ - command = 'broadcastAndStore' + See :py:meth:`iota.api.Iota.broadcast_and_store` for more info. + """ + command = 'broadcastAndStore' - def get_request_filter(self): - pass + def get_request_filter(self): + pass - def get_response_filter(self): - pass + def get_response_filter(self): + pass - def _execute(self, request): - BroadcastTransactionsCommand(self.adapter)(**request) - StoreTransactionsCommand(self.adapter)(**request) - return { - 'trytes': request['trytes'], - } + def _execute(self, request): + BroadcastTransactionsCommand(self.adapter)(**request) + StoreTransactionsCommand(self.adapter)(**request) + return { + 'trytes': request['trytes'], + } diff --git a/iota/commands/extended/get_account_data.py b/iota/commands/extended/get_account_data.py index 4b602f6..12ef466 100644 --- a/iota/commands/extended/get_account_data.py +++ b/iota/commands/extended/get_account_data.py @@ -1,145 +1,153 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from operator import attrgetter from typing import List, Optional import filters as f + from iota import Address, TransactionHash from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.commands.core.get_balances import GetBalancesCommand from iota.commands.extended.utils import get_bundles_from_transaction_hashes, \ - iter_used_addresses + iter_used_addresses from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes __all__ = [ - 'GetAccountDataCommand', + 'GetAccountDataCommand', ] class GetAccountDataCommand(FilterCommand): - """ - Executes ``getAccountData`` extended API command. - - See :py:meth:`iota.api.Iota.get_account_data` for more info. - """ - command = 'getAccountData' - - def get_request_filter(self): - return GetAccountDataRequestFilter() - - def get_response_filter(self): - pass - - def _execute(self, request): - inclusion_states = request['inclusionStates'] # type: bool - seed = request['seed'] # type: Seed - start = request['start'] # type: int - stop = request['stop'] # type: Optional[int] - - if stop is None: - my_addresses = [] # type: List[Address] - my_hashes = [] # type: List[TransactionHash] - - for addy, hashes in iter_used_addresses(self.adapter, seed, start): - my_addresses.append(addy) - my_hashes.extend(hashes) - else: - ft_command = FindTransactionsCommand(self.adapter) - - my_addresses = AddressGenerator(seed).get_addresses(start, stop - start) - my_hashes = ft_command(addresses=my_addresses).get('hashes') or [] - - account_balance = 0 - if my_hashes: - # Load balances for the addresses that we generated. - gb_response = GetBalancesCommand(self.adapter)(addresses=my_addresses) - - for i, balance in enumerate(gb_response['balances']): - my_addresses[i].balance = balance - account_balance += balance - - return { - 'addresses': list(sorted(my_addresses, key=attrgetter('key_index'))), - 'balance': account_balance, - - 'bundles': - get_bundles_from_transaction_hashes( - adapter = self.adapter, - transaction_hashes = my_hashes, - inclusion_states = inclusion_states, - ), - } + """ + Executes ``getAccountData`` extended API command. + + See :py:meth:`iota.api.Iota.get_account_data` for more info. + """ + command = 'getAccountData' + + def get_request_filter(self): + return GetAccountDataRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + inclusion_states = request['inclusionStates'] # type: bool + seed = request['seed'] # type: Seed + start = request['start'] # type: int + stop = request['stop'] # type: Optional[int] + + if stop is None: + my_addresses = [] # type: List[Address] + my_hashes = [] # type: List[TransactionHash] + + for addy, hashes in iter_used_addresses(self.adapter, seed, start): + my_addresses.append(addy) + my_hashes.extend(hashes) + else: + ft_command = FindTransactionsCommand(self.adapter) + + my_addresses = ( + AddressGenerator(seed).get_addresses(start, stop - start) + ) + my_hashes = ft_command(addresses=my_addresses).get('hashes') or [] + + account_balance = 0 + if my_hashes: + # Load balances for the addresses that we generated. + gb_response = ( + GetBalancesCommand(self.adapter)(addresses=my_addresses) + ) + + for i, balance in enumerate(gb_response['balances']): + my_addresses[i].balance = balance + account_balance += balance + + return { + 'addresses': + list(sorted(my_addresses, key=attrgetter('key_index'))), + + 'balance': account_balance, + + 'bundles': + get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=my_hashes, + inclusion_states=inclusion_states, + ), + } class GetAccountDataRequestFilter(RequestFilter): - MAX_INTERVAL = 500 - - CODE_INTERVAL_INVALID = 'interval_invalid' - CODE_INTERVAL_TOO_BIG = 'interval_too_big' - - templates = { - CODE_INTERVAL_INVALID: '``start`` must be <= ``stop``', - CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}', - } - - def __init__(self): - super(GetAccountDataRequestFilter, self).__init__( - { - # Required parameters. - 'seed': f.Required | Trytes(result_type=Seed), - - # Optional parameters. - 'stop': f.Type(int) | f.Min(0), - 'start': f.Type(int) | f.Min(0) | f.Optional(0), - - 'inclusionStates': f.Type(bool) | f.Optional(False), - }, - - allow_missing_keys = { - 'stop', - 'inclusionStates', - 'start', - }, - ) - - def _apply(self, value): - # noinspection PyProtectedMember - filtered = super(GetAccountDataRequestFilter, self)._apply(value) - - if self._has_errors: - return filtered - - if filtered['stop'] is not None: - if filtered['start'] > filtered['stop']: - filtered['start'] = self._invalid_value( - value = filtered['start'], - reason = self.CODE_INTERVAL_INVALID, - sub_key = 'start', - - context = { - 'start': filtered['start'], - 'stop': filtered['stop'], - }, - ) - elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL: - filtered['stop'] = self._invalid_value( - value = filtered['stop'], - reason = self.CODE_INTERVAL_TOO_BIG, - sub_key = 'stop', - - context = { - 'start': filtered['start'], - 'stop': filtered['stop'], - }, - - template_vars = { - 'max_interval': self.MAX_INTERVAL, - }, + MAX_INTERVAL = 500 + + CODE_INTERVAL_INVALID = 'interval_invalid' + CODE_INTERVAL_TOO_BIG = 'interval_too_big' + + templates = { + CODE_INTERVAL_INVALID: '``start`` must be <= ``stop``', + CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}', + } + + def __init__(self): + super(GetAccountDataRequestFilter, self).__init__( + { + # Required parameters. + 'seed': f.Required | Trytes(Seed), + + # Optional parameters. + 'stop': f.Type(int) | f.Min(0), + 'start': f.Type(int) | f.Min(0) | f.Optional(0), + + 'inclusionStates': f.Type(bool) | f.Optional(False), + }, + + allow_missing_keys={ + 'stop', + 'inclusionStates', + 'start', + }, ) - return filtered + def _apply(self, value): + # noinspection PyProtectedMember + filtered = super(GetAccountDataRequestFilter, self)._apply(value) + + if self._has_errors: + return filtered + + if filtered['stop'] is not None: + if filtered['start'] > filtered['stop']: + filtered['start'] = self._invalid_value( + value=filtered['start'], + reason=self.CODE_INTERVAL_INVALID, + sub_key='start', + + context={ + 'start': filtered['start'], + 'stop': filtered['stop'], + }, + ) + + elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL: + filtered['stop'] = self._invalid_value( + value=filtered['stop'], + reason=self.CODE_INTERVAL_TOO_BIG, + sub_key='stop', + + context={ + 'start': filtered['start'], + 'stop': filtered['stop'], + }, + + template_vars={ + 'max_interval': self.MAX_INTERVAL, + }, + ) + + return filtered diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index dab84ce..bec1efc 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -1,12 +1,13 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, Optional import filters as f + from iota import BadApiResponse, Bundle, BundleHash, Transaction, \ - TransactionHash, TryteString + TransactionHash, TryteString from iota.commands import FilterCommand, RequestFilter from iota.commands.core.get_trytes import GetTrytesCommand from iota.exceptions import with_context @@ -14,108 +15,111 @@ from iota.transaction.validator import BundleValidator __all__ = [ - 'GetBundlesCommand', + '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): - transaction_hash = request['transaction'] # type: TransactionHash - - bundle = Bundle(self._traverse_bundle(transaction_hash)) - validator = BundleValidator(bundle) - - if not validator.is_valid(): - raise with_context( - exc = BadApiResponse( - 'Bundle failed validation (``exc.context`` has more info).', - ), - - context = { - 'bundle': bundle, - 'errors': validator.errors, - }, - ) - - 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] """ - Recursively traverse the Tangle, collecting transactions until we - hit a new bundle. + Executes ``getBundles`` extended API command. - This method is (usually) faster than ``findTransactions``, and it - ensures we don't collect transactions from replayed bundles. + See :py:meth:`iota.api.Iota.get_bundles` for more info. """ - trytes = GetTrytesCommand(self.adapter)(hashes=[txn_hash])['trytes'] # type: List[TryteString] - - if not trytes: - raise with_context( - exc = BadApiResponse( - 'Bundle transactions not visible (``exc.context`` has more info).', - ), - - context = { - 'transaction_hash': txn_hash, - 'target_bundle_hash': 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 = { - '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( - txn_hash = transaction.trunk_transaction_hash, - target_bundle_hash = target_bundle_hash - ) - + command = 'getBundles' + + def get_request_filter(self): + return GetBundlesRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + transaction_hash = request['transaction'] # type: TransactionHash + + bundle = Bundle(self._traverse_bundle(transaction_hash)) + validator = BundleValidator(bundle) + + if not validator.is_valid(): + raise with_context( + exc=BadApiResponse( + 'Bundle failed validation (``exc.context`` has more info).', + ), + + context={ + 'bundle': bundle, + 'errors': validator.errors, + }, + ) + + 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] + """ + 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=[txn_hash])['trytes'] + ) # type: List[TryteString] + + if not trytes: + raise with_context( + exc=BadApiResponse( + 'Bundle transactions not visible ' + '(``exc.context`` has more info).', + ), + + context={ + 'transaction_hash': txn_hash, + 'target_bundle_hash': 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={ + '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( + txn_hash=transaction.trunk_transaction_hash, + target_bundle_hash=target_bundle_hash + ) class GetBundlesRequestFilter(RequestFilter): - def __init__(self): - super(GetBundlesRequestFilter, self).__init__({ - 'transaction': f.Required | Trytes(result_type=TransactionHash), - }) + def __init__(self): + super(GetBundlesRequestFilter, self).__init__({ + 'transaction': f.Required | Trytes(TransactionHash), + }) diff --git a/iota/commands/extended/get_inputs.py b/iota/commands/extended/get_inputs.py index 77d62c1..0349c0c 100644 --- a/iota/commands/extended/get_inputs.py +++ b/iota/commands/extended/get_inputs.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Optional @@ -16,151 +16,164 @@ from iota.filters import SecurityLevel, Trytes __all__ = [ - 'GetInputsCommand', + '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): - stop = request['stop'] # type: Optional[int] - seed = request['seed'] # type: Seed - start = request['start'] # type: int - threshold = request['threshold'] # type: Optional[int] - security_level = request['securityLevel'] # int - - # Determine the addresses we will be scanning. - if stop is None: - addresses =\ - [addy for addy, _ in iter_used_addresses(self.adapter, seed, start, security_level=security_level)] - else: - addresses = AddressGenerator(seed, security_level).get_addresses(start, stop - start) - - if addresses: - # Load balances for the addresses that we generated. - gb_response = GetBalancesCommand(self.adapter)(addresses=addresses) - else: - gb_response = {'balances': []} - - result = { - 'inputs': [], - 'totalBalance': 0, - } - - threshold_met = threshold is None - - for i, balance in enumerate(gb_response['balances']): - addresses[i].balance = balance - - if balance: - result['inputs'].append(addresses[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( - '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'], - }, - ) + """ + 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): + stop = request['stop'] # type: Optional[int] + seed = request['seed'] # type: Seed + start = request['start'] # type: int + threshold = request['threshold'] # type: Optional[int] + security_level = request['securityLevel'] # int + + # Determine the addresses we will be scanning. + if stop is None: + addresses = [addy for addy, _ in iter_used_addresses( + adapter=self.adapter, + seed=seed, + start=start, + security_level=security_level + )] + else: + addresses = ( + AddressGenerator(seed, security_level).get_addresses( + start=start, + count=stop - start, + ) + ) + + if addresses: + # Load balances for the addresses that we generated. + gb_response = GetBalancesCommand(self.adapter)(addresses=addresses) + else: + gb_response = {'balances': []} + + result = { + 'inputs': [], + 'totalBalance': 0, + } + + threshold_met = threshold is None + + for i, balance in enumerate(gb_response['balances']): + addresses[i].balance = balance + + if balance: + result['inputs'].append(addresses[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( + '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'], + }, + ) 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 <= ``stop``', - CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}', - } - - def __init__(self): - super(GetInputsRequestFilter, self).__init__( - { - # These arguments are optional. - 'stop': f.Type(int) | f.Min(0), - 'start': f.Type(int) | f.Min(0) | f.Optional(0), - 'threshold': f.Type(int) | f.Min(0), - - 'securityLevel': SecurityLevel, - - # These arguments are required. - 'seed': f.Required | Trytes(result_type=Seed), - }, - - allow_missing_keys={ - 'stop', - 'start', - 'threshold', - 'securityLevel', - } - ) - - def _apply(self, value): - # noinspection PyProtectedMember - filtered = super(GetInputsRequestFilter, self)._apply(value) - - if self._has_errors: - return filtered - - if filtered['stop'] is not None: - if filtered['start'] > filtered['stop']: - filtered['start'] = self._invalid_value( - value = filtered['start'], - reason = self.CODE_INTERVAL_INVALID, - sub_key = 'start', - - context = { - 'start': filtered['start'], - 'stop': filtered['stop'], - }, - ) - elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL: - filtered['stop'] = self._invalid_value( - value = filtered['stop'], - reason = self.CODE_INTERVAL_TOO_BIG, - sub_key = 'stop', - - context = { - 'start': filtered['start'], - 'stop': filtered['stop'], - }, - - template_vars = { - 'max_interval': self.MAX_INTERVAL, - }, + MAX_INTERVAL = 500 + + CODE_INTERVAL_INVALID = 'interval_invalid' + CODE_INTERVAL_TOO_BIG = 'interval_too_big' + + templates = { + CODE_INTERVAL_INVALID: '``start`` must be <= ``stop``', + CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}', + } + + def __init__(self): + super(GetInputsRequestFilter, self).__init__( + { + # These arguments are optional. + 'stop': f.Type(int) | f.Min(0), + 'start': f.Type(int) | f.Min(0) | f.Optional(0), + 'threshold': f.Type(int) | f.Min(0), + + 'securityLevel': SecurityLevel, + + # These arguments are required. + 'seed': f.Required | Trytes(Seed), + }, + + allow_missing_keys={ + 'stop', + 'start', + 'threshold', + 'securityLevel', + } ) - return filtered + def _apply(self, value): + filtered = super(GetInputsRequestFilter, self)._apply(value) + + if self._has_errors: + return filtered + + if filtered['stop'] is not None: + if filtered['start'] > filtered['stop']: + filtered['start'] = self._invalid_value( + value=filtered['start'], + reason=self.CODE_INTERVAL_INVALID, + sub_key='start', + + context={ + 'start': filtered['start'], + 'stop': filtered['stop'], + }, + ) + + elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL: + filtered['stop'] = self._invalid_value( + value=filtered['stop'], + reason=self.CODE_INTERVAL_TOO_BIG, + sub_key='stop', + + context={ + 'start': filtered['start'], + 'stop': filtered['stop'], + }, + + template_vars={ + 'max_interval': self.MAX_INTERVAL, + }, + ) + + return filtered diff --git a/iota/commands/extended/get_latest_inclusion.py b/iota/commands/extended/get_latest_inclusion.py index 4d42a35..cf96bbf 100644 --- a/iota/commands/extended/get_latest_inclusion.py +++ b/iota/commands/extended/get_latest_inclusion.py @@ -1,10 +1,11 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + 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 @@ -12,45 +13,44 @@ from iota.filters import Trytes __all__ = [ - 'GetLatestInclusionCommand', + 'GetLatestInclusionCommand', ] class GetLatestInclusionCommand(FilterCommand): - """ - Executes ``getLatestInclusion`` extended API command. + """ + Executes ``getLatestInclusion`` extended API command. - See :py:meth:`iota.api.Iota.get_latest_inclusion` for more info. - """ - command = 'getLatestInclusion' + See :py:meth:`iota.api.Iota.get_latest_inclusion` for more info. + """ + command = 'getLatestInclusion' - def get_request_filter(self): - return GetLatestInclusionRequestFilter() + def get_request_filter(self): + return GetLatestInclusionRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass - def _execute(self, request): - hashes = request['hashes'] # type: List[TransactionHash] + def _execute(self, request): + hashes = request['hashes'] # type: List[TransactionHash] - gni_response = GetNodeInfoCommand(self.adapter)() + gni_response = GetNodeInfoCommand(self.adapter)() - gis_response = GetInclusionStatesCommand(self.adapter)( - transactions = hashes, - tips = [gni_response['latestSolidSubtangleMilestone']], - ) + gis_response = GetInclusionStatesCommand(self.adapter)( + transactions=hashes, + tips=[gni_response['latestSolidSubtangleMilestone']], + ) - return { - 'states': dict(zip(hashes, gis_response['states'])), - } + 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)) - ), - }) + def __init__(self): + super(GetLatestInclusionRequestFilter, self).__init__({ + 'hashes': + f.Required | f.Array | f.FilterRepeater( + f.Required | Trytes(TransactionHash), + ), + }) diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index 552561d..fa9d088 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, Optional @@ -14,86 +14,82 @@ from iota.filters import SecurityLevel, Trytes __all__ = [ - 'GetNewAddressesCommand', + '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 - - def _execute(self, request): - checksum = request['checksum'] # type: bool - count = request['count'] # type: Optional[int] - index = request['index'] # type: int - security_level = request['securityLevel'] # type: int - seed = request['seed'] # type: Seed - - return { - 'addresses': - self._find_addresses(seed, index, count, security_level, checksum), - } - - def _find_addresses(self, seed, index, count, security_level, checksum): - # type: (Seed, int, Optional[int], int, bool) -> List[Address] """ - Find addresses matching the command parameters. - """ - generator = AddressGenerator(seed, security_level, checksum) - - if count is None: - # Connect to Tangle and find the first address without any - # transactions. - for addy in generator.create_iterator(start=index): - # We use addy.address here because FindTransactions does - # not work on an address with a checksum - response = FindTransactionsCommand(self.adapter)( - addresses=[addy.address] - ) - - if not response.get('hashes'): - return [addy] + Executes ``getNewAddresses`` extended API command. - return generator.get_addresses(start=index, count=count) + 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 + + def _execute(self, request): + checksum = request['checksum'] # type: bool + count = request['count'] # type: Optional[int] + index = request['index'] # type: int + security_level = request['securityLevel'] # type: int + seed = request['seed'] # type: Seed + + return { + 'addresses': + self._find_addresses( + seed, + index, + count, + security_level, + checksum, + ), + } + + def _find_addresses(self, seed, index, count, security_level, checksum): + # type: (Seed, int, Optional[int], int, bool) -> List[Address] + """ + Find addresses matching the command parameters. + """ + generator = AddressGenerator(seed, security_level, checksum) + + if count is None: + # Connect to Tangle and find the first address without any + # transactions. + for addy in generator.create_iterator(start=index): + # We use addy.address here because FindTransactions does + # not work on an address with a checksum + response = FindTransactionsCommand(self.adapter)( + addresses=[addy.address], + ) + + if not response.get('hashes'): + return [addy] + + return generator.get_addresses(start=index, count=count) class GetNewAddressesRequestFilter(RequestFilter): - MAX_SECURITY_LEVEL = 3 - """ - Max allowed value for ``securityLevel``. - - Note that :py:class:`AddressGenerator` does not enforce a limit, just - in case you are sure that you REALLY know what you are doing. - """ - - def __init__(self): - super(GetNewAddressesRequestFilter, self).__init__( - { - # Everything except ``seed`` is optional. - - 'checksum': f.Type(bool) | f.Optional(default=False), - 'count': f.Type(int) | f.Min(1), - 'index': f.Type(int) | f.Min(0) | f.Optional(default=0), - - 'securityLevel': SecurityLevel, - - 'seed': f.Required | Trytes(result_type=Seed), - }, - - allow_missing_keys = { - 'checksum', - 'count', - 'index', - 'securityLevel', - }, - ) + def __init__(self): + super(GetNewAddressesRequestFilter, self).__init__( + { + # Everything except ``seed`` is optional. + 'checksum': f.Type(bool) | f.Optional(default=False), + 'count': f.Type(int) | f.Min(1), + 'index': f.Type(int) | f.Min(0) | f.Optional(default=0), + 'securityLevel': SecurityLevel, + + 'seed': f.Required | Trytes(Seed), + }, + + allow_missing_keys={ + 'checksum', + 'count', + 'index', + 'securityLevel', + }, + ) diff --git a/iota/commands/extended/get_transfers.py b/iota/commands/extended/get_transfers.py index e337042..a8e5e9a 100644 --- a/iota/commands/extended/get_transfers.py +++ b/iota/commands/extended/get_transfers.py @@ -1,134 +1,136 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from itertools import chain from typing import Optional import filters as f + from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.commands.extended.utils import get_bundles_from_transaction_hashes, \ - iter_used_addresses + iter_used_addresses from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed from iota.filters import Trytes __all__ = [ - 'GetTransfersCommand', + '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): - inclusion_states = request['inclusionStates'] # type: bool - seed = request['seed'] # type: Seed - start = request['start'] # type: int - stop = request['stop'] # type: Optional[int] - - # Determine the addresses we will be scanning, and pull their - # transaction hashes. - if stop is None: - my_hashes = list(chain(*( - hashes - for _, hashes in iter_used_addresses(self.adapter, seed, start) - ))) - else: - ft_response =\ - FindTransactionsCommand(self.adapter)( - addresses = - AddressGenerator(seed).get_addresses(start, stop - start), - ) + """ + 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): + inclusion_states = request['inclusionStates'] # type: bool + seed = request['seed'] # type: Seed + start = request['start'] # type: int + stop = request['stop'] # type: Optional[int] + + # Determine the addresses we will be scanning, and pull their + # transaction hashes. + if stop is None: + my_hashes = list(chain(*( + hashes + for _, hashes in iter_used_addresses(self.adapter, seed, start) + ))) + else: + ft_response = \ + FindTransactionsCommand(self.adapter)( + addresses= + AddressGenerator(seed).get_addresses(start, stop - start), + ) + + my_hashes = ft_response['hashes'] + + return { + 'bundles': + get_bundles_from_transaction_hashes( + adapter=self.adapter, + transaction_hashes=my_hashes, + inclusion_states=inclusion_states, + ), + } + + +class GetTransfersRequestFilter(RequestFilter): + MAX_INTERVAL = 500 - my_hashes = ft_response['hashes'] + CODE_INTERVAL_INVALID = 'interval_invalid' + CODE_INTERVAL_TOO_BIG = 'interval_too_big' - return { - 'bundles': - get_bundles_from_transaction_hashes( - adapter = self.adapter, - transaction_hashes = my_hashes, - inclusion_states = inclusion_states, - ), + templates = { + CODE_INTERVAL_INVALID: '``start`` must be <= ``stop``', + CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}', } + def __init__(self): + super(GetTransfersRequestFilter, self).__init__( + { + # Required parameters. + 'seed': f.Required | Trytes(Seed), -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 <= ``stop``', - CODE_INTERVAL_TOO_BIG: '``stop`` - ``start`` must be <= {max_interval}', - } - - def __init__(self): - super(GetTransfersRequestFilter, self).__init__( - { - # Required parameters. - 'seed': f.Required | Trytes(result_type=Seed), - - # Optional parameters. - 'stop': f.Type(int) | f.Min(0), - 'start': f.Type(int) | f.Min(0) | f.Optional(0), - - 'inclusionStates': f.Type(bool) | f.Optional(False), - }, - - allow_missing_keys = { - 'stop', - 'inclusionStates', - 'start', - }, - ) - - def _apply(self, value): - # noinspection PyProtectedMember - filtered = super(GetTransfersRequestFilter, self)._apply(value) - - if self._has_errors: - return filtered - - if filtered['stop'] is not None: - if filtered['start'] > filtered['stop']: - filtered['start'] = self._invalid_value( - value = filtered['start'], - reason = self.CODE_INTERVAL_INVALID, - sub_key = 'start', - - context = { - 'start': filtered['start'], - 'stop': filtered['stop'], - }, - ) - elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL: - filtered['stop'] = self._invalid_value( - value = filtered['stop'], - reason = self.CODE_INTERVAL_TOO_BIG, - sub_key = 'stop', - - context = { - 'start': filtered['start'], - 'stop': filtered['stop'], - }, - - template_vars = { - 'max_interval': self.MAX_INTERVAL, - }, + # Optional parameters. + 'stop': f.Type(int) | f.Min(0), + 'start': f.Type(int) | f.Min(0) | f.Optional(0), + + 'inclusionStates': f.Type(bool) | f.Optional(False), + }, + + allow_missing_keys={ + 'stop', + 'inclusionStates', + 'start', + }, ) - return filtered + def _apply(self, value): + # noinspection PyProtectedMember + filtered = super(GetTransfersRequestFilter, self)._apply(value) + + if self._has_errors: + return filtered + + if filtered['stop'] is not None: + if filtered['start'] > filtered['stop']: + filtered['start'] = self._invalid_value( + value=filtered['start'], + reason=self.CODE_INTERVAL_INVALID, + sub_key='start', + + context={ + 'start': filtered['start'], + 'stop': filtered['stop'], + }, + ) + + elif (filtered['stop'] - filtered['start']) > self.MAX_INTERVAL: + filtered['stop'] = self._invalid_value( + value=filtered['stop'], + reason=self.CODE_INTERVAL_TOO_BIG, + sub_key='stop', + + context={ + 'start': filtered['start'], + 'stop': filtered['stop'], + }, + + template_vars={ + 'max_interval': self.MAX_INTERVAL, + }, + ) + + return filtered diff --git a/iota/commands/extended/is_reattachable.py b/iota/commands/extended/is_reattachable.py index b308567..8238dc6 100644 --- a/iota/commands/extended/is_reattachable.py +++ b/iota/commands/extended/is_reattachable.py @@ -1,5 +1,7 @@ # coding=utf-8 -from __future__ import absolute_import, division, print_function, unicode_literals +from __future__ import absolute_import, division, print_function, \ + unicode_literals + from typing import List import filters as f @@ -11,65 +13,76 @@ from iota.filters import Trytes __all__ = [ - 'IsReattachableCommand', + 'IsReattachableCommand', ] class IsReattachableCommand(FilterCommand): - """ - Executes ``isReattachable`` extended API command. - """ - command = 'isReattachable' + """ + Executes ``isReattachable`` extended API command. + """ + command = 'isReattachable' - def get_request_filter(self): - return IsReattachableRequestFilter() + def get_request_filter(self): + return IsReattachableRequestFilter() - def get_response_filter(self): - return IsReattachableResponseFilter() + def get_response_filter(self): + return IsReattachableResponseFilter() - def _execute(self, request): - addresses = request['addresses'] # type: List[Address] + def _execute(self, request): + addresses = request['addresses'] # type: List[Address] - # fetch full transaction objects - transactions = find_transaction_objects(adapter=self.adapter, **{'addresses': addresses}) + # fetch full transaction objects + transactions = find_transaction_objects( + adapter=self.adapter, + addresses=addresses, + ) - # map and filter transactions, which have zero value. - # If multiple transactions for the same address are returned the one with the - # highest attachment_timestamp is selected - transactions = sorted(transactions, key=lambda t: t.attachment_timestamp) - transaction_map = {t.address: t.hash for t in transactions if t.value > 0} + # Map and filter transactions which have zero value. + # If multiple transactions for the same address are returned, + # the one with the highest ``attachment_timestamp`` is selected. + transactions = sorted( + transactions, + key=lambda t: t.attachment_timestamp + ) - # fetch inclusion states - inclusion_states = GetLatestInclusionCommand(adapter=self.adapter)(hashes=list(transaction_map.values())) - inclusion_states = inclusion_states['states'] + transaction_map = { + t.address: t.hash + for t in transactions + if t.value > 0 + } + + # Fetch inclusion states. + inclusion_states = GetLatestInclusionCommand(adapter=self.adapter)( + hashes=list(transaction_map.values()), + ) + inclusion_states = inclusion_states['states'] - return { - 'reattachable': [not inclusion_states[transaction_map[address]] for address in addresses] - } + return { + 'reattachable': [ + not inclusion_states[transaction_map[address]] + for address in addresses + ], + } class IsReattachableRequestFilter(RequestFilter): - def __init__(self): - super(IsReattachableRequestFilter, self).__init__( - { - 'addresses': ( - f.Required - | f.Array - | f.FilterRepeater( - f.Required - | Trytes(result_type=Address) - | f.Unicode(encoding='ascii', normalize=False) - ) + def __init__(self): + super(IsReattachableRequestFilter, self).__init__( + { + 'addresses': + f.Required | f.Array | f.FilterRepeater( + f.Required | + Trytes(Address) | + f.Unicode(encoding='ascii', normalize=False), + ), + }, ) - } - ) class IsReattachableResponseFilter(ResponseFilter): - def __init__(self): - super(IsReattachableResponseFilter, self).__init__({ - 'reattachable': ( - f.Required - | f.Array - | f.FilterRepeater(f.Type(bool))) - }) + def __init__(self): + super(IsReattachableResponseFilter, self).__init__({ + 'reattachable': + f.Required | f.Array | f.FilterRepeater(f.Type(bool)), + }) diff --git a/iota/commands/extended/prepare_transfer.py b/iota/commands/extended/prepare_transfer.py index cd3cb90..dd725b8 100644 --- a/iota/commands/extended/prepare_transfer.py +++ b/iota/commands/extended/prepare_transfer.py @@ -1,13 +1,13 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, Optional import filters as f from iota import Address, BadApiResponse, ProposedBundle, \ - ProposedTransaction + ProposedTransaction from iota.commands import FilterCommand, RequestFilter from iota.commands.core.get_balances import GetBalancesCommand from iota.commands.extended.get_inputs import GetInputsCommand @@ -18,131 +18,136 @@ from iota.filters import GeneratedAddress, SecurityLevel, Trytes __all__ = [ - 'PrepareTransferCommand', + 'PrepareTransferCommand', ] class PrepareTransferCommand(FilterCommand): - """ - Executes ``prepareTransfer`` extended API command. - - See :py:meth:`iota.api.Iota.prepare_transfer` for more info. - """ - command = 'prepareTransfer' - - def get_request_filter(self): - return PrepareTransferRequestFilter() - - def get_response_filter(self): - pass - - def _execute(self, request): - # Required parameters. - seed = request['seed'] # type: Seed - bundle = ProposedBundle(request['transfers']) - - # Optional parameters. - change_address = request.get('changeAddress') # type: Optional[Address] - proposed_inputs = request.get('inputs') # type: Optional[List[Address]] - security_level = request['securityLevel'] # type: int - - 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 is None: - # No inputs provided. Scan addresses for unspent inputs. - gi_response = GetInputsCommand(self.adapter)( - seed = seed, - threshold = want_to_spend, - securityLevel=security_level, - ) - - confirmed_inputs = gi_response['inputs'] - else: - # Inputs provided. Check to make sure we have sufficient - # balance. - available_to_spend = 0 - confirmed_inputs = [] # type: List[Address] - - gb_response = GetBalancesCommand(self.adapter)( - addresses = [i.address for i in proposed_inputs], - ) + """ + Executes ``prepareTransfer`` extended API command. - for i, balance in enumerate(gb_response.get('balances') or []): - input_ = proposed_inputs[i] - - if balance > 0: - available_to_spend += balance - - # 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( - 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, - }, - ) - - bundle.add_inputs(confirmed_inputs) + See :py:meth:`iota.api.Iota.prepare_transfer` for more info. + """ + command = 'prepareTransfer' - if bundle.balance < 0: - if not change_address: - change_address =\ - GetNewAddressesCommand(self.adapter)(seed=seed, securityLevel=security_level)['addresses'][0] + def get_request_filter(self): + return PrepareTransferRequestFilter() - bundle.send_unspent_inputs_to(change_address) + def get_response_filter(self): + pass - bundle.finalize() - - if confirmed_inputs: - bundle.sign_inputs(KeyGenerator(seed)) - else: - bundle.finalize() + def _execute(self, request): + # Required parameters. + seed = request['seed'] # type: Seed + bundle = ProposedBundle(request['transfers']) - return { - 'trytes': bundle.as_tryte_strings(), - } + # Optional parameters. + change_address = request.get('changeAddress') # type: Optional[Address] + proposed_inputs = request.get('inputs') # type: Optional[List[Address]] + security_level = request['securityLevel'] # type: int + + 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 is None: + # No inputs provided. Scan addresses for unspent + # inputs. + gi_response = GetInputsCommand(self.adapter)( + seed=seed, + threshold=want_to_spend, + securityLevel=security_level, + ) + + confirmed_inputs = gi_response['inputs'] + else: + # Inputs provided. Check to make sure we have + # sufficient balance. + available_to_spend = 0 + 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 + + # 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( + 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, + }, + ) + + bundle.add_inputs(confirmed_inputs) + + if bundle.balance < 0: + if not change_address: + change_address = \ + GetNewAddressesCommand(self.adapter)( + seed=seed, + securityLevel=security_level, + )['addresses'][0] + + bundle.send_unspent_inputs_to(change_address) + + bundle.finalize() + + if confirmed_inputs: + bundle.sign_inputs(KeyGenerator(seed)) + else: + bundle.finalize() + + return { + 'trytes': bundle.as_tryte_strings(), + } class PrepareTransferRequestFilter(RequestFilter): - def __init__(self): - super(PrepareTransferRequestFilter, self).__init__( - { - # Required parameters. - 'seed': f.Required | Trytes(result_type=Seed), - - 'transfers': ( - f.Required - | f.Array - | f.FilterRepeater(f.Required | f.Type(ProposedTransaction)) - ), + def __init__(self): + super(PrepareTransferRequestFilter, self).__init__( + { + # Required parameters. + 'seed': f.Required | Trytes(Seed), + + 'transfers': + f.Required | f.Array | f.FilterRepeater( + f.Required | f.Type(ProposedTransaction), + ), + + # Optional parameters. + 'changeAddress': Trytes(Address), + 'securityLevel': SecurityLevel, + + # Note that ``inputs`` is allowed to be an empty array. + 'inputs': + f.Array | f.FilterRepeater(f.Required | GeneratedAddress), + }, - # Optional parameters. - 'changeAddress': Trytes(result_type=Address), - 'securityLevel': SecurityLevel, - - # Note that ``inputs`` is allowed to be an empty array. - 'inputs': - f.Array | f.FilterRepeater(f.Required | GeneratedAddress), - }, - - allow_missing_keys = { - 'changeAddress', - 'inputs', - 'securityLevel', - }, - ) + allow_missing_keys={ + 'changeAddress', + 'inputs', + 'securityLevel', + }, + ) diff --git a/iota/commands/extended/promote_transaction.py b/iota/commands/extended/promote_transaction.py index ccc6e3e..3bcbd1a 100644 --- a/iota/commands/extended/promote_transaction.py +++ b/iota/commands/extended/promote_transaction.py @@ -1,68 +1,68 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import filters as f -from iota import ( - Bundle, TransactionHash, Address, ProposedTransaction, BadApiResponse, -) + +from iota import Address, BadApiResponse, ProposedTransaction, TransactionHash from iota.commands import FilterCommand, RequestFilter from iota.commands.core.check_consistency import CheckConsistencyCommand from iota.commands.extended.send_transfer import SendTransferCommand from iota.filters import Trytes __all__ = [ - 'PromoteTransactionCommand', + 'PromoteTransactionCommand', ] class PromoteTransactionCommand(FilterCommand): - """ - Executes ``promoteTransaction`` extended API command. + """ + Executes ``promoteTransaction`` extended API command. - See :py:meth:`iota.api.Iota.promote_transaction` for more information. - """ - command = 'promoteTransaction' + See :py:meth:`iota.api.Iota.promote_transaction` for more + information. + """ + command = 'promoteTransaction' - def get_request_filter(self): - return PromoteTransactionRequestFilter() + def get_request_filter(self): + return PromoteTransactionRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass - def _execute(self, request): - depth = request['depth'] # type: int - min_weight_magnitude = request['minWeightMagnitude'] # type: int - transaction = request['transaction'] # type: TransactionHash + def _execute(self, request): + depth = request['depth'] # type: int + min_weight_magnitude = request['minWeightMagnitude'] # type: int + transaction = request['transaction'] # type: TransactionHash - cc_response = CheckConsistencyCommand(self.adapter)(tails=[transaction]) - if cc_response['state'] is False: - raise BadApiResponse( - 'Transaction {transaction} is not promotable. ' - 'You should reattach first.'.format(transaction=transaction) - ) + cc_response = CheckConsistencyCommand(self.adapter)(tails=[transaction]) + if cc_response['state'] is False: + raise BadApiResponse( + 'Transaction {transaction} is not promotable. ' + 'You should reattach first.'.format(transaction=transaction) + ) - spam_transfer = ProposedTransaction( - address=Address(b''), - value=0, - ) + spam_transfer = ProposedTransaction( + address=Address(b''), + value=0, + ) - return SendTransferCommand(self.adapter)( - seed=spam_transfer.address, - depth=depth, - transfers=[spam_transfer], - minWeightMagnitude=min_weight_magnitude, - reference=transaction, - ) + return SendTransferCommand(self.adapter)( + seed=spam_transfer.address, + depth=depth, + transfers=[spam_transfer], + minWeightMagnitude=min_weight_magnitude, + reference=transaction, + ) class PromoteTransactionRequestFilter(RequestFilter): - def __init__(self): - super(PromoteTransactionRequestFilter, self).__init__({ - 'depth': f.Required | f.Type(int) | f.Min(1), - 'transaction': f.Required | Trytes(result_type=TransactionHash), + def __init__(self): + super(PromoteTransactionRequestFilter, self).__init__({ + 'depth': f.Required | f.Type(int) | f.Min(1), + 'transaction': f.Required | Trytes(TransactionHash), - # Loosely-validated; testnet nodes require a different value than - # mainnet. - 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), - }) + # Loosely-validated; testnet nodes require a different value + # than mainnet. + 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), + }) diff --git a/iota/commands/extended/replay_bundle.py b/iota/commands/extended/replay_bundle.py index e29eb35..aff8e1e 100644 --- a/iota/commands/extended/replay_bundle.py +++ b/iota/commands/extended/replay_bundle.py @@ -1,63 +1,59 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals - -from typing import List + unicode_literals import filters as f -from iota import Bundle -from iota import TransactionHash + +from iota import Bundle, TransactionHash 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 __all__ = [ - 'ReplayBundleCommand', + '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() + """ + Executes ``replayBundle`` extended API command. - def get_response_filter(self): - pass + See :py:meth:`iota.api.Iota.replay_bundle` for more information. + """ + command = 'replayBundle' - def _execute(self, request): - depth = request['depth'] # type: int - min_weight_magnitude = request['minWeightMagnitude'] # type: int - transaction = request['transaction'] # type: TransactionHash + def get_request_filter(self): + return ReplayBundleRequestFilter() - gb_response = GetBundlesCommand(self.adapter)(transaction=transaction) + def get_response_filter(self): + pass - # Note that we only replay the first bundle returned by - # ``getBundles``. - bundle = gb_response['bundles'][0] # type: Bundle + def _execute(self, request): + depth = request['depth'] # type: int + min_weight_magnitude = request['minWeightMagnitude'] # type: int + transaction = request['transaction'] # type: TransactionHash - return SendTrytesCommand(self.adapter)( - depth = depth, - minWeightMagnitude = min_weight_magnitude, + gb_response = GetBundlesCommand(self.adapter)(transaction=transaction) + # Note that we only replay the first bundle returned by + # ``getBundles``. + bundle = gb_response['bundles'][0] # type: Bundle - trytes = bundle.as_tryte_strings(), - ) + return SendTrytesCommand(self.adapter)( + depth=depth, + minWeightMagnitude=min_weight_magnitude, + trytes=bundle.as_tryte_strings(), + ) 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), - - # Loosely-validated; testnet nodes require a different value than - # mainnet. - 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), - }) + def __init__(self): + super(ReplayBundleRequestFilter, self).__init__({ + 'depth': f.Required | f.Type(int) | f.Min(1), + 'transaction': f.Required | Trytes(TransactionHash), + + # Loosely-validated; testnet nodes require a different value + # than mainnet. + 'minWeightMagnitude': 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 7c5060d..ada6607 100644 --- a/iota/commands/extended/send_transfer.py +++ b/iota/commands/extended/send_transfer.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, Optional @@ -14,87 +14,86 @@ from iota.filters import SecurityLevel, Trytes __all__ = [ - 'SendTransferCommand', + '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): - change_address = request['changeAddress'] # type: Optional[Address] - depth = request['depth'] # type: int - inputs = request['inputs'] # type: Optional[List[Address]] - min_weight_magnitude = request['minWeightMagnitude'] # type: int - seed = request['seed'] # type: Seed - transfers = request['transfers'] # type: List[ProposedTransaction] - reference = request['reference'] # type: Optional[TransactionHash] - security_level = request['securityLevel'] # int - - pt_response = PrepareTransferCommand(self.adapter)( - changeAddress = change_address, - inputs = inputs, - seed = seed, - transfers = transfers, - securityLevel = security_level, - ) - - st_response = SendTrytesCommand(self.adapter)( - depth = depth, - minWeightMagnitude = min_weight_magnitude, - trytes = pt_response['trytes'], - reference = reference, - ) - - return { - 'bundle': Bundle.from_tryte_strings(st_response['trytes']), - } + """ + 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): + change_address = request['changeAddress'] # type: Optional[Address] + depth = request['depth'] # type: int + inputs = request['inputs'] # type: Optional[List[Address]] + min_weight_magnitude = request['minWeightMagnitude'] # type: int + seed = request['seed'] # type: Seed + transfers = request['transfers'] # type: List[ProposedTransaction] + reference = request['reference'] # type: Optional[TransactionHash] + security_level = request['securityLevel'] # int + + pt_response = PrepareTransferCommand(self.adapter)( + changeAddress=change_address, + inputs=inputs, + seed=seed, + transfers=transfers, + securityLevel=security_level, + ) + + st_response = SendTrytesCommand(self.adapter)( + depth=depth, + minWeightMagnitude=min_weight_magnitude, + trytes=pt_response['trytes'], + reference=reference, + ) + + return { + 'bundle': Bundle.from_tryte_strings(st_response['trytes']), + } class SendTransferRequestFilter(RequestFilter): - def __init__(self): - super(SendTransferRequestFilter, self).__init__( - { - # Required parameters. - '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. - 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), - - 'transfers': ( - f.Required - | f.Array - | f.FilterRepeater(f.Required | f.Type(ProposedTransaction)) - ), - - # Optional parameters. - 'changeAddress': Trytes(result_type=Address), - 'securityLevel': SecurityLevel, - - # Note that ``inputs`` is allowed to be an empty array. - 'inputs': - f.Array | f.FilterRepeater(f.Required | Trytes(result_type=Address)), - - 'reference': Trytes(result_type=TransactionHash), - }, - - allow_missing_keys = { - 'changeAddress', - 'inputs', - 'reference', - 'securityLevel', - }, - ) + def __init__(self): + super(SendTransferRequestFilter, self).__init__( + { + # Required parameters. + '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. + 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), + + 'transfers': + f.Required | f.Array | f.FilterRepeater( + f.Required | f.Type(ProposedTransaction), + ), + + # Optional parameters. + 'changeAddress': Trytes(result_type=Address), + 'securityLevel': SecurityLevel, + + # Note that ``inputs`` is allowed to be an empty array. + 'inputs': + f.Array | f.FilterRepeater(f.Required | Trytes(Address)), + + 'reference': Trytes(TransactionHash), + }, + + allow_missing_keys={ + 'changeAddress', + 'inputs', + 'reference', + 'securityLevel', + }, + ) diff --git a/iota/commands/extended/send_trytes.py b/iota/commands/extended/send_trytes.py index 1b43d84..7e54d38 100644 --- a/iota/commands/extended/send_trytes.py +++ b/iota/commands/extended/send_trytes.py @@ -1,85 +1,86 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, Optional import filters as f -from iota import TransactionTrytes, TryteString, TransactionHash + +from iota import TransactionHash, TransactionTrytes, TryteString 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 + GetTransactionsToApproveCommand from iota.commands.extended.broadcast_and_store import BroadcastAndStoreCommand from iota.filters import Trytes __all__ = [ - 'SendTrytesCommand', + 'SendTrytesCommand', ] class SendTrytesCommand(FilterCommand): - """ - Executes `sendTrytes` extended API command. + """ + Executes `sendTrytes` extended API command. - See :py:meth:`iota.api.IotaApi.send_trytes` for more info. - """ - command = 'sendTrytes' + See :py:meth:`iota.api.IotaApi.send_trytes` for more info. + """ + command = 'sendTrytes' - def get_request_filter(self): - return SendTrytesRequestFilter() + def get_request_filter(self): + return SendTrytesRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass - def _execute(self, request): - depth = request['depth'] # type: int - min_weight_magnitude = request['minWeightMagnitude'] # type: int - trytes = request['trytes'] # type: List[TryteString] - reference = request['reference'] # type: Optional[TransactionHash] + def _execute(self, request): + depth = request['depth'] # type: int + min_weight_magnitude = request['minWeightMagnitude'] # type: int + trytes = request['trytes'] # type: List[TryteString] + reference = request['reference'] # type: Optional[TransactionHash] - # Call ``getTransactionsToApprove`` to locate trunk and branch - # transactions so that we can attach the bundle to the Tangle. - gta_response = GetTransactionsToApproveCommand(self.adapter)( - depth=depth, - reference=reference, - ) + # Call ``getTransactionsToApprove`` to locate trunk and branch + # transactions so that we can attach the bundle to the Tangle. + gta_response = GetTransactionsToApproveCommand(self.adapter)( + depth=depth, + reference=reference, + ) - att_response = AttachToTangleCommand(self.adapter)( - branchTransaction = gta_response.get('branchTransaction'), - trunkTransaction = gta_response.get('trunkTransaction'), + att_response = AttachToTangleCommand(self.adapter)( + branchTransaction=gta_response.get('branchTransaction'), + trunkTransaction=gta_response.get('trunkTransaction'), - minWeightMagnitude = min_weight_magnitude, - trytes = trytes, - ) + minWeightMagnitude=min_weight_magnitude, + trytes=trytes, + ) - # ``trytes`` now have POW! - trytes = att_response['trytes'] + # ``trytes`` now have POW! + trytes = att_response['trytes'] - BroadcastAndStoreCommand(self.adapter)(trytes=trytes) + BroadcastAndStoreCommand(self.adapter)(trytes=trytes) - return { - 'trytes': trytes, - } + return { + 'trytes': trytes, + } class SendTrytesRequestFilter(RequestFilter): - def __init__(self): - super(SendTrytesRequestFilter, self).__init__({ - 'depth': f.Required | f.Type(int) | f.Min(1), + def __init__(self): + super(SendTrytesRequestFilter, self).__init__({ + 'depth': f.Required | f.Type(int) | f.Min(1), - 'trytes': - f.Required - | f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=TransactionTrytes)), + 'trytes': + f.Required | f.Array | f.FilterRepeater( + f.Required | Trytes(TransactionTrytes), + ), - # Loosely-validated; testnet nodes require a different value than - # mainnet. - 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), + # Loosely-validated; testnet nodes require a different value + # than mainnet. + 'minWeightMagnitude': f.Required | f.Type(int) | f.Min(1), - 'reference': Trytes(result_type=TransactionHash), - }, + 'reference': Trytes(TransactionHash), + }, - allow_missing_keys = { - 'reference', - }) + allow_missing_keys={ + 'reference', + }) diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index a3a2807..740ec43 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -1,17 +1,17 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Generator, Iterable, List, Optional, Tuple from iota import Address, Bundle, Transaction, \ - TransactionHash + TransactionHash from iota.adapter import BaseAdapter 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 + GetLatestInclusionCommand from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed @@ -27,114 +27,119 @@ def find_transaction_objects(adapter, **kwargs): hashes = ft_response['hashes'] if hashes: - gt_response = GetTrytesCommand(adapter)(hashes=hashes) + gt_response = GetTrytesCommand(adapter)(hashes=hashes) - return list(map( - Transaction.from_tryte_string, - gt_response.get('trytes') or [], - )) # type: List[Transaction] + return list(map( + Transaction.from_tryte_string, + gt_response.get('trytes') or [], + )) # type: List[Transaction] return [] -def iter_used_addresses(adapter, seed, start, security_level=None): - # type: (BaseAdapter, Seed, int, Optional[int]) -> Generator[Tuple[Address, List[TransactionHash]]] - """ - Scans the Tangle for used addresses. +def iter_used_addresses( + adapter, # type: BaseAdapter + seed, # type: Seed + start, # type: int + security_level=None, # type: Optional[int] +): + # type: (...) -> Generator[Tuple[Address, List[TransactionHash]], None, None] + """ + Scans the Tangle for used addresses. - This is basically the opposite of invoking ``getNewAddresses`` with - ``stop=None``. - """ - if security_level is None: - security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL + This is basically the opposite of invoking ``getNewAddresses`` with + ``stop=None``. + """ + if security_level is None: + security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL - ft_command = FindTransactionsCommand(adapter) + ft_command = FindTransactionsCommand(adapter) - for addy in AddressGenerator(seed, security_level=security_level).create_iterator(start): - ft_response = ft_command(addresses=[addy]) + for addy in AddressGenerator(seed, security_level).create_iterator(start): + ft_response = ft_command(addresses=[addy]) - if ft_response['hashes']: - yield addy, ft_response['hashes'] - else: - break + if ft_response['hashes']: + yield addy, ft_response['hashes'] + else: + break - # Reset the command so that we can call it again. - ft_command.reset() + # Reset the command so that we can call it again. + ft_command.reset() def get_bundles_from_transaction_hashes( - adapter, - transaction_hashes, - inclusion_states, + adapter, + transaction_hashes, + inclusion_states, ): - # type: (BaseAdapter, Iterable[TransactionHash], bool) -> List[Bundle] - """ - Given a set of transaction hashes, returns the corresponding bundles, - sorted by tail transaction timestamp. - """ - transaction_hashes = list(transaction_hashes) - if not transaction_hashes: - return [] - - my_bundles = [] # type: List[Bundle] - - # Sort transactions into tail and non-tail. - tail_transaction_hashes = set() - non_tail_bundle_hashes = set() - - gt_response = GetTrytesCommand(adapter)(hashes=transaction_hashes) - all_transactions = list(map( - Transaction.from_tryte_string, - gt_response['trytes'], - )) # type: List[Transaction] - - for txn in all_transactions: - if txn.is_tail: - tail_transaction_hashes.add(txn.hash) - else: - # Capture the bundle ID instead of the transaction hash so that - # we can query the node to find the tail transaction for that - # bundle. - non_tail_bundle_hashes.add(txn.bundle_hash) - - if non_tail_bundle_hashes: - for txn in find_transaction_objects( - adapter = adapter, - bundles = list(non_tail_bundle_hashes) - ): - if txn.is_tail: - if txn.hash not in tail_transaction_hashes: - all_transactions.append(txn) - tail_transaction_hashes.add(txn.hash) - - # Filter out all non-tail transactions. - tail_transactions = [ - txn - for txn in all_transactions - if txn.hash in tail_transaction_hashes - ] - - # Attach inclusion states, if requested. - if inclusion_states: - gli_response = GetLatestInclusionCommand(adapter)( - hashes = list(tail_transaction_hashes), - ) + # type: (BaseAdapter, Iterable[TransactionHash], bool) -> List[Bundle] + """ + Given a set of transaction hashes, returns the corresponding bundles, + sorted by tail transaction timestamp. + """ + transaction_hashes = list(transaction_hashes) + if not transaction_hashes: + return [] - for txn in tail_transactions: - txn.is_confirmed = gli_response['states'].get(txn.hash) + my_bundles = [] # type: List[Bundle] - # Find the bundles for each transaction. - for txn in tail_transactions: - gb_response = GetBundlesCommand(adapter)(transaction=txn.hash) - txn_bundles = gb_response['bundles'] # type: List[Bundle] + # Sort transactions into tail and non-tail. + tail_transaction_hashes = set() + non_tail_bundle_hashes = set() + gt_response = GetTrytesCommand(adapter)(hashes=transaction_hashes) + all_transactions = list(map( + Transaction.from_tryte_string, + gt_response['trytes'], + )) # type: List[Transaction] + + for txn in all_transactions: + if txn.is_tail: + tail_transaction_hashes.add(txn.hash) + else: + # Capture the bundle ID instead of the transaction hash so + # that we can query the node to find the tail transaction + # for that bundle. + non_tail_bundle_hashes.add(txn.bundle_hash) + + if non_tail_bundle_hashes: + for txn in find_transaction_objects( + adapter=adapter, + bundles=list(non_tail_bundle_hashes), + ): + if txn.is_tail: + if txn.hash not in tail_transaction_hashes: + all_transactions.append(txn) + tail_transaction_hashes.add(txn.hash) + + # Filter out all non-tail transactions. + tail_transactions = [ + txn + for txn in all_transactions + if txn.hash in tail_transaction_hashes + ] + + # Attach inclusion states, if requested. if inclusion_states: - for bundle in txn_bundles: - bundle.is_confirmed = txn.is_confirmed + gli_response = GetLatestInclusionCommand(adapter)( + hashes=list(tail_transaction_hashes), + ) + + for txn in tail_transactions: + txn.is_confirmed = gli_response['states'].get(txn.hash) + + # Find the bundles for each transaction. + for txn in tail_transactions: + gb_response = GetBundlesCommand(adapter)(transaction=txn.hash) + txn_bundles = gb_response['bundles'] # type: List[Bundle] + + if inclusion_states: + for bundle in txn_bundles: + bundle.is_confirmed = txn.is_confirmed - my_bundles.extend(txn_bundles) + my_bundles.extend(txn_bundles) - return list(sorted( - my_bundles, - key = lambda bundle_: bundle_.tail_transaction.timestamp, - )) + return list(sorted( + my_bundles, + key=lambda bundle_: bundle_.tail_transaction.timestamp, + )) From 49e7b8e9b883dcc215cb56653395d0bf84612687 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 14 Jun 2018 08:42:24 +1200 Subject: [PATCH 09/32] [#145] Reformat kerl package for PEP-8. --- iota/crypto/kerl/__init__.py | 2 +- iota/crypto/kerl/conv.py | 77 ++++++++------ iota/crypto/kerl/pykerl.py | 195 ++++++++++++++++++----------------- 3 files changed, 145 insertions(+), 129 deletions(-) diff --git a/iota/crypto/kerl/__init__.py b/iota/crypto/kerl/__init__.py index 489aceb..094b795 100644 --- a/iota/crypto/kerl/__init__.py +++ b/iota/crypto/kerl/__init__.py @@ -1,5 +1,5 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from .pykerl import * diff --git a/iota/crypto/kerl/conv.py b/iota/crypto/kerl/conv.py index 8d54888..5541fa1 100644 --- a/iota/crypto/kerl/conv.py +++ b/iota/crypto/kerl/conv.py @@ -1,44 +1,44 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals - + unicode_literals BYTE_HASH_LENGTH = 48 TRIT_HASH_LENGTH = 243 tryte_table = { - '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 - '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 - } + '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 + '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 +} # Invert for trit -> tryte lookup trit_table = {tuple(v): k for k, v in tryte_table.items()} + def trytes_to_trits(trytes): trits = [] for tryte in trytes: @@ -46,6 +46,7 @@ def trytes_to_trits(trytes): return trits + def trits_to_trytes(trits): trytes = [] trits_chunks = [trits[i:i + 3] for i in range(0, len(trits), 3)] @@ -55,16 +56,19 @@ def trits_to_trytes(trits): return ''.join(trytes) + def convertToTrits(bytes_k): bigInt = convertBytesToBigInt(bytes_k) trits = convertBigintToBase(bigInt, 3, TRIT_HASH_LENGTH) return trits + def convertToBytes(trits): bigInt = convertBaseToBigint(trits, 3) bytes_k = convertBigintToBytes(bigInt) return bytes_k + def convertBytesToBigInt(ba): # copy of array bytesArray = list(map(lambda x: x, ba)) @@ -93,8 +97,10 @@ def convertBigintToBytes(big): range(48)] # big endian and balanced - bytesArray = list(map(lambda x: (x if x <= 0x7F else x - 0x100), - reversed(bytesArrayTemp))) + bytesArray = list(map( + lambda x: (x if x <= 0x7F else x - 0x100), + reversed(bytesArrayTemp) + )) if big < 0: # 1-compliment @@ -109,6 +115,7 @@ def convertBigintToBytes(big): return bytesArray + def convertBaseToBigint(array, base): bigint = 0 @@ -117,13 +124,14 @@ def convertBaseToBigint(array, base): return bigint + def convertBigintToBase(bigInt, base, length): result = [] is_negative = bigInt < 0 quotient = abs(bigInt) - MAX = (base-1) // 2 + MAX = (base - 1) // 2 if is_negative: MAX = base // 2 @@ -142,9 +150,10 @@ def convertBigintToBase(bigInt, base, length): return result + def convert_sign(byte): """ - Convert between signed and unsigned bytes + Convert between signed and unsigned bytes. """ if byte < 0: return 256 + byte diff --git a/iota/crypto/kerl/pykerl.py b/iota/crypto/kerl/pykerl.py index ea4187e..86edcb5 100644 --- a/iota/crypto/kerl/pykerl.py +++ b/iota/crypto/kerl/pykerl.py @@ -1,140 +1,147 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals + +from typing import MutableSequence, Optional from sha3 import keccak_384 from six import PY2 -from typing import MutableSequence, Optional from iota.crypto.kerl import conv from iota.exceptions import with_context __all__ = [ - 'Kerl', + 'Kerl', ] BYTE_HASH_LENGTH = 48 TRIT_HASH_LENGTH = 243 -class Kerl(object): - k = None # type: keccak_384 - - def __init__(self): - self.reset() - def absorb(self, trits, offset=0, length=None): - # type: (MutableSequence[int], int, Optional[int]) -> None - """ - Absorb trits into the sponge from a buffer. +class Kerl(object): + k = None # type: keccak_384 + + def __init__(self): + self.reset() + + def absorb(self, trits, offset=0, length=None): + # type: (MutableSequence[int], int, Optional[int]) -> None + """ + Absorb trits into the sponge from a buffer. - :param trits: - Buffer that contains the trits to absorb. + :param trits: + Buffer that contains the trits to absorb. - :param offset: - Starting offset in ``trits``. + :param offset: + Starting offset in ``trits``. - :param length: - Number of trits to absorb. Defaults to ``len(trits)``. - """ - # Pad input if necessary, so that it can be divided evenly into - # hashes. - # Note that this operation creates a COPY of ``trits``; the - # incoming buffer is not modified! - pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) - trits += [0] * (TRIT_HASH_LENGTH - pad) + :param length: + Number of trits to absorb. Defaults to ``len(trits)``. + """ + # Pad input if necessary, so that it can be divided evenly into + # hashes. + # Note that this operation creates a COPY of ``trits``; the + # incoming buffer is not modified! + pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) + trits += [0] * (TRIT_HASH_LENGTH - pad) - if length is None: - length = len(trits) + if length is None: + length = len(trits) - if length < 1: - raise with_context( - exc = ValueError('Invalid length passed to ``absorb``.'), + if length < 1: + raise with_context( + exc=ValueError('Invalid length passed to ``absorb``.'), - context = { - 'trits': trits, - 'offset': offset, - 'length': length, - }, - ) + context={ + 'trits': trits, + 'offset': offset, + 'length': length, + }, + ) - while offset < length: - stop = min(offset + TRIT_HASH_LENGTH, length) + while offset < length: + stop = min(offset + TRIT_HASH_LENGTH, length) - # If we're copying over a full chunk, zero last trit - if stop - offset == TRIT_HASH_LENGTH: - trits[stop - 1] = 0 + # If we're copying over a full chunk, zero last trit. + if stop - offset == TRIT_HASH_LENGTH: + trits[stop - 1] = 0 - signed_nums = conv.convertToBytes(trits[offset:stop]) + signed_nums = conv.convertToBytes(trits[offset:stop]) - # Convert signed bytes into their equivalent unsigned representation - # In order to use Python's built-in bytes type - unsigned_bytes = bytearray(conv.convert_sign(b) for b in signed_nums) + # Convert signed bytes into their equivalent unsigned + # representation, in order to use Python's built-in bytes + # type. + unsigned_bytes = bytearray( + conv.convert_sign(b) for b in signed_nums + ) - self.k.update(unsigned_bytes) + self.k.update(unsigned_bytes) - offset += TRIT_HASH_LENGTH + offset += TRIT_HASH_LENGTH - def squeeze(self, trits, offset=0, length=None): - # type: (MutableSequence[int], int, Optional[int]) -> None - """ - Squeeze trits from the sponge into a buffer. + def squeeze(self, trits, offset=0, length=None): + # type: (MutableSequence[int], int, Optional[int]) -> None + """ + Squeeze trits from the sponge into a buffer. - :param trits: - Buffer that will hold the squeezed trits. + :param trits: + Buffer that will hold the squeezed trits. - IMPORTANT: If ``trits`` is too small, it will be extended! + IMPORTANT: If ``trits`` is too small, it will be extended! - :param offset: - Starting offset in ``trits``. + :param offset: + Starting offset in ``trits``. - :param length: - Number of trits to squeeze from the sponge. + :param length: + Number of trits to squeeze from the sponge. - If not specified, defaults to :py:data:`TRIT_HASH_LENGTH` (i.e., - by default, we will try to squeeze exactly 1 hash). - """ - # Pad input if necessary, so that it can be divided evenly into - # hashes. - pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) - trits += [0] * (TRIT_HASH_LENGTH - pad) + If not specified, defaults to :py:data:`TRIT_HASH_LENGTH` + (i.e., by default, we will try to squeeze exactly 1 hash). + """ + # Pad input if necessary, so that it can be divided evenly into + # hashes. + pad = ((len(trits) % TRIT_HASH_LENGTH) or TRIT_HASH_LENGTH) + trits += [0] * (TRIT_HASH_LENGTH - pad) - if length is None: - # By default, we will try to squeeze one hash. - # Note that this is different than ``absorb``. - length = len(trits) or TRIT_HASH_LENGTH + if length is None: + # By default, we will try to squeeze one hash. + # Note that this is different than ``absorb``. + length = len(trits) or TRIT_HASH_LENGTH - if length < 1: - raise with_context( - exc = ValueError('Invalid length passed to ``squeeze``.'), + if length < 1: + raise with_context( + exc=ValueError('Invalid length passed to ``squeeze``.'), - context = { - 'trits': trits, - 'offset': offset, - 'length': length, - }, - ) + context={ + 'trits': trits, + 'offset': offset, + 'length': length, + }, + ) - while offset < length: - unsigned_hash = self.k.digest() + while offset < length: + unsigned_hash = self.k.digest() - if PY2: - unsigned_hash = map(ord, unsigned_hash) # type: ignore + if PY2: + unsigned_hash = map(ord, unsigned_hash) # type: ignore - signed_hash = [conv.convert_sign(b) for b in unsigned_hash] + signed_hash = [conv.convert_sign(b) for b in unsigned_hash] - trits_from_hash = conv.convertToTrits(signed_hash) - trits_from_hash[TRIT_HASH_LENGTH - 1] = 0 + trits_from_hash = conv.convertToTrits(signed_hash) + trits_from_hash[TRIT_HASH_LENGTH - 1] = 0 - stop = min(TRIT_HASH_LENGTH, length-offset) - trits[offset:offset+stop] = trits_from_hash[0:stop] + stop = min(TRIT_HASH_LENGTH, length - offset) + trits[offset:offset + stop] = trits_from_hash[0:stop] - flipped_bytes = bytearray(conv.convert_sign(~b) for b in unsigned_hash) + flipped_bytes = bytearray( + conv.convert_sign(~b) for b in unsigned_hash + ) - # Reset internal state before feeding back in - self.reset() - self.k.update(flipped_bytes) + # Reset internal state before feeding back in. + self.reset() + self.k.update(flipped_bytes) - offset += TRIT_HASH_LENGTH + offset += TRIT_HASH_LENGTH - def reset(self): - self.k = keccak_384() + def reset(self): + self.k = keccak_384() From ba9951781f5bc71f8cc22474fa552ecd3646e5c6 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 14 Jun 2018 09:02:40 +1200 Subject: [PATCH 10/32] [#145] Reformat rest of crypto package for PEP-8. --- iota/crypto/__init__.py | 16 +- iota/crypto/addresses.py | 354 ++++++++++--------- iota/crypto/pycurl.py | 341 +++++++++--------- iota/crypto/signing.py | 724 ++++++++++++++++++++------------------- iota/crypto/types.py | 497 ++++++++++++++------------- 5 files changed, 986 insertions(+), 946 deletions(-) diff --git a/iota/crypto/__init__.py b/iota/crypto/__init__.py index 8338ae6..c76906f 100644 --- a/iota/crypto/__init__.py +++ b/iota/crypto/__init__.py @@ -1,17 +1,15 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals - + unicode_literals # Load curl library. # If a compiled c extension is available, we will prefer to load that; # otherwise fall back to pure-Python implementation. # https://pypi.python.org/pypi/PyOTA-CCurl try: - from ccurl import * + from ccurl import * except ImportError: - from .pycurl import * - + from .pycurl import * FRAGMENT_LENGTH = 2187 """ @@ -23,7 +21,7 @@ class SeedWarning(Warning): - """ - Warning for inappropriate seeds. - """ - pass + """ + Warning for insecure or otherwise inappropriate seeds. + """ + pass diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index a2a5ac6..072540b 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Generator, Iterable, List, MutableSequence @@ -11,184 +11,198 @@ from iota.exceptions import with_context __all__ = [ - 'AddressGenerator', + '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 (: - """ - DEFAULT_SECURITY_LEVEL = 2 - """ - Default 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, security_level=DEFAULT_SECURITY_LEVEL, checksum=False): - # type: (TrytesCompatible, int, bool) -> None - super(AddressGenerator, self).__init__() - - self.security_level = security_level - self.checksum = checksum - self.seed = Seed(seed) - - 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_iterator() - - 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). + Generates new addresses using a standard algorithm. - This is a one-time operation; if you want to create lots of - addresses across multiple contexts, consider invoking - :py:meth:`create_iterator` and sharing the resulting generator - object instead. + 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. - 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. - Must be >= 0. - - :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). + 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 (: """ - if count < 1: - raise with_context( - exc = ValueError('``count`` must be positive.'), - - context = { - 'start': start, - 'count': count, - 'step': step, - }, - ) - - if not step: - raise with_context( - exc = ValueError('``step`` must not be zero.'), - - context = { - 'start': start, - 'count': count, - 'step': step, - }, - ) - - generator = self.create_iterator(start, step) - - addresses = [] - for _ in range(count): - try: - next_addy = next(generator) - except StopIteration: - break - else: - addresses.append(next_addy) - - return addresses - - def create_iterator(self, start=0, step=1): - # type: (int, int) -> Generator[Address] + DEFAULT_SECURITY_LEVEL = 2 """ - Creates an iterator 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. + Default 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: - Warning: The generator may take awhile to advance between - iterations if ``step`` is a large number! - """ - key_iterator = ( - KeyGenerator(self.seed) - .create_iterator(start, step, self.security_level) - ) - - while True: - yield self._generate_address(key_iterator) - - @staticmethod - def address_from_digest(digest): - # type: (Digest) -> Address - """ - Generates an address from a private key digest. - """ - address_trits = [0] * (Address.LEN * TRITS_PER_TRYTE) # type: MutableSequence[int] - - sponge = Kerl() - sponge.absorb(digest.as_trits()) - sponge.squeeze(address_trits) - - return Address.from_trits( - trits = address_trits, - - key_index = digest.key_index, - security_level = digest.security_level, - ) - - def _generate_address(self, key_iterator): - # type: (KeyIterator) -> Address - """ - Generates a new address. - - Used in the event of a cache miss. - """ - if self.checksum: - return self.address_from_digest(self._get_digest(key_iterator)).with_valid_checksum() - else: - return self.address_from_digest(self._get_digest(key_iterator)) - - @staticmethod - def _get_digest(key_iterator): - # type: (KeyIterator) -> Digest + - :py:meth:`iota.transaction.ProposedBundle.sign_inputs` + - :py:class:`iota.transaction.BundleValidator` """ - Extracts parameters for :py:meth:`address_from_digest`. - Split into a separate method so that it can be mocked during unit - tests. - """ - private_key = next(key_iterator) # type: PrivateKey - return private_key.get_digest() + def __init__( + self, + seed, + security_level=DEFAULT_SECURITY_LEVEL, + checksum=False, + ): + # type: (TrytesCompatible, int, bool) -> None + super(AddressGenerator, self).__init__() + + self.security_level = security_level + self.checksum = checksum + self.seed = Seed(seed) + + def __iter__(self): + # type: () -> Generator[Address, None, None] + """ + Returns a generator for creating new addresses, starting at + index 0 and potentially continuing on forever. + """ + return self.create_iterator() + + 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). + + This is a one-time operation; if you want to create lots of + addresses across multiple contexts, consider invoking + :py:meth:`create_iterator` 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. + Must be >= 0. + + :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 with_context( + exc=ValueError('``count`` must be positive.'), + + context={ + 'start': start, + 'count': count, + 'step': step, + }, + ) + + if not step: + raise with_context( + exc=ValueError('``step`` must not be zero.'), + + context={ + 'start': start, + 'count': count, + 'step': step, + }, + ) + + generator = self.create_iterator(start, step) + + addresses = [] + for _ in range(count): + try: + next_addy = next(generator) + except StopIteration: + break + else: + addresses.append(next_addy) + + return addresses + + def create_iterator(self, start=0, step=1): + # type: (int, int) -> Generator[Address] + """ + Creates an iterator 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_iterator = ( + KeyGenerator(self.seed).create_iterator( + start, + step, + self.security_level, + ) + ) + + while True: + yield self._generate_address(key_iterator) + + @staticmethod + def address_from_digest(digest): + # type: (Digest) -> Address + """ + Generates an address from a private key digest. + """ + address_trits = [0] * (Address.LEN * TRITS_PER_TRYTE) # type: List[int] + + sponge = Kerl() + sponge.absorb(digest.as_trits()) + sponge.squeeze(address_trits) + + return Address.from_trits( + trits=address_trits, + + key_index=digest.key_index, + security_level=digest.security_level, + ) + + def _generate_address(self, key_iterator): + # type: (KeyIterator) -> Address + """ + Generates a new address. + + Used in the event of a cache miss. + """ + if self.checksum: + return ( + self.address_from_digest( + digest=self._get_digest(key_iterator), + ).with_valid_checksum() + ) + else: + return self.address_from_digest(self._get_digest(key_iterator)) + + @staticmethod + def _get_digest(key_iterator): + # type: (KeyIterator) -> Digest + """ + Extracts parameters for :py:meth:`address_from_digest`. + + Split into a separate method so that it can be mocked during + unit tests. + """ + private_key = next(key_iterator) # type: PrivateKey + return private_key.get_digest() diff --git a/iota/crypto/pycurl.py b/iota/crypto/pycurl.py index 1748175..48074c2 100644 --- a/iota/crypto/pycurl.py +++ b/iota/crypto/pycurl.py @@ -1,17 +1,16 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, MutableSequence, Optional, Sequence from iota.exceptions import with_context __all__ = [ - 'Curl', - 'HASH_LENGTH', + 'Curl', + 'HASH_LENGTH', ] - HASH_LENGTH = 243 """ Number of trits in a hash. @@ -44,172 +43,176 @@ class Curl(object): - """ - Python implementation of Curl. - - **IMPORTANT: Not thread-safe!** - """ - def __init__(self): - # type: (Optional[Sequence[int]]) -> None - self.reset() - - # noinspection PyAttributeOutsideInit - def reset(self): - # type: () -> None - """ - Resets internal state. - """ - self._state = [0] * STATE_LENGTH # type: List[int] - - def absorb(self, trits, offset=0, length=None): - # type: (Sequence[int], Optional[int], Optional[int]) -> None """ - Absorb trits into the sponge. - - :param trits: - Sequence of trits to absorb. - - :param offset: - Starting offset in ``trits``. + Python implementation of Curl. - :param length: - Number of trits to absorb. Defaults to ``len(trits)``. + **IMPORTANT: Not thread-safe!** """ - pad = ((len(trits) % HASH_LENGTH) or HASH_LENGTH) - trits += [0] * (HASH_LENGTH - pad) - - if length is None: - length = len(trits) - - if length < 1: - raise with_context( - exc = ValueError('Invalid length passed to ``absorb``.'), - context = { - 'trits': trits, - 'offset': offset, - 'length': length, - }, - ) - - # 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) - - # - # 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, 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. - self._transform() - - # Move on to the next hash. - offset += HASH_LENGTH - - def squeeze(self, trits, offset=0, length=HASH_LENGTH): - # type: (MutableSequence[int], Optional[int], Optional[int]) -> None - """ - Squeeze trits from the sponge. - - :param trits: - Sequence that the squeezed trits will be copied to. - Note: this object will be modified! - - :param offset: - Starting offset in ``trits``. - :param length: - Number of trits to squeeze, default to ``HASH_LENGTH`` - """ - # - # 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, only the first hash of the state is "public", so we - # can simplify the implementation somewhat. - # - - # Ensure length can be mod by HASH_LENGTH - if length % HASH_LENGTH != 0: - raise with_context( - exc = ValueError('Invalid length passed to ``squeeze`.'), - context = { - 'trits': trits, - 'offset': offset, - 'length': length, - }) - - # Ensure that ``trits`` can hold at least one hash worth of trits. - trits.extend([0] * max(0, length - len(trits))) - - # Check trits with offset can handle hash length - if len(trits) - offset < HASH_LENGTH: - raise with_context( - exc = ValueError('Invalid offset passed to ``squeeze``.'), - context = { - 'trits': trits, - 'offset': offset, - 'length': length - }, - ) - - while length >= HASH_LENGTH: - # Copy exactly one hash. - trits[offset:offset + HASH_LENGTH] = self._state[0:HASH_LENGTH] - - # One hash worth of trits copied; now transform. - self._transform() - - offset += HASH_LENGTH - length -= HASH_LENGTH - - def _transform(self): - # type: () -> None - """ - Transforms internal state. - """ - # Copy some values locally so we can avoid global lookups in the - # inner loop. - # - # References: - # - https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Local_Variables - state_length = STATE_LENGTH - truth_table = TRUTH_TABLE - - # Operate on a copy of ``self._state`` to eliminate dot lookups in - # the inner loop. - # - # References: - # - https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Avoiding_dots... - # - http://stackoverflow.com/a/2612990/ - prev_state = self._state[:] - new_state = prev_state[:] - - # Note: This code looks significantly different from the C - # implementation because it has been optimized to limit the number - # of list item lookups (these are relatively slow in Python). - index = 0 - for _ in range(NUMBER_OF_ROUNDS): - prev_trit = prev_state[index] - - for pos in range(state_length): - index += (364 if index < 365 else -365) - - new_trit = prev_state[index] - - new_state[pos] = truth_table[prev_trit + (3 * new_trit) + 4] - - prev_trit = new_trit - - prev_state = new_state - new_state = new_state[:] - - self._state = new_state + def __init__(self): + # type: (Optional[Sequence[int]]) -> None + self.reset() + + # noinspection PyAttributeOutsideInit + def reset(self): + # type: () -> None + """ + Resets internal state. + """ + self._state = [0] * STATE_LENGTH # type: List[int] + + def absorb(self, trits, offset=0, length=None): + # type: (Sequence[int], Optional[int], Optional[int]) -> None + """ + Absorb trits into the sponge. + + :param trits: + Sequence of trits to absorb. + + :param offset: + Starting offset in ``trits``. + + :param length: + Number of trits to absorb. Defaults to ``len(trits)``. + """ + pad = ((len(trits) % HASH_LENGTH) or HASH_LENGTH) + trits += [0] * (HASH_LENGTH - pad) + + if length is None: + length = len(trits) + + if length < 1: + raise with_context( + exc=ValueError('Invalid length passed to ``absorb``.'), + + context={ + 'trits': trits, + 'offset': offset, + 'length': length, + }, + ) + + # 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) + + # 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, 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. + self._transform() + + # Move on to the next hash. + offset += HASH_LENGTH + + def squeeze(self, trits, offset=0, length=HASH_LENGTH): + # type: (MutableSequence[int], Optional[int], Optional[int]) -> None + """ + Squeeze trits from the sponge. + + :param trits: + Sequence that the squeezed trits will be copied to. + Note: this object will be modified! + + :param offset: + Starting offset in ``trits``. + + :param length: + Number of trits to squeeze, default to ``HASH_LENGTH`` + """ + # 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, only the first hash of the state is "public", so we + # can simplify the implementation somewhat. + + # Ensure length can be mod by HASH_LENGTH + if length % HASH_LENGTH != 0: + raise with_context( + exc=ValueError('Invalid length passed to ``squeeze`.'), + + context={ + 'trits': trits, + 'offset': offset, + 'length': length, + }) + + # Ensure that ``trits`` can hold at least one hash worth of + # trits. + trits.extend([0] * max(0, length - len(trits))) + + # Check trits with offset can handle hash length + if len(trits) - offset < HASH_LENGTH: + raise with_context( + exc=ValueError('Invalid offset passed to ``squeeze``.'), + + context={ + 'trits': trits, + 'offset': offset, + 'length': length + }, + ) + + while length >= HASH_LENGTH: + # Copy exactly one hash. + trits[offset:offset + HASH_LENGTH] = self._state[0:HASH_LENGTH] + + # One hash worth of trits copied; now transform. + self._transform() + + offset += HASH_LENGTH + length -= HASH_LENGTH + + def _transform(self): + # type: () -> None + """ + Transforms internal state. + """ + # Copy some values locally so we can avoid global lookups in the + # inner loop. + # + # References: + # + # - https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Local_Variables + state_length = STATE_LENGTH + truth_table = TRUTH_TABLE + + # Operate on a copy of ``self._state`` to eliminate dot lookups + # in the inner loop. + # + # References: + # + # - https://wiki.python.org/moin/PythonSpeed/PerformanceTips#Avoiding_dots... + # - http://stackoverflow.com/a/2612990/ + prev_state = self._state[:] + new_state = prev_state[:] + + # Note: This code looks significantly different from the C + # implementation because it has been optimized to limit the + # number of list item lookups (these are relatively slow in + # Python). + index = 0 + for _ in range(NUMBER_OF_ROUNDS): + prev_trit = prev_state[index] + + for pos in range(state_length): + index += (364 if index < 365 else -365) + + new_trit = prev_state[index] + + new_state[pos] = truth_table[prev_trit + (3 * new_trit) + 4] + + prev_trit = new_trit + + prev_state = new_state + new_state = new_state[:] + + self._state = new_state diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index d23548e..da01ad4 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -1,8 +1,8 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals -from typing import Iterator, List, MutableSequence, Sequence, Tuple +from typing import Iterator, List, Sequence from six import PY2 @@ -14,431 +14,445 @@ from iota.trits import add_trits, trits_from_int __all__ = [ - 'KeyGenerator', - 'SignatureFragmentGenerator', - 'validate_signature_fragments', + 'KeyGenerator', + 'SignatureFragmentGenerator', + '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() + # type: (Hash) -> List[List[int]] + """ + "Normalizes" a hash, converting it into a sequence of integers + (not trits!) suitable for use in signature generation/validation. - chunk_size = 27 + 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() - for i in range(Hash.LEN // chunk_size): - start = i * chunk_size - stop = start + chunk_size + chunk_size = 27 - chunk = source[start:stop] - chunk_sum = sum(chunk) + for i in range(Hash.LEN // chunk_size): + start = i * chunk_size + stop = start + chunk_size - while chunk_sum > 0: - chunk_sum -= 1 - for j in range(chunk_size): - if chunk[j] > -13: - chunk[j] -= 1 - break + 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 + while chunk_sum < 0: + chunk_sum += 1 + for j in range(chunk_size): + if chunk[j] < 13: + chunk[j] += 1 + break - normalized.append(chunk) + normalized.append(chunk) - return normalized + return normalized class KeyGenerator(object): - """ - Generates signing keys for messages. - """ - def __init__(self, seed): - # type: (TrytesCompatible) -> None - super(KeyGenerator, self).__init__() + """ + Generates signing keys for messages. + """ + + def __init__(self, seed): + # type: (TrytesCompatible) -> None + super(KeyGenerator, self).__init__() + + self.seed = Seed(seed) + + def get_key(self, index, iterations): + # type: (int, int) -> PrivateKey + """ + Generates a single key. + + :param index: + The key index. + + :param iterations: + Number of transform iterations to apply to the key, also + known as security level. + Must be >= 1. + + Increasing this value makes key generation slower, but more + resistant to brute-forcing. + """ + return ( + self.get_keys( + start=index, + count=1, + step=1, + iterations=iterations, + )[0] + ) + + def get_key_for(self, address): + """ + Generates the key associated with the specified address. + + Note that this method will generate the wrong key if the input + address was generated from a different key! + """ + return self.get_key( + index=address.key_index, + iterations=address.security_level, + ) + + def get_keys(self, start, count=1, step=1, iterations=1): + # type: (int, int, int, int) -> List[PrivateKey] + """ + 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_iterator` 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. + Must be >= 0. + + :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, also + known as security level. + 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. + + The returned list will contain ``count`` keys, except when + ``step * count < start`` (only applies when ``step`` is + negative). + """ + if count < 1: + raise with_context( + exc=ValueError('``count`` must be positive.'), + + context={ + 'start': start, + 'count': count, + 'step': step, + 'iterations': iterations, + }, + ) + + if not step: + raise with_context( + exc=ValueError('``step`` must not be zero.'), + + context={ + 'start': start, + 'count': count, + 'step': step, + 'iterations': iterations, + }, + ) + + iterator = self.create_iterator(start, step, iterations) + + keys = [] + for _ in range(count): + try: + next_key = next(iterator) + except StopIteration: + break + else: + keys.append(next_key) + + return keys + + def create_iterator(self, start=0, step=1, security_level=1): + # type: (int, int, int) -> KeyIterator + """ + 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 security_level: + 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 KeyIterator(self.seed, start, step, security_level) - self.seed = Seed(seed) - def get_key(self, index, iterations): - # type: (int, int) -> PrivateKey +class KeyIterator(Iterator[PrivateKey]): + """ + Creates PrivateKeys from a set of iteration parameters. """ - Generates a single key. - :param index: - The key index. + def __init__(self, seed, start, step, security_level): + # type: (Seed, int, int, int) -> None + super(KeyIterator, self).__init__() + + if start < 0: + raise with_context( + exc=ValueError('``start`` cannot be negative.'), + + context={ + 'start': start, + 'step': step, + 'security_level': security_level, + }, + ) - :param iterations: - Number of transform iterations to apply to the key, also known - as security level. - Must be >= 1. + if security_level < 1: + raise with_context( + exc=ValueError('``security_level`` must be >= 1.'), - Increasing this value makes key generation slower, but more - resistant to brute-forcing. - """ - return self.get_keys(start=index, count=1, step=1, iterations=iterations)[0] + context={ + 'start': start, + 'step': step, + 'security_level': security_level, + }, + ) - def get_key_for(self, address): - """ - Generates the key associated with the specified address. + # In order to work correctly, the seed must be padded so that it + # is a multiple of 81 trytes. + seed += b'9' * (Hash.LEN - ((len(seed) % Hash.LEN) or Hash.LEN)) - Note that this method will generate the wrong key if the input - address was generated from a different key! - """ - return self.get_key( - index = address.key_index, - iterations = address.security_level, - ) + self.security_level = security_level + self.seed_as_trits = seed.as_trits() + self.start = start + self.step = step - def get_keys(self, start, count=1, step=1, iterations=1): - # type: (int, int, int, int) -> List[PrivateKey] - """ - Generates and returns one or more keys at the specified index(es). + self.current = self.start - This is a one-time operation; if you want to create lots of keys - across multiple contexts, consider invoking - :py:meth:`create_iterator` and sharing the resulting generator - object instead. + self.fragment_length = FRAGMENT_LENGTH * TRITS_PER_TRYTE + self.hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN - Warning: This method may take awhile to run if the starting index - and/or the number of requested keys is a large number! + def __iter__(self): + # type: () -> KeyIterator + return self - :param start: - Starting index. - Must be >= 0. + def __next__(self): + # type: () -> PrivateKey + while self.current >= 0: + sponge = self._create_sponge(self.current) - :param count: - Number of keys to generate. - Must be > 0. + key = [0] * (self.fragment_length * self.security_level) + buffer = [0] * len(self.seed_as_trits) - :param step: - Number of indexes to advance after each key. - This may be any non-zero (positive or negative) integer. + for fragment_seq in range(self.security_level): + # Squeeze trits from the buffer and append them to the + # key, one hash at a time. + for hash_seq in range(self.hashes_per_fragment): + sponge.squeeze(buffer) - :param iterations: - Number of transform iterations to apply to each key, also known - as security level. - Must be >= 1. + key_start = ( + (fragment_seq * self.fragment_length) + + (hash_seq * HASH_LENGTH) + ) - Increasing this value makes key generation slower, but more - resistant to brute-forcing. + key_stop = key_start + HASH_LENGTH - :return: - Always returns a list, even if only one key is generated. + # Ensure we only capture one hash from the buffer, + # in case it is longer than that (i.e., if the seed + # is longer than 81 trytes). + key[key_start:key_stop] = buffer[0:HASH_LENGTH] - The returned list will contain ``count`` keys, except when - ``step * count < start`` (only applies when ``step`` is - negative). - """ - if count < 1: - raise with_context( - exc = ValueError('``count`` must be positive.'), - - context = { - 'start': start, - 'count': count, - 'step': step, - 'iterations': iterations, - }, - ) - - if not step: - raise with_context( - exc = ValueError('``step`` must not be zero.'), - - context = { - 'start': start, - 'count': count, - 'step': step, - 'iterations': iterations, - }, - ) - - iterator = self.create_iterator(start, step, iterations) - - keys = [] - for _ in range(count): - try: - next_key = next(iterator) - except StopIteration: - break - else: - keys.append(next_key) - - return keys - - def create_iterator(self, start=0, step=1, security_level=1): - # type: (int, int, int) -> KeyIterator - """ - Creates a generator that can be used to progressively generate new - keys. + private_key = PrivateKey.from_trits( + key_index=self.current, + security_level=self.security_level, + trits=key, + ) - :param start: - Starting index. + self.advance() - Warning: This method may take awhile to reset if ``start`` - is a large number! + return private_key - :param step: - Number of indexes to advance after each key. + if PY2: + next = __next__ - This value can be negative; the generator will exit if it - reaches an index < 0. + def advance(self): + """ + Advances the generator without creating a key. + """ + self.current += self.step - Warning: The generator may take awhile to advance between - iterations if ``step`` is a large number! + def _create_sponge(self, index): + # type: (int) -> Kerl + """ + Prepares the hash sponge for the generator. + """ + seed = self.seed_as_trits[:] - :param security_level: - Number of _transform iterations to apply to each key. - Must be >= 1. + sponge = Kerl() + sponge.absorb(add_trits(seed, trits_from_int(index))) - Increasing this value makes key generation slower, but more - resistant to brute-forcing. - """ - return KeyIterator(self.seed, start, step, security_level) + # Squeeze all of the trits out of the sponge and re-absorb them. + # Note that the sponge 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 -class KeyIterator(Iterator[PrivateKey]): - """ - Creates PrivateKeys from a set of iteration parameters. - """ - def __init__(self, seed, start, step, security_level): - # type: (Seed, int, int, int) -> None - super(KeyIterator, self).__init__() - - if start < 0: - raise with_context( - exc = ValueError('``start`` cannot be negative.'), - - context = { - 'start': start, - 'step': step, - 'security_level': security_level, - }, - ) - - if security_level < 1: - raise with_context( - exc = ValueError('``security_level`` must be >= 1.'), - - context = { - 'start': start, - 'step': step, - 'security_level': security_level, - }, - ) - - # In order to work correctly, the seed must be padded so that it is - # a multiple of 81 trytes. - seed += b'9' * (Hash.LEN - ((len(seed) % Hash.LEN) or Hash.LEN)) - - self.security_level = security_level - self.seed_as_trits = seed.as_trits() - self.start = start - self.step = step - - self.current = self.start - - self.fragment_length = FRAGMENT_LENGTH * TRITS_PER_TRYTE - self.hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN - - def __iter__(self): - # type: () -> KeyIterator - return self - - def __next__(self): - # type: () -> PrivateKey - while self.current >= 0: - sponge = self._create_sponge(self.current) - - key = [0] * (self.fragment_length * self.security_level) - buffer = [0] * len(self.seed_as_trits) - - for fragment_seq in range(self.security_level): - # Squeeze trits from the buffer and append them to the key, one - # hash at a time. - for hash_seq in range(self.hashes_per_fragment): - sponge.squeeze(buffer) - - key_start =\ - (fragment_seq * self.fragment_length) + (hash_seq * HASH_LENGTH) - - key_stop = key_start + HASH_LENGTH - - # Ensure we only capture one hash from the buffer, in case - # it is longer than that (i.e., if the seed is longer than 81 - # trytes). - key[key_start:key_stop] = buffer[0:HASH_LENGTH] - - private_key =\ - PrivateKey.from_trits( - key_index = self.current, - security_level = self.security_level, - trits = key, - ) # type: PrivateKey - - self.advance() - - return private_key - - if PY2: - next = __next__ - - def advance(self): - """ - Advances the generator without creating a key. +class SignatureFragmentGenerator(Iterator[TryteString]): """ - self.current += self.step + Used to generate signature fragments progressively. - def _create_sponge(self, index): - # type: (int) -> Kerl + Each instance can generate 1 signature per fragment in the private + key. """ - Prepares the hash sponge for the generator. - """ - seed = self.seed_as_trits[:] - sponge = Kerl() - sponge.absorb(add_trits(seed, trits_from_int(index))) + def __init__(self, private_key, hash_): + # type: (PrivateKey, Hash) -> None + super(SignatureFragmentGenerator, self).__init__() - # Squeeze all of the trits out of the sponge and re-absorb them. - # Note that the sponge 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) + self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) + self._iteration = -1 + self._normalized_hash = normalize(hash_) + self._sponge = Kerl() - return sponge + def __iter__(self): + # type: () -> SignatureFragmentGenerator + return self + def __len__(self): + # type: () -> int + """ + Returns the number of fragments this generator can create. -class SignatureFragmentGenerator(Iterator[TryteString]): - """ - Used to generate signature fragments progressively. - - Each instance can generate 1 signature per fragment in the private - key. - """ - def __init__(self, private_key, hash_): - # type: (PrivateKey, Hash) -> None - super(SignatureFragmentGenerator, self).__init__() - - self._key_chunks = private_key.iter_chunks(FRAGMENT_LENGTH) - self._iteration = -1 - self._normalized_hash = normalize(hash_) - self._sponge = Kerl() - - def __iter__(self): - # type: () -> SignatureFragmentGenerator - return self - - 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) - Note: This method always returns the same result, no matter how - many iterations have been completed. - """ - return len(self._key_chunks) - - def __next__(self): - # type: () -> TryteString - """ - Returns the next signature fragment. - """ - key_trytes = next(self._key_chunks) # type: TryteString - self._iteration += 1 + def __next__(self): + # type: () -> TryteString + """ + Returns the next signature fragment. + """ + key_trytes = next(self._key_chunks) # type: TryteString + self._iteration += 1 - # If the key is long enough, loop back around to the start. - normalized_chunk =\ - self._normalized_hash[self._iteration % len(self._normalized_hash)] + # If the key is long enough, loop back around to the start. + normalized_chunk = ( + self._normalized_hash[self._iteration % len(self._normalized_hash)] + ) - signature_fragment = 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 + # 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 - buffer = signature_fragment[hash_start:hash_end] # type: MutableSequence[int] + buffer = signature_fragment[hash_start:hash_end] # type: List[int] - for _ in range(13 - normalized_chunk[i]): - self._sponge.reset() - self._sponge.absorb(buffer) - self._sponge.squeeze(buffer) + for _ in range(13 - normalized_chunk[i]): + self._sponge.reset() + self._sponge.absorb(buffer) + self._sponge.squeeze(buffer) - signature_fragment[hash_start:hash_end] = buffer + signature_fragment[hash_start:hash_end] = buffer - return TryteString.from_trits(signature_fragment) + return TryteString.from_trits(signature_fragment) - if PY2: - next = __next__ + if PY2: + next = __next__ def validate_signature_fragments( - fragments, - hash_, - public_key, - sponge_type = Kerl, + fragments, + hash_, + public_key, + sponge_type=Kerl, ): - # type: (Sequence[TryteString], Hash, TryteString, type) -> bool - """ - Returns whether a sequence of signature fragments is valid. + # type: (Sequence[TryteString], Hash, TryteString, type) -> bool + """ + Returns whether a sequence of signature fragments is valid. - :param fragments: - Sequence of signature fragments (usually - :py:class:`iota.transaction.Fragment` instances). + :param fragments: + Sequence of signature fragments (usually + :py:class:`iota.transaction.Fragment` instances). - :param hash_: - Hash used to generate the signature fragments (usually a - :py:class:`iota.transaction.BundleHash` instance). + :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). + :param public_key: + The public key value used to verify the signature digest (usually a + :py:class:`iota.types.Address` instance). - :param sponge_type: - The class used to create the cryptographic sponge (i.e., Curl or Kerl). - """ - checksum = [0] * (HASH_LENGTH * len(fragments)) - normalized_hash = normalize(hash_) + :param sponge_type: + The class used to create the cryptographic sponge (i.e., Curl or Kerl). + """ + checksum = [0] * (HASH_LENGTH * len(fragments)) + normalized_hash = normalize(hash_) - for (i, fragment) in enumerate(fragments): # type: Tuple[int, TryteString] - outer_sponge = sponge_type() + for i, fragment in enumerate(fragments): + outer_sponge = sponge_type() - # If there are more than 3 iterations, loop back around to the - # start. - normalized_chunk = normalized_hash[i % len(normalized_hash)] + # 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] - buffer = hash_trytes.as_trits() # type: MutableSequence[int] - inner_sponge = sponge_type() + buffer = [] + for j, hash_trytes in enumerate(fragment.iter_chunks(Hash.LEN)): + buffer = hash_trytes.as_trits() # type: List[int] + inner_sponge = sponge_type() - # Note the sign flip compared to ``SignatureFragmentGenerator``. - for _ in range(13 + normalized_chunk[j]): - inner_sponge.reset() - inner_sponge.absorb(buffer) - inner_sponge.squeeze(buffer) + # Note the sign flip compared to + # :py;class:`SignatureFragmentGenerator`. + for _ in range(13 + normalized_chunk[j]): + inner_sponge.reset() + inner_sponge.absorb(buffer) + inner_sponge.squeeze(buffer) - outer_sponge.absorb(buffer) + outer_sponge.absorb(buffer) - outer_sponge.squeeze(buffer) - checksum[i*HASH_LENGTH:(i+1)*HASH_LENGTH] = buffer + outer_sponge.squeeze(buffer) + checksum[i * HASH_LENGTH:(i + 1) * HASH_LENGTH] = buffer - actual_public_key = [0] * HASH_LENGTH # type: MutableSequence[int] - addy_sponge = sponge_type() - addy_sponge.absorb(checksum) - addy_sponge.squeeze(actual_public_key) + actual_public_key = [0] * HASH_LENGTH + addy_sponge = sponge_type() + addy_sponge.absorb(checksum) + addy_sponge.squeeze(actual_public_key) - return actual_public_key == public_key.as_trits() + return actual_public_key == public_key.as_trits() diff --git a/iota/crypto/types.py b/iota/crypto/types.py index 0bf33c4..6b2664b 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -1,9 +1,9 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import warnings -from typing import MutableSequence, Optional, Tuple +from typing import Optional from iota.crypto import FRAGMENT_LENGTH, HASH_LENGTH, SeedWarning from iota.crypto.kerl import Kerl @@ -12,274 +12,285 @@ from iota.types import Hash, TryteString, TrytesCompatible __all__ = [ - 'Digest', - 'PrivateKey', - 'Seed', + 'Digest', + 'PrivateKey', + 'Seed', ] class Digest(TryteString): - """ - A private key digest. Basically the same thing as a regular - :py:class:`TryteString`, except that it (usually) has a key index - associated with it. - - Note: in a few cases (e.g., generating multisig addresses), a key - index is not necessary/available. - """ - def __init__(self, trytes, key_index=None): - # type: (TrytesCompatible, Optional[int]) -> None - super(Digest, self).__init__(trytes) - - # A digest is a series of hashes; its length should reflect that. - if len(self) % Hash.LEN: - raise with_context( - exc = ValueError( - 'Length of {cls} values must be a multiple of {len} trytes.'.format( - cls = type(self).__name__, - len = Hash.LEN, - ), - ), - - context = { - 'trytes': trytes, - }, - ) - - self.key_index = key_index - - @property - def security_level(self): - # type: () -> int """ - Returns the number of iterations that were used to generate this - digest (also known as "security level"). + A private key digest. Basically the same thing as a regular + :py:class:`TryteString`, except that it (usually) has a key index + associated with it. + + Note: in a few cases (e.g., generating multisig addresses), a key + index is not necessary/available. """ - return len(self) // Hash.LEN - def as_json_compatible(self): - # type: () -> dict - return { - 'trytes': self._trytes.decode('ascii'), - 'key_index': self.key_index, - } + def __init__(self, trytes, key_index=None): + # type: (TrytesCompatible, Optional[int]) -> None + super(Digest, self).__init__(trytes) + + # A digest is a series of hashes; its length should reflect + # that. + if len(self) % Hash.LEN: + raise with_context( + exc=ValueError( + 'Length of {cls} values ' + 'must be a multiple of {len} trytes.'.format( + cls=type(self).__name__, + len=Hash.LEN, + ), + ), + + context={ + 'trytes': trytes, + }, + ) + + self.key_index = key_index + + @property + def security_level(self): + # type: () -> int + """ + Returns the number of iterations that were used to generate this + digest (also known as "security level"). + """ + return len(self) // Hash.LEN + + def as_json_compatible(self): + # type: () -> dict + return { + 'trytes': self._trytes.decode('ascii'), + 'key_index': self.key_index, + } class Seed(TryteString): - """ - A TryteString that acts as a seed for crypto functions. - - Note: This class is identical to :py:class:`TryteString`, but it has - a distinct type so that seeds can be identified in Python code. - - IMPORTANT: For maximum security, a seed must be EXACTLY 81 trytes! - - WARNINGS: - .. warning:: :py:class:`SeedWarning` if seed has inappropriate length. - - References: - - https://forum.iota.org/t/why-arent-seeds-longer-than-81-trytes-more-secure/1278 - """ - - def __init__(self, trytes=None): - # type: (Optional[TrytesCompatible]) -> None - if trytes and len(trytes) > Hash.LEN: - warnings.warn("Seed has inappropriate length! " - "(https://forum.iota.org/t/why-arent-seeds-longer-than-81-trytes-more-secure/1278)", - category=SeedWarning) - - super(Seed, self).__init__(trytes) - - @classmethod - def random(cls, length=Hash.LEN): - """ - Generates a random seed using a CSPRNG. - - :param length: - Length of seed, in trytes. - For maximum security, this should always be set to 81. - - References: - - https://forum.iota.org/t/why-arent-seeds-longer-than-81-trytes-more-secure/1278 """ - return super(Seed, cls).random(length) + A TryteString that acts as a seed for crypto functions. + Note: This class is identical to :py:class:`TryteString`, but it has + a distinct type so that seeds can be identified in Python code. -class PrivateKey(TryteString): - """ - A TryteString that acts as a private key, e.g., for generating - message signatures, new addresses, etc. - """ - def __init__(self, trytes, key_index=None, security_level=None): - # type: (TrytesCompatible, Optional[int], Optional[int]) -> None - super(PrivateKey, self).__init__(trytes) - - 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 = FRAGMENT_LENGTH, - ), - ), - - context = { - 'trytes': self._trytes, - }, - ) - - self.key_index = key_index - self.security_level = security_level - - def as_json_compatible(self): - # type: () -> dict - return { - 'trytes': self._trytes.decode('ascii'), - 'key_index': self.key_index, - 'security_level': self.security_level, - } - - def get_digest(self): - # type: () -> Digest - """ - Generates the digest used to do the actual signing. + IMPORTANT: For maximum security, a seed must be EXACTLY 81 trytes! - Signing keys can have variable length and tend to be quite long, - which makes them not-well-suited for use in crypto algorithms. + References: - 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. + - https://iota.stackexchange.com/q/249 """ - hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN - - key_fragments = self.iter_chunks(FRAGMENT_LENGTH) - - # The digest will contain one hash per key fragment. - digest = [0] * HASH_LENGTH * len(key_fragments) - # Iterate over each fragment in the key. - for (i, fragment) in enumerate(key_fragments): # type: Tuple[int, TryteString] - fragment_trits = fragment.as_trits() + def __init__(self, trytes=None): + # type: (Optional[TrytesCompatible]) -> None + if trytes and len(trytes) > Hash.LEN: + warnings.warn( + message=( + "Seed has inappropriate length! " + "(see https://iota.stackexchange.com/q/249 for more info)." + ), - key_fragment = [0] * FRAGMENT_LENGTH - hash_trits = [] + category=SeedWarning, + ) - # Within each fragment, iterate over one hash at a time. - 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] + super(Seed, self).__init__(trytes) - for k in range(26): - sponge = Kerl() - sponge.absorb(hash_trits) - sponge.squeeze(hash_trits) + @classmethod + def random(cls, length=Hash.LEN): + """ + Generates a random seed using a CSPRNG. - key_fragment[hash_start:hash_end] = hash_trits + :param length: + Length of seed, in trytes. - # - # After processing all of the hashes in the fragment, generate a - # final hash and append it to the digest. - # - # Note that we will do this once per fragment in the key, so the - # longer the key is, the longer the digest will be. - # - sponge = Kerl() - sponge.absorb(key_fragment) - sponge.squeeze(hash_trits) + For maximum security, this should always be set to 81, but + you can change it if you're 110% sure you know what you're + doing. - fragment_hash_start = i * HASH_LENGTH - fragment_hash_end = fragment_hash_start + HASH_LENGTH + See https://iota.stackexchange.com/q/249 for more info. + """ + return super(Seed, cls).random(length) - digest[fragment_hash_start:fragment_hash_end] = hash_trits - return Digest(TryteString.from_trits(digest), self.key_index) - - def sign_input_transactions(self, bundle, start_index): - # type: (Bundle, int) -> None +class PrivateKey(TryteString): """ - Signs the inputs starting at the specified index. - - :param bundle: - The bundle that contains the input transactions to sign. - - :param start_index: - The index of the first input transaction. - - If necessary, the resulting signature will be split across - subsequent transactions automatically. + A TryteString that acts as a private key, e.g., for generating + message signatures, new addresses, etc. """ - if not bundle.hash: - raise with_context( - exc = ValueError('Cannot sign inputs without a bundle hash!'), - - context = { - 'bundle': bundle, - 'key_index': self.key_index, - 'start_index': start_index, - }, - ) - - from iota.crypto.signing import SignatureFragmentGenerator - signature_fragment_generator = SignatureFragmentGenerator(self, bundle.hash) - - # We can only fit one signature fragment into each transaction, - # so we have to split the entire signature. - for j in range(self.security_level): - # Do lots of validation before we attempt to sign the - # transaction, and attach lots of context info to any exception. - # This method is likely to be invoked at a very low level in the - # application, so if anything goes wrong, we want to make sure - # it's as easy to troubleshoot as possible! - try: - txn = bundle[start_index+j] - except IndexError as e: - raise with_context( - exc = e, - - context = { - 'bundle': bundle, - 'key_index': self.key_index, - 'current_index': start_index + j, - }, - ) - - # Only inputs can be signed. - if txn.value > 0: - raise with_context( - exc = - ValueError( - 'Attempting to sign non-input transaction #{i} ' - '(value={value}).'.format( - i = txn.current_index, - value = txn.value, - ), - ), - - context = { - 'bundle': bundle, - 'key_index': self.key_index, - 'start_index': start_index, - }, - ) - - if txn.signature_message_fragment: - raise with_context( - exc = - ValueError( - 'Attempting to sign input transaction #{i}, ' - 'but it has a non-empty fragment (is it already signed?).'.format( - i = txn.current_index, - ), - ), - - context = { - 'bundle': bundle, - 'key_index': self.key_index, - 'start_index': start_index, - }, + def __init__(self, trytes, key_index=None, security_level=None): + # type: (TrytesCompatible, Optional[int], Optional[int]) -> None + super(PrivateKey, self).__init__(trytes) + + 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=FRAGMENT_LENGTH, + ), + ), + + context={ + 'trytes': self._trytes, + }, + ) + + self.key_index = key_index + self.security_level = security_level + + def as_json_compatible(self): + # type: () -> dict + return { + 'trytes': self._trytes.decode('ascii'), + 'key_index': self.key_index, + 'security_level': self.security_level, + } + + def get_digest(self): + # type: () -> Digest + """ + 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. + """ + hashes_per_fragment = FRAGMENT_LENGTH // Hash.LEN + + key_fragments = self.iter_chunks(FRAGMENT_LENGTH) + + # The digest will contain one hash per key fragment. + digest = [0] * HASH_LENGTH * len(key_fragments) + + # Iterate over each fragment in the key. + for i, fragment in enumerate(key_fragments): + fragment_trits = fragment.as_trits() + + key_fragment = [0] * FRAGMENT_LENGTH + hash_trits = [] + + # Within each fragment, iterate over one hash at a time. + 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] + + for k in range(26): + sponge = Kerl() + sponge.absorb(hash_trits) + sponge.squeeze(hash_trits) + + key_fragment[hash_start:hash_end] = hash_trits + + # After processing all of the hashes in the fragment, + # generate a final hash and append it to the digest. + # + # Note that we will do this once per fragment in the key, so + # the longer the key is, the longer the digest will be. + sponge = Kerl() + sponge.absorb(key_fragment) + sponge.squeeze(hash_trits) + + fragment_hash_start = i * HASH_LENGTH + fragment_hash_end = fragment_hash_start + HASH_LENGTH + + digest[fragment_hash_start:fragment_hash_end] = hash_trits + + return Digest(TryteString.from_trits(digest), self.key_index) + + def sign_input_transactions(self, bundle, start_index): + # type: (Bundle, int) -> None + """ + Signs the inputs starting at the specified index. + + :param bundle: + The bundle that contains the input transactions to sign. + + :param start_index: + The index of the first input transaction. + + If necessary, the resulting signature will be split across + subsequent transactions automatically. + """ + + if not bundle.hash: + raise with_context( + exc=ValueError('Cannot sign inputs without a bundle hash!'), + + context={ + 'bundle': bundle, + 'key_index': self.key_index, + 'start_index': start_index, + }, + ) + + from iota.crypto.signing import SignatureFragmentGenerator + signature_fragment_generator = ( + SignatureFragmentGenerator(self, bundle.hash) ) - txn.signature_message_fragment = next(signature_fragment_generator) + # We can only fit one signature fragment into each transaction, + # so we have to split the entire signature. + for j in range(self.security_level): + # Do lots of validation before we attempt to sign the + # transaction, and attach lots of context info to any + # exception. + # + # This method is likely to be invoked at a very low level in + # the application, so if anything goes wrong, we want to + # make sure it's as easy to troubleshoot as possible! + try: + txn = bundle[start_index + j] + except IndexError as e: + raise with_context( + exc=e, + + context={ + 'bundle': bundle, + 'key_index': self.key_index, + 'current_index': start_index + j, + }, + ) + + # Only inputs can be signed. + if txn.value > 0: + raise with_context( + exc=ValueError( + 'Attempting to sign non-input transaction #{i} ' + '(value={value}).'.format( + i=txn.current_index, + value=txn.value, + ), + ), + + context={ + 'bundle': bundle, + 'key_index': self.key_index, + 'start_index': start_index, + }, + ) + + if txn.signature_message_fragment: + raise with_context( + exc=ValueError( + 'Attempting to sign input transaction #{i}, ' + 'but it has a non-empty fragment ' + '(is it already signed?).'.format( + i=txn.current_index, + ), + ), + + context={ + 'bundle': bundle, + 'key_index': self.key_index, + 'start_index': start_index, + }, + ) + + txn.signature_message_fragment = next(signature_fragment_generator) From 24e2c73dc7dcaf595a1ce00b53b37a3a4c719ada Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 14 Jun 2018 09:04:17 +1200 Subject: [PATCH 11/32] Fixed incorrect type hints. --- iota/crypto/addresses.py | 4 ++-- iota/crypto/signing.py | 1 + iota/transaction/validator.py | 2 +- iota/types.py | 2 +- 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 072540b..88eb001 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 +from typing import Generator, Iterable, List from iota import Address, TRITS_PER_TRYTE, TrytesCompatible from iota.crypto.kerl import Kerl @@ -132,7 +132,7 @@ def get_addresses(self, start, count=1, step=1): return addresses def create_iterator(self, start=0, step=1): - # type: (int, int) -> Generator[Address] + # type: (int, int) -> Generator[Address, None, None] """ Creates an iterator that can be used to progressively generate new addresses. diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index da01ad4..c598775 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -15,6 +15,7 @@ __all__ = [ 'KeyGenerator', + 'KeyIterator', 'SignatureFragmentGenerator', 'validate_signature_fragments', ] diff --git a/iota/transaction/validator.py b/iota/transaction/validator.py index 0666cab..d1088eb 100644 --- a/iota/transaction/validator.py +++ b/iota/transaction/validator.py @@ -68,7 +68,7 @@ def is_valid(self): return not self._errors def _create_validator(self): - # type: () -> Generator[Text] + # type: () -> Generator[Text, None, None] """ Creates a generator that does all the work. """ diff --git a/iota/types.py b/iota/types.py index 70372f9..609d1d8 100644 --- a/iota/types.py +++ b/iota/types.py @@ -321,7 +321,7 @@ def __len__(self): return len(self._trytes) def __iter__(self): - # type: () -> Generator[binary_type] + # type: () -> Generator[binary_type, None, None] # :see: http://stackoverflow.com/a/14267935/ return (binary_type(self._trytes[i:i + 1]) for i in range(len(self))) From 02f476742da82a000cdb842dd077a00ad266b73e Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 16 Jun 2018 09:51:38 +1200 Subject: [PATCH 12/32] [#145] Reformat multisig commands for PEP-8. --- iota/multisig/commands/__init__.py | 3 +- .../commands/create_multisig_address.py | 54 +++-- iota/multisig/commands/get_digests.py | 99 ++++---- iota/multisig/commands/get_private_keys.py | 97 ++++---- .../commands/prepare_multisig_transfer.py | 229 +++++++++--------- 5 files changed, 233 insertions(+), 249 deletions(-) diff --git a/iota/multisig/commands/__init__.py b/iota/multisig/commands/__init__.py index ff7fa14..80470a7 100644 --- a/iota/multisig/commands/__init__.py +++ b/iota/multisig/commands/__init__.py @@ -1,7 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals - + unicode_literals from .create_multisig_address import * from .get_digests import * diff --git a/iota/multisig/commands/create_multisig_address.py b/iota/multisig/commands/create_multisig_address.py index 9b49eb9..06ff70c 100644 --- a/iota/multisig/commands/create_multisig_address.py +++ b/iota/multisig/commands/create_multisig_address.py @@ -1,53 +1,55 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List import filters as f + from iota.commands import FilterCommand, RequestFilter from iota.crypto.types import Digest from iota.filters import Trytes from iota.multisig.crypto.addresses import MultisigAddressBuilder __all__ = [ - 'CreateMultisigAddressCommand', + 'CreateMultisigAddressCommand', ] class CreateMultisigAddressCommand(FilterCommand): - """ - Implements `create_multisig_address` multisig command. + """ + Implements `create_multisig_address` multisig command. + + References: - References: - :py:meth:`iota.multisig.api.MultisigIota.create_multisig_address` - """ - command = 'createMultisigAddress' + """ + command = 'createMultisigAddress' - def get_request_filter(self): - return CreateMultisigAddressRequestFilter() + def get_request_filter(self): + return CreateMultisigAddressRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass - def _execute(self, request): - digests = request['digests'] # type: List[Digest] + def _execute(self, request): + digests = request['digests'] # type: List[Digest] - builder = MultisigAddressBuilder() + builder = MultisigAddressBuilder() - for d in digests: - builder.add_digest(d) + for d in digests: + builder.add_digest(d) - return { - 'address': builder.get_address(), - } + return { + 'address': builder.get_address(), + } class CreateMultisigAddressRequestFilter(RequestFilter): - def __init__(self): - super(CreateMultisigAddressRequestFilter, self).__init__({ - 'digests': - f.Required - | f.Array - | f.FilterRepeater(f.Required | Trytes(result_type=Digest)), - }) + def __init__(self): + super(CreateMultisigAddressRequestFilter, self).__init__({ + 'digests': + f.Required | f.Array | f.FilterRepeater( + f.Required | Trytes(Digest), + ), + }) diff --git a/iota/multisig/commands/get_digests.py b/iota/multisig/commands/get_digests.py index 7438a8a..91256b9 100644 --- a/iota/multisig/commands/get_digests.py +++ b/iota/multisig/commands/get_digests.py @@ -1,80 +1,71 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Optional import filters as f from iota.commands import FilterCommand, RequestFilter -from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed -from iota.filters import Trytes +from iota.filters import SecurityLevel, Trytes from iota.multisig.commands.get_private_keys import GetPrivateKeysCommand __all__ = [ - 'GetDigestsCommand', + 'GetDigestsCommand', ] class GetDigestsCommand(FilterCommand): - """ - Implements `getDigests` multisig API command. + """ + Implements `getDigests` multisig API command. + + References: - References: - :py:meth:`iota.multisig.api.MultisigIota.get_digests` - """ - command = 'getDigests' + """ + command = 'getDigests' - def get_request_filter(self): - return GetDigestsRequestFilter() + def get_request_filter(self): + return GetDigestsRequestFilter() - def get_response_filter(self): - pass + def get_response_filter(self): + pass - def _execute(self, request): - count = request['count'] # type: Optional[int] - index = request['index'] # type: int - seed = request['seed'] # type: Seed - security_level = request['securityLevel'] # type: int + def _execute(self, request): + count = request['count'] # type: Optional[int] + index = request['index'] # type: int + seed = request['seed'] # type: Seed + security_level = request['securityLevel'] # type: int - gpk_result =\ - GetPrivateKeysCommand(self.adapter)( - seed = seed, - count = count, - index = index, - securityLevel = security_level, - ) + gpk_result = GetPrivateKeysCommand(self.adapter)( + seed=seed, + count=count, + index=index, + securityLevel=security_level, + ) - return { - 'digests': [key.get_digest() for key in gpk_result['keys']], - } + return { + 'digests': [key.get_digest() for key in gpk_result['keys']], + } class GetDigestsRequestFilter(RequestFilter): - def __init__(self): - super(GetDigestsRequestFilter, self).__init__( - { - # Optional Parameters - 'count': - f.Type(int) | f.Min(1) | f.Optional(default=1), - - 'index': - f.Type(int) | f.Min(0) | f.Optional(default=0), - - 'securityLevel': - f.Type(int) - | f.Min(1) - | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), - - # Required Parameters - 'seed': - f.Required | Trytes(result_type=Seed), - }, - - allow_missing_keys = { - 'count', - 'index', - 'securityLevel', - }, - ) + def __init__(self): + super(GetDigestsRequestFilter, self).__init__( + { + # Optional Parameters + 'count': f.Type(int) | f.Min(1) | f.Optional(default=1), + 'index': f.Type(int) | f.Min(0) | f.Optional(default=0), + 'securityLevel': SecurityLevel, + + # Required Parameters + 'seed': f.Required | Trytes(Seed), + }, + + allow_missing_keys={ + 'count', + 'index', + 'securityLevel', + }, + ) diff --git a/iota/multisig/commands/get_private_keys.py b/iota/multisig/commands/get_private_keys.py index 0673b49..2a58abf 100644 --- a/iota/multisig/commands/get_private_keys.py +++ b/iota/multisig/commands/get_private_keys.py @@ -1,76 +1,71 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Optional import filters as f from iota.commands import FilterCommand, RequestFilter -from iota.crypto.addresses import AddressGenerator from iota.crypto.signing import KeyGenerator from iota.crypto.types import Seed -from iota.filters import Trytes +from iota.filters import SecurityLevel, Trytes __all__ = [ - 'GetPrivateKeysCommand', + 'GetPrivateKeysCommand', ] class GetPrivateKeysCommand(FilterCommand): - """ - Implements `get_private_keys` multisig API command. + """ + Implements `get_private_keys` multisig API command. - References: - - :py:meth:`iota.multisig.MultisigIota.get_private_key` - - https://github.com/iotaledger/wiki/blob/master/multisigs.md - """ - command = 'getPrivateKeys' + References: - def get_request_filter(self): - return GetPrivateKeysRequestFilter() + - :py:meth:`iota.multisig.MultisigIota.get_private_key` + - https://github.com/iotaledger/wiki/blob/master/multisigs.md + """ + command = 'getPrivateKeys' - def get_response_filter(self): - pass + def get_request_filter(self): + return GetPrivateKeysRequestFilter() - def _execute(self, request): - count = request['count'] # type: Optional[int] - index = request['index'] # type: int - seed = request['seed'] # type: Seed - security_level = request['securityLevel'] # type: int + def get_response_filter(self): + pass - generator = KeyGenerator(seed) + def _execute(self, request): + count = request['count'] # type: Optional[int] + index = request['index'] # type: int + seed = request['seed'] # type: Seed + security_level = request['securityLevel'] # type: int - return { - 'keys': - generator.get_keys(start=index, count=count, iterations=security_level), - } + generator = KeyGenerator(seed) + + return { + 'keys': generator.get_keys( + start=index, + count=count, + iterations=security_level, + ), + } class GetPrivateKeysRequestFilter(RequestFilter): - def __init__(self): - super(GetPrivateKeysRequestFilter, self).__init__( - { - # Optional Parameters - 'count': - f.Type(int) | f.Min(1) | f.Optional(default=1), - - 'index': - f.Type(int) | f.Min(0) | f.Optional(default=0), - - 'securityLevel': - f.Type(int) - | f.Min(1) - | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), - - # Required Parameters - 'seed': - f.Required | Trytes(result_type=Seed), - }, - - allow_missing_keys = { - 'count', - 'index', - 'securityLevel', - }, - ) + def __init__(self): + super(GetPrivateKeysRequestFilter, self).__init__( + { + # Optional Parameters + 'count': f.Type(int) | f.Min(1) | f.Optional(default=1), + 'index': f.Type(int) | f.Min(0) | f.Optional(default=0), + 'securityLevel': SecurityLevel, + + # Required Parameters + 'seed': f.Required | Trytes(Seed), + }, + + allow_missing_keys={ + 'count', + 'index', + 'securityLevel', + }, + ) diff --git a/iota/multisig/commands/prepare_multisig_transfer.py b/iota/multisig/commands/prepare_multisig_transfer.py index e842cd7..7d43c2c 100644 --- a/iota/multisig/commands/prepare_multisig_transfer.py +++ b/iota/multisig/commands/prepare_multisig_transfer.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, Optional @@ -15,131 +15,128 @@ from iota.multisig.types import MultisigAddress __all__ = [ - 'PrepareMultisigTransferCommand', + 'PrepareMultisigTransferCommand', ] class PrepareMultisigTransferCommand(FilterCommand): - """ - Implements `prepare_multisig_transfer` multisig API command. + """ + Implements `prepare_multisig_transfer` multisig API command. - References: - - :py:meth:`iota.multisig.api.MultisigIota.prepare_multisig_transfer` - """ - command = 'prepareMultisigTransfer' - - def get_request_filter(self): - return PrepareMultisigTransferRequestFilter() - - def get_response_filter(self): - pass - - def _execute(self, request): - change_address = request['changeAddress'] # type: Optional[Address] - multisig_input = request['multisigInput'] # type: MultisigAddress - transfers = request['transfers'] # type: List[ProposedTransaction] - - bundle = ProposedMultisigBundle(transfers) - - want_to_spend = bundle.balance - if want_to_spend > 0: - gb_response =\ - GetBalancesCommand(self.adapter)( - addresses = [multisig_input], - ) - - multisig_input.balance = gb_response['balances'][0] - - if multisig_input.balance < want_to_spend: - raise with_context( - exc = - ValueError( - 'Insufficient balance; found {found}, need {need} ' - '(``exc.context`` has more info).'.format( - found = multisig_input.balance, - need = want_to_spend, - ), - ), - - # The structure of this context object is intended to match - # the one from ``PrepareTransferCommand``. - context = { - 'available_to_spend': multisig_input.balance, - 'confirmed_inputs': [multisig_input], - 'request': request, - 'want_to_spend': want_to_spend, - }, - ) - - bundle.add_inputs([multisig_input]) + References: - if bundle.balance < 0: - if change_address: - bundle.send_unspent_inputs_to(change_address) + - :py:meth:`iota.multisig.api.MultisigIota.prepare_multisig_transfer` + """ + command = 'prepareMultisigTransfer' + + def get_request_filter(self): + return PrepareMultisigTransferRequestFilter() + + def get_response_filter(self): + pass + + def _execute(self, request): + change_address = request['changeAddress'] # type: Optional[Address] + multisig_input = request['multisigInput'] # type: MultisigAddress + transfers = request['transfers'] # type: List[ProposedTransaction] + + bundle = ProposedMultisigBundle(transfers) + + want_to_spend = bundle.balance + if want_to_spend > 0: + gb_response = GetBalancesCommand(self.adapter)( + addresses=[multisig_input], + ) + + multisig_input.balance = gb_response['balances'][0] + + if multisig_input.balance < want_to_spend: + raise with_context( + exc=ValueError( + 'Insufficient balance; found {found}, need {need} ' + '(``exc.context`` has more info).'.format( + found=multisig_input.balance, + need=want_to_spend, + ), + ), + + # The structure of this context object is intended + # to match the one from ``PrepareTransferCommand``. + context={ + 'available_to_spend': multisig_input.balance, + 'confirmed_inputs': [multisig_input], + 'request': request, + 'want_to_spend': want_to_spend, + }, + ) + + bundle.add_inputs([multisig_input]) + + if bundle.balance < 0: + if change_address: + bundle.send_unspent_inputs_to(change_address) + else: + # + # Unlike :py:meth:`iota.api.Iota.prepare_transfer` + # where all of the inputs are owned by the same + # seed, creating a multisig transfer usually + # involves multiple people. + # + # It would be unfair to the participants of the + # transaction if we were to automatically generate a + # change address using the seed of whoever happened + # to invoke the + # :py:meth:`MultisigIota.prepare_multisig_transfer` + # method! + # + raise with_context( + exc=ValueError( + 'Bundle has unspent inputs, ' + 'but no change address specified.', + ), + + context={ + 'available_to_spend': multisig_input.balance, + 'balance': bundle.balance, + 'confirmed_inputs': [multisig_input], + 'request': request, + 'want_to_spend': want_to_spend, + }, + ) else: - # - # Unlike :py:meth:`iota.api.Iota.prepare_transfer` where all - # of the inputs are owned by the same seed, creating a - # multisig transfer usually involves multiple people. - # - # It would be unfair to the participants of the transaction - # if we were to automatically generate a change address using - # the seed of whoever happened to invoke the - # :py:meth:`MultisigIota.prepare_multisig_transfer` method! - # - raise with_context( - exc = - ValueError( - 'Bundle has unspent inputs, but no change address specified.', - ), - - context = { - 'available_to_spend': multisig_input.balance, - 'balance': bundle.balance, - 'confirmed_inputs': [multisig_input], - 'request': request, - 'want_to_spend': want_to_spend, - }, - ) - else: - raise with_context( - exc = - ValueError( - 'Use ``prepare_transfer`` ' - 'to create a bundle without spending IOTAs.', - ), + raise with_context( + exc=ValueError( + 'Use ``prepare_transfer`` ' + 'to create a bundle without spending IOTAs.', + ), - context = { - 'request': request, - }, - ) + context={ + 'request': request, + }, + ) - bundle.finalize() + bundle.finalize() - # Return the bundle with inputs unsigned. - return { - 'trytes': bundle.as_tryte_strings(), - } + # Return the bundle with inputs unsigned. + return { + 'trytes': bundle.as_tryte_strings(), + } class PrepareMultisigTransferRequestFilter(RequestFilter): - def __init__(self): - super(PrepareMultisigTransferRequestFilter, self).__init__( - { - 'changeAddress': - Trytes(result_type=Address), - - 'multisigInput': - f.Required - | f.Type(MultisigAddress), - - 'transfers': - f.Required - | f.Array - | f.FilterRepeater(f.Required | f.Type(ProposedTransaction)), - }, - - allow_missing_keys = { - 'changeAddress', - }, - ) + def __init__(self): + super(PrepareMultisigTransferRequestFilter, self).__init__( + { + 'changeAddress': Trytes(Address), + 'multisigInput': f.Required | f.Type(MultisigAddress), + + 'transfers': + f.Required | f.Array | f.FilterRepeater( + f.Required | f.Type(ProposedTransaction), + ), + }, + + allow_missing_keys={ + 'changeAddress', + }, + ) From 3ce90c05bf702291edf1e34c0bdb2425be87f72f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 16 Jun 2018 09:55:00 +1200 Subject: [PATCH 13/32] [#145] Reformat multisig crypto for PEP-8. --- iota/multisig/crypto/__init__.py | 2 +- iota/multisig/crypto/addresses.py | 119 ++++++++++++++++-------------- 2 files changed, 64 insertions(+), 57 deletions(-) diff --git a/iota/multisig/crypto/__init__.py b/iota/multisig/crypto/__init__.py index 3f3d02d..725c9ff 100644 --- a/iota/multisig/crypto/__init__.py +++ b/iota/multisig/crypto/__init__.py @@ -1,3 +1,3 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals diff --git a/iota/multisig/crypto/addresses.py b/iota/multisig/crypto/addresses.py index d19a7e2..a12e861 100644 --- a/iota/multisig/crypto/addresses.py +++ b/iota/multisig/crypto/addresses.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List, Optional @@ -10,76 +10,83 @@ from iota.multisig.types import MultisigAddress __all__ = [ - 'MultisigAddressBuilder', + 'MultisigAddressBuilder', ] class MultisigAddressBuilder(object): - """ - Creates multisig addresses. - - Note that this class generates a single address from multiple inputs, - (digests) unlike :py:class:`iota.crypto.addresses.AddressGenerator` - which generates multiple addresses from a single input (seed). - """ - def __init__(self): - super(MultisigAddressBuilder, self).__init__() - - self._digests = [] # type: List[Digest] - """ - Keeps track of digests that were added, so that we can attach them - to the final :py:class:`MultisigAddress` object. """ + Creates multisig addresses. - self._address = None # type: Optional[MultisigAddress] + Note that this class generates a single address from multiple + inputs (digests), unlike + :py:class:`iota.crypto.addresses.AddressGenerator` which generates + multiple addresses from a single input (seed). """ - Caches the generated address. - Generating the address modifies the internal state of the curl - sponge, so each :py:class:`MultisigAddressBuilder` instance can - only generate a single address. - """ + def __init__(self): + super(MultisigAddressBuilder, self).__init__() - self._sponge = Kerl() + self._digests = [] # type: List[Digest] + """ + Keeps track of digests that were added, so that we can attach + them to the final :py:class:`MultisigAddress` object. + """ - def add_digest(self, digest): - # type: (Digest) -> None - """ - Absorbs a digest into the sponge. + self._address = None # type: Optional[MultisigAddress] + """ + Caches the generated address. - IMPORTANT: Keep track of the order that digests are added! - To spend inputs from a multisig address, you must provide the - private keys in the same order! + Generating the address modifies the internal state of the curl + sponge, so each :py:class:`MultisigAddressBuilder` instance can + only generate a single address. + """ - References: - - https://github.com/iotaledger/wiki/blob/master/multisigs.md#spending-inputs - """ - if self._address: - raise ValueError('Cannot add digests once an address is extracted.') + self._sponge = Kerl() - self._sponge.absorb(digest.as_trits()) - self._digests.append(digest) + def add_digest(self, digest): + # type: (Digest) -> None + """ + Absorbs a digest into the sponge. - def get_address(self): - # type: () -> MultisigAddress - """ - Returns the new multisig address. + .. important:: + Keep track of the order that digests are added! - Note that you can continue to add digests after extracting an - address; the next address will use *all* of the digests that have - been added so far. - """ - if not self._digests: - raise ValueError( - 'Must call ``add_digest`` at least once ' - 'before calling ``get_address``.', - ) + To spend inputs from a multisig address, you must provide + the private keys in the same order! + + References: + + - https://github.com/iotaledger/wiki/blob/master/multisigs.md#spending-inputs + """ + if self._address: + raise ValueError('Cannot add digests once an address is extracted.') + + self._sponge.absorb(digest.as_trits()) + self._digests.append(digest) + + def get_address(self): + # type: () -> MultisigAddress + """ + Returns the new multisig address. + + Note that you can continue to add digests after extracting an + address; the next address will use *all* of the digests that + have been added so far. + """ + if not self._digests: + raise ValueError( + 'Must call ``add_digest`` at least once ' + 'before calling ``get_address``.', + ) - if not self._address: - address_trits = [0] * HASH_LENGTH - self._sponge.squeeze(address_trits) + if not self._address: + address_trits = [0] * HASH_LENGTH + self._sponge.squeeze(address_trits) - self._address =\ - MultisigAddress.from_trits(address_trits, digests=self._digests[:]) + self._address = MultisigAddress.from_trits( + address_trits, + digests=self._digests[:], + ) - return self._address + return self._address From ac0a6e479589a706c94135497e35b0aedf021663 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 16 Jun 2018 10:09:08 +1200 Subject: [PATCH 14/32] [#145] Reformatted rest of multisig package for PEP-8. --- iota/multisig/__init__.py | 2 +- iota/multisig/api.py | 417 ++++++++++++++++++++------------------ iota/multisig/types.py | 55 ++--- 3 files changed, 252 insertions(+), 222 deletions(-) diff --git a/iota/multisig/__init__.py b/iota/multisig/__init__.py index 98b2f2c..b058d43 100644 --- a/iota/multisig/__init__.py +++ b/iota/multisig/__init__.py @@ -1,5 +1,5 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from .api import * diff --git a/iota/multisig/api.py b/iota/multisig/api.py index d9c159e..563f7ca 100644 --- a/iota/multisig/api.py +++ b/iota/multisig/api.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Iterable, Optional @@ -12,205 +12,232 @@ from iota.multisig.types import MultisigAddress __all__ = [ - 'MultisigIota', + 'MultisigIota', ] -class MultisigIota(Iota): - """ - Extends the IOTA API so that it can send multi-signature - transactions. - - **CAUTION:** Make sure you understand how multisig works before - attempting to use it. If you are not careful, you could easily - compromise the security of your private keys, send IOTAs to - unspendable addresses, etc. - - References: - - https://github.com/iotaledger/wiki/blob/master/multisigs.md - """ - commands = discover_commands('iota.multisig.commands') - - def create_multisig_address(self, digests): - # type: (Iterable[Digest]) -> dict - """ - Generates a multisig address from a collection of digests. - - :param digests: - Digests to use to create the multisig address. - - IMPORTANT: In order to spend IOTAs from a multisig address, the - signature must be generated from the corresponding private keys - in the exact same order. - - :return: - Dict with the following items:: - - { - 'address': MultisigAddress, - The generated multisig address. - } - """ - return commands.CreateMultisigAddressCommand(self.adapter)( - digests = digests, - ) - - def get_digests( - self, - index = 0, - count = 1, - security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL, - ): - # type: (int, int, int) -> dict - """ - Generates one or more key digests from the seed. - - Digests are safe to share; use them to generate multisig addresses. - - :param index: - The starting key index. - - :param count: - Number of digests to generate. - - :param security_level: - Number of iterations to use when generating new addresses. - Larger values take longer, but the resulting signatures are more - secure. - - This value must be between 1 and 3, inclusive. - - :return: - Dict with the following items:: - - { - 'digests': List[Digest], - Always contains a list, even if only one digest was - generated. - } - """ - return commands.GetDigestsCommand(self.adapter)( - seed = self.seed, - index = index, - count = count, - securityLevel = security_level, - ) - - def get_private_keys( - self, - index = 0, - count = 1, - security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL, - ): - # type: (int, int, int) -> dict +class MultisigIota(Iota): """ - Generates one or more private keys from the seed. - - As the name implies, private keys should not be shared. However, - in a few cases it may be necessary (e.g., for M-of-N transactions). - - :param index: - The starting key index. - - :param count: - Number of keys to generate. + Extends the IOTA API so that it can send multi-signature + transactions. - :param security_level: - Number of iterations to use when generating new keys. - - Larger values take longer, but the resulting signatures are more - secure. - - This value must be between 1 and 3, inclusive. - - :return: - Dict with the following items:: - - { - 'keys': List[PrivateKey], - Always contains a list, even if only one key was - generated. - } + .. caution:: + Make sure you understand how multisig works before attempting to + use it. If you are not careful, you could easily compromise the + security of your private keys, send IOTAs to unspendable + addresses, etc. References: - - :py:class:`iota.crypto.signing.KeyGenerator` - - https://github.com/iotaledger/wiki/blob/master/multisigs.md#how-m-of-n-works - """ - return commands.GetPrivateKeysCommand(self.adapter)( - seed = self.seed, - index = index, - count = count, - securityLevel = security_level, - ) - - def prepare_multisig_transfer( - self, - transfers, - multisig_input, - change_address = None, - ): - # type: (Iterable[ProposedTransaction], MultisigAddress, Optional[Address]) -> dict - """ - Prepares a bundle that authorizes the spending of IOTAs from a - multisig address. - - Note: if you want to spend IOTAs from non-multisig addresses, or if - you want to create 0-value transfers (i.e., that don't require - inputs), you can use :py:meth:`iota.api.Iota.prepare_transfer` - instead. - - :param transfers: - Transaction objects to prepare. - - Important: Must include at least one transaction that spends - IOTAs (has a nonzero ``value``). If you want to prepare a bundle - that does not spend any IOTAs, use - :py:meth:`iota.api.prepare_transfer` instead. - - :param multisig_input: - The multisig address to use as the input for the transfers. - - Note: a bundle may contain only one multisig input. - - :param change_address: - If inputs are provided, any unspent amount will be sent to this - address. If the bundle has no unspent inputs, ``change_address` - is ignored. - - Unlike :py:meth:`iota.api.Iota.prepare_transfer`, this method - will NOT generate a change address automatically. If there are - unspent inputs and ``change_address`` is empty, an exception will - be raised. - - This is because multisig transactions typically involve multiple - individuals, and it would be unfair to the participants if we - generated a change address automatically using the seed of - whoever happened to run the ``prepare_multisig_transfer`` method! - - Note: this is not a substitute for due diligence! Always verify - the details of every transaction in a bundle (including the - change transaction) before signing the input(s)! - - :return: - Dict containing the following values:: - - { - 'trytes': List[TransactionTrytes], - Finalized bundle, as trytes. - The input transactions are not signed. - } - - In order to authorize the spending of IOTAs from the multisig - input, you must generate the correct private keys and invoke the - :py:meth:`iota.crypto.types.PrivateKey.sign_input_at` method for - each key, in the correct order. - - Once the correct signatures are applied, you can then perform - proof of work (``attachToTangle``) and broadcast the bundle using - :py:meth:`iota.api.Iota.send_trytes`. + + - https://github.com/iotaledger/wiki/blob/master/multisigs.md """ - return commands.PrepareMultisigTransferCommand(self.adapter)( - changeAddress = change_address, - multisigInput = multisig_input, - transfers = transfers, - ) + commands = discover_commands('iota.multisig.commands') + + def create_multisig_address(self, digests): + # type: (Iterable[Digest]) -> dict + """ + Generates a multisig address from a collection of digests. + + :param digests: + Digests to use to create the multisig address. + + .. important:: + In order to spend IOTAs from a multisig address, the + signature must be generated from the corresponding private + keys in the exact same order. + + :return: + Dict with the following items:: + + { + 'address': MultisigAddress, + The generated multisig address. + } + """ + return commands.CreateMultisigAddressCommand(self.adapter)( + digests=digests, + ) + + def get_digests( + self, + index=0, + count=1, + security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL, + ): + # type: (int, int, int) -> dict + """ + Generates one or more key digests from the seed. + + Digests are safe to share; use them to generate multisig + addresses. + + :param index: + The starting key index. + + :param count: + Number of digests to generate. + + :param security_level: + Number of iterations to use when generating new addresses. + + Larger values take longer, but the resulting signatures are + more secure. + + This value must be between 1 and 3, inclusive. + + :return: + Dict with the following items:: + + { + 'digests': List[Digest], + Always contains a list, even if only one digest + was generated. + } + """ + return commands.GetDigestsCommand(self.adapter)( + seed=self.seed, + index=index, + count=count, + securityLevel=security_level, + ) + + def get_private_keys( + self, + index=0, + count=1, + security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL, + ): + # type: (int, int, int) -> dict + """ + Generates one or more private keys from the seed. + + As the name implies, private keys should not be shared. + However, in a few cases it may be necessary (e.g., for M-of-N + transactions). + + :param index: + The starting key index. + + :param count: + Number of keys to generate. + + :param security_level: + Number of iterations to use when generating new keys. + + Larger values take longer, but the resulting signatures are + more secure. + + This value must be between 1 and 3, inclusive. + + :return: + Dict with the following items:: + + { + 'keys': List[PrivateKey], + Always contains a list, even if only one key was + generated. + } + + References: + + - :py:class:`iota.crypto.signing.KeyGenerator` + - https://github.com/iotaledger/wiki/blob/master/multisigs.md#how-m-of-n-works + """ + return commands.GetPrivateKeysCommand(self.adapter)( + seed=self.seed, + index=index, + count=count, + securityLevel=security_level, + ) + + def prepare_multisig_transfer( + self, + transfers, # type: Iterable[ProposedTransaction] + multisig_input, # type: MultisigAddress + change_address=None, # type: Optional[Address] + ): + # type: (...) -> dict + """ + Prepares a bundle that authorizes the spending of IOTAs from a + multisig address. + + .. note:: + This method is used exclusively to spend IOTAs from a + multisig address. + + If you want to spend IOTAs from non-multisig addresses, or + if you want to create 0-value transfers (i.e., that don't + require inputs), use + :py:meth:`iota.api.Iota.prepare_transfer` instead. + + :param transfers: + Transaction objects to prepare. + + .. important:: + Must include at least one transaction that spends IOTAs + (i.e., has a nonzero ``value``). If you want to prepare + a bundle that does not spend any IOTAs, use + :py:meth:`iota.api.prepare_transfer` instead. + + :param multisig_input: + The multisig address to use as the input for the transfers. + + .. note:: + This method only supports creating a bundle with a + single multisig input. + + If you would like to spend from multiple multisig + addresses in the same bundle, create the + :py:class:`iota.multisig.transaction.ProposedMultisigBundle` + object manually. + + :param change_address: + If inputs are provided, any unspent amount will be sent to + this address. + + If the bundle has no unspent inputs, ``change_address` is + ignored. + + .. important:: + Unlike :py:meth:`iota.api.Iota.prepare_transfer`, this + method will NOT generate a change address automatically. + If there are unspent inputs and ``change_address`` is + empty, an exception will be raised. + + This is because multisig transactions typically involve + multiple individuals, and it would be unfair to the + participants if we generated a change address + automatically using the seed of whoever happened to run + the ``prepare_multisig_transfer`` method! + + .. danger:: + Note that this protective measure is not a + substitute for due diligence! + + Always verify the details of every transaction in a + bundle (including the change transaction) before + signing the input(s)! + + :return: + Dict containing the following values:: + + { + 'trytes': List[TransactionTrytes], + Finalized bundle, as trytes. + The input transactions are not signed. + } + + In order to authorize the spending of IOTAs from the multisig + input, you must generate the correct private keys and invoke + the :py:meth:`iota.crypto.types.PrivateKey.sign_input_at` + method for each key, in the correct order. + + Once the correct signatures are applied, you can then perform + proof of work (``attachToTangle``) and broadcast the bundle + using :py:meth:`iota.api.Iota.send_trytes`. + """ + return commands.PrepareMultisigTransferCommand(self.adapter)( + changeAddress=change_address, + multisigInput=multisig_input, + transfers=transfers, + ) diff --git a/iota/multisig/types.py b/iota/multisig/types.py index bf6d84a..b9e9fa1 100644 --- a/iota/multisig/types.py +++ b/iota/multisig/types.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from operator import attrgetter from typing import Iterable, Optional @@ -9,32 +9,35 @@ from iota.crypto.types import Digest __all__ = [ - 'MultisigAddress', + 'MultisigAddress', ] class MultisigAddress(Address): - """ - An address that was generated using digests from multiple private - keys. - - In order to spend inputs from a multisig address, the same private - keys must be used, in the same order that the corresponding digests - were used to generate the address. - """ - def __init__(self, trytes, digests, balance=None): - # type: (TrytesCompatible, Iterable[Digest], Optional[int]) -> None - # Key index is meaningless for multisig addresses. - super(MultisigAddress, self).__init__(trytes, balance, key_index=None) - - self.digests = digests - - self.security_level = sum(map(attrgetter('security_level'), self.digests)) - - def as_json_compatible(self): - # type: () -> dict - return { - 'trytes': self._trytes.decode('ascii'), - 'balance': self.balance, - 'digests': self.digests, - } + """ + An address that was generated using digests from multiple private + keys. + + In order to spend inputs from a multisig address, the same private + keys must be used, in the same order that the corresponding digests + were used to generate the address. + """ + + def __init__(self, trytes, digests, balance=None): + # type: (TrytesCompatible, Iterable[Digest], Optional[int]) -> None + # Key index is meaningless for multisig addresses. + super(MultisigAddress, self).__init__(trytes, balance, key_index=None) + + self.digests = digests + + self.security_level = sum( + map(attrgetter('security_level'), self.digests) + ) + + def as_json_compatible(self): + # type: () -> dict + return { + 'trytes': self._trytes.decode('ascii'), + 'balance': self.balance, + 'digests': self.digests, + } From 687f68f9f5e6f74cb1422ef84e1835427d7d5fef Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 16 Jun 2018 10:38:00 +1200 Subject: [PATCH 15/32] [#145] Reformatted transaction package for PEP-8. --- iota/crypto/signing.py | 1 + iota/transaction/__init__.py | 2 +- iota/transaction/base.py | 1051 +++++++++++++++++---------------- iota/transaction/creation.py | 106 ++-- iota/transaction/types.py | 143 ++--- iota/transaction/utils.py | 96 +-- iota/transaction/validator.py | 463 ++++++++------- 7 files changed, 975 insertions(+), 887 deletions(-) diff --git a/iota/crypto/signing.py b/iota/crypto/signing.py index c598775..8c2b2a7 100644 --- a/iota/crypto/signing.py +++ b/iota/crypto/signing.py @@ -16,6 +16,7 @@ __all__ = [ 'KeyGenerator', 'KeyIterator', + 'normalize', 'SignatureFragmentGenerator', 'validate_signature_fragments', ] diff --git a/iota/transaction/__init__.py b/iota/transaction/__init__.py index 8a0ddf4..2414a61 100644 --- a/iota/transaction/__init__.py +++ b/iota/transaction/__init__.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals # Import symbols to package namespace, for backwards-compatibility with # PyOTA 1.1.x. diff --git a/iota/transaction/base.py b/iota/transaction/base.py index da7dd1c..776dba4 100644 --- a/iota/transaction/base.py +++ b/iota/transaction/base.py @@ -1,550 +1,599 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from operator import attrgetter from typing import Iterable, Iterator, List, MutableSequence, \ - Optional, Sequence, Text + Optional, Sequence, Text from iota.codecs import TrytesDecodeError from iota.crypto import Curl, HASH_LENGTH from iota.json import JsonSerializable from iota.transaction.types import BundleHash, Fragment, Nonce, \ - TransactionHash, TransactionTrytes + TransactionHash, TransactionTrytes from iota.trits import int_from_trits, trits_from_int from iota.types import Address, Tag, TryteString, TrytesCompatible __all__ = [ - 'Bundle', - 'Transaction', + 'Bundle', + 'Transaction', ] class Transaction(JsonSerializable): - """ - A transaction that has been attached to the Tangle. - """ - @classmethod - def from_tryte_string(cls, trytes, hash_=None): - # type: (TrytesCompatible, Optional[TransactionHash]) -> Transaction """ - Creates a Transaction object from a sequence of trytes. + A transaction that has been attached to the Tangle. + """ + + @classmethod + def from_tryte_string(cls, trytes, hash_=None): + # type: (TrytesCompatible, Optional[TransactionHash]) -> Transaction + """ + Creates a Transaction object from a sequence of trytes. + + :param trytes: + Raw trytes. Should be exactly 2673 trytes long. + + :param hash_: + The transaction hash, if available. + + If not provided, it will be computed from the transaction + trytes. + """ + tryte_string = TransactionTrytes(trytes) + + if not hash_: + hash_trits = [0] * HASH_LENGTH # type: MutableSequence[int] + + sponge = Curl() + sponge.absorb(tryte_string.as_trits()) + sponge.squeeze(hash_trits) + + hash_ = TransactionHash.from_trits(hash_trits) + + return cls( + hash_=hash_, + signature_message_fragment=Fragment(tryte_string[0:2187]), + address=Address(tryte_string[2187:2268]), + value=int_from_trits(tryte_string[2268:2295].as_trits()), + legacy_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_hash=BundleHash(tryte_string[2349:2430]), + trunk_transaction_hash=TransactionHash(tryte_string[2430:2511]), + branch_transaction_hash=TransactionHash(tryte_string[2511:2592]), + tag=Tag(tryte_string[2592:2619]), + + attachment_timestamp=int_from_trits( + tryte_string[2619:2628].as_trits()), + + attachment_timestamp_lower_bound=int_from_trits( + tryte_string[2628:2637].as_trits()), + + attachment_timestamp_upper_bound=int_from_trits( + tryte_string[2637:2646].as_trits()), + + nonce=Nonce(tryte_string[2646:2673]), + ) + + def __init__( + self, + hash_, # type: Optional[TransactionHash] + signature_message_fragment, # type: Optional[Fragment] + address, # type: Address + value, # type: int + timestamp, # type: int + current_index, # type: Optional[int] + last_index, # type: Optional[int] + bundle_hash, # type: Optional[BundleHash] + trunk_transaction_hash, # type: Optional[TransactionHash] + branch_transaction_hash, # type: Optional[TransactionHash] + tag, # type: Optional[Tag] + attachment_timestamp, # type: Optional[int] + attachment_timestamp_lower_bound, # type: Optional[int] + attachment_timestamp_upper_bound, # type: Optional[int] + nonce, # type: Optional[Nonce] + legacy_tag=None # type: Optional[Tag] + ): + self.hash = hash_ + """ + Transaction ID, generated by taking a hash of the transaction + trits. + """ + + self.bundle_hash = bundle_hash + """ + Bundle hash, generated by taking a hash of metadata from 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._legacy_tag = legacy_tag + """ + Optional classification legacy_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.trunk_transaction_hash = trunk_transaction_hash + """ + In order to add a transaction to the Tangle, the client 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, the client must + perform PoW to "approve" two existing transactions, called the + "trunk" and "branch" transactions. + + The branch transaction may be selected strategically to maximize + the bundle's chances of getting confirmed; otherwise it usually + has no significance. + """ + + self.tag = tag + """ + Optional classification tag applied to this transaction. + """ + + self.attachment_timestamp = attachment_timestamp + + self.attachment_timestamp_lower_bound = attachment_timestamp_lower_bound + + self.attachment_timestamp_upper_bound = attachment_timestamp_upper_bound + + self.signature_message_fragment = signature_message_fragment + """ + "Signature/Message Fragment" (note the slash): + + - For inputs, this contains a fragment of the cryptographic + signature, used to verify the transaction (depending on the + security level of the corresponding address, the entire + signature is usually 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] + """ + 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 (first one in the + bundle). + + Because of the way the Tangle is organized, the tail transaction + is generally the last one in the bundle that gets attached, even + though it occupies the first logical position inside the bundle. + """ + return self.current_index == 0 + + @property + def value_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`value`. + """ + # Note that we are padding to 81 *trits*. + 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 + :py:attr:`timestamp`. + """ + # Note that we are padding to 27 *trits*. + 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 + :py:attr:`current_index`. + """ + # Note that we are padding to 27 *trits*. + 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 + :py:attr:`last_index`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits(trits_from_int(self.last_index, pad=27)) + + @property + def attachment_timestamp_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`attachment_timestamp`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits( + trits_from_int(self.attachment_timestamp, pad=27), + ) + + @property + def attachment_timestamp_lower_bound_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`attachment_timestamp_lower_bound`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits( + trits_from_int(self.attachment_timestamp_lower_bound, pad=27), + ) + + @property + def attachment_timestamp_upper_bound_as_trytes(self): + # type: () -> TryteString + """ + Returns a TryteString representation of the transaction's + :py:attr:`attachment_timestamp_upper_bound`. + """ + # Note that we are padding to 27 *trits*. + return TryteString.from_trits( + trits_from_int(self.attachment_timestamp_upper_bound, 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, + 'legacy_tag': self.legacy_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, + 'tag': self.tag, + 'attachment_timestamp': self.attachment_timestamp, + + 'attachment_timestamp_lower_bound': + self.attachment_timestamp_lower_bound, + + 'attachment_timestamp_upper_bound': + self.attachment_timestamp_upper_bound, + + 'nonce': self.nonce, + } + + def as_tryte_string(self): + # type: () -> TransactionTrytes + """ + Returns a TryteString representation of the transaction. + """ + return TransactionTrytes( + self.signature_message_fragment + + self.address.address + + self.value_as_trytes + + self.legacy_tag + + 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.tag + + self.attachment_timestamp_as_trytes + + self.attachment_timestamp_lower_bound_as_trytes + + self.attachment_timestamp_upper_bound_as_trytes + + 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.legacy_tag + + self.timestamp_as_trytes + + self.current_index_as_trytes + + self.last_index_as_trytes + ) + + @property + def legacy_tag(self): + # type: () -> Tag + """ + Return the legacy tag of the transaction. + If no legacy tag was set, returns the tag instead. + """ + return self._legacy_tag or self.tag - :param trytes: - Raw trytes. Should be exactly 2673 trytes long. - :param hash_: - The transaction hash, if available. - If not provided, it will be computed from the transaction trytes. - """ - tryte_string = TransactionTrytes(trytes) - - if not hash_: - hash_trits = [0] * HASH_LENGTH # type: MutableSequence[int] - - sponge = Curl() - sponge.absorb(tryte_string.as_trits()) - sponge.squeeze(hash_trits) - - hash_ = TransactionHash.from_trits(hash_trits) - - return cls( - hash_ = hash_, - signature_message_fragment = Fragment(tryte_string[0:2187]), - address = Address(tryte_string[2187:2268]), - value = int_from_trits(tryte_string[2268:2295].as_trits()), - legacy_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_hash = BundleHash(tryte_string[2349:2430]), - trunk_transaction_hash = TransactionHash(tryte_string[2430:2511]), - branch_transaction_hash = TransactionHash(tryte_string[2511:2592]), - tag = Tag(tryte_string[2592:2619]), - attachment_timestamp = int_from_trits(tryte_string[2619:2628].as_trits()), - attachment_timestamp_lower_bound = int_from_trits(tryte_string[2628:2637].as_trits()), - attachment_timestamp_upper_bound = int_from_trits(tryte_string[2637:2646].as_trits()), - nonce = Nonce(tryte_string[2646:2673]), - ) - - def __init__( - self, - hash_, # type: Optional[TransactionHash] - signature_message_fragment, # type: Optional[Fragment] - address, # type: Address - value, # type: int - timestamp, # type: int - current_index, # type: Optional[int] - last_index, # type: Optional[int] - bundle_hash, # type: Optional[BundleHash] - trunk_transaction_hash, # type: Optional[TransactionHash] - branch_transaction_hash, # type: Optional[TransactionHash] - tag, # type: Optional[Tag] - attachment_timestamp, # type: Optional[int] - attachment_timestamp_lower_bound, # type: Optional[int] - attachment_timestamp_upper_bound, # type: Optional[int] - nonce, # type: Optional[Nonce] - legacy_tag = None # type: Optional[Tag] - ): - self.hash = hash_ - """ - Transaction ID, generated by taking a hash of the transaction - trits. - """ - - self.bundle_hash = bundle_hash - """ - Bundle hash, generated by taking a hash of metadata from 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._legacy_tag = legacy_tag - """ - Optional classification legacy_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.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.tag = tag - """ - Optional classification tag applied to this transaction. - """ - - self.attachment_timestamp = attachment_timestamp - - self.attachment_timestamp_lower_bound = attachment_timestamp_lower_bound - - self.attachment_timestamp_upper_bound = attachment_timestamp_upper_bound - - self.signature_message_fragment = signature_message_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] - """ - 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 - - @property - def value_as_trytes(self): - # type: () -> TryteString - """ - 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 - def timestamp_as_trytes(self): - # type: () -> TryteString - """ - 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 - def current_index_as_trytes(self): - # type: () -> TryteString - """ - 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 - def last_index_as_trytes(self): - # type: () -> TryteString - """ - 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)) - - @property - def attachment_timestamp_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - attachment timestamp. - """ - #Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.attachment_timestamp, pad=27)) - - @property - def attachment_timestamp_lower_bound_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - attachment timestamp lower bound. +class Bundle(JsonSerializable, Sequence[Transaction]): """ - #Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.attachment_timestamp_lower_bound, pad=27)) + A collection of transactions, treated as an atomic unit when + attached to the Tangle. - @property - def attachment_timestamp_upper_bound_as_trytes(self): - # type: () -> TryteString - """ - Returns a TryteString representation of the transaction's - attachment timestamp upper bound. - """ - #Note that we are padding to 27 _trits_. - return TryteString.from_trits(trits_from_int(self.attachment_timestamp_upper_bound, pad=27)) + Note: unlike a block in a blockchain, bundles are not first-class + citizens in IOTA; only transactions get stored in the Tangle. - def as_json_compatible(self): - # type: () -> dict - """ - Returns a JSON-compatible representation of the object. + Instead, Bundles must be inferred by following linked transactions + with the same bundle hash. References: - - :py:class:`iota.json.JsonEncoder`. - """ - return { - 'hash_': self.hash, - 'signature_message_fragment': self.signature_message_fragment, - 'address': self.address, - 'value': self.value, - 'legacy_tag': self.legacy_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, - 'tag': self.tag, - 'attachment_timestamp': self.attachment_timestamp, - 'attachment_timestamp_lower_bound': self.attachment_timestamp_lower_bound, - 'attachment_timestamp_upper_bound': self.attachment_timestamp_upper_bound, - 'nonce': self.nonce, - } - - def as_tryte_string(self): - # type: () -> TransactionTrytes - """ - Returns a TryteString representation of the transaction. - """ - return TransactionTrytes( - self.signature_message_fragment - + self.address.address - + self.value_as_trytes - + self.legacy_tag - + 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.tag - + self.attachment_timestamp_as_trytes - + self.attachment_timestamp_lower_bound_as_trytes - + self.attachment_timestamp_upper_bound_as_trytes - + 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.legacy_tag - + self.timestamp_as_trytes - + self.current_index_as_trytes - + self.last_index_as_trytes - ) - - @property - def legacy_tag(self): - # type: () -> Tag - """ - Return the legacy tag of the transaction. - If no legacy tag was set, returns the tag instead. - """ - return self._legacy_tag or self.tag - - -class Bundle(JsonSerializable, Sequence[Transaction]): - """ - 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. - References: - :py:class:`iota.commands.extended.get_bundles.GetBundlesCommand` - """ - @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__() + @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)) - self.transactions = [] # type: List[Transaction] - if transactions: - self.transactions.extend( - sorted(transactions, key=attrgetter('current_index')) - ) + def __init__(self, transactions=None): + # type: (Optional[Iterable[Transaction]]) -> None + super(Bundle, self).__init__() - self._is_confirmed = None # type: Optional[bool] - """ - Whether this bundle has been confirmed by neighbor nodes. - Must be set manually. + self.transactions = [] # type: List[Transaction] + if transactions: + self.transactions.extend( + sorted(transactions, key=attrgetter('current_index')), + ) - References: - - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` - """ + self._is_confirmed = None # type: Optional[bool] + """ + Whether this bundle has been confirmed by neighbor nodes. + Must be set manually. - def __contains__(self, transaction): - # type: (Transaction) -> bool - return transaction in self.transactions + References: - def __getitem__(self, index): - # type: (int) -> Transaction - return self.transactions[index] + - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` + """ - def __iter__(self): - # type: () -> Iterator[Transaction] - return iter(self.transactions) - - 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. + def __contains__(self, transaction): + # type: (Transaction) -> bool + return transaction in self.transactions - This attribute must be set manually. + def __getitem__(self, index): + # type: (int) -> Transaction + return self.transactions[index] - References: - - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` - """ - return self._is_confirmed + def __iter__(self): + # type: () -> Iterator[Transaction] + return iter(self.transactions) - @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 + def __len__(self): + # type: () -> int + return len(self.transactions) - for txn in self: - txn.is_confirmed = new_is_confirmed + @property + def is_confirmed(self): + # type: () -> Optional[bool] + """ + Returns whether this bundle has been confirmed by neighbor + nodes. - @property - def hash(self): - # type: () -> Optional[BundleHash] - """ - Returns the hash of the bundle. + This attribute must be set manually. - This value is determined by inspecting the bundle's tail - transaction, so in a few edge cases, it may be incorrect. + References: - 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 - """ - Returns the tail transaction of the bundle. - """ - return self[0] + - :py:class:`iota.commands.extended.get_transfers.GetTransfersCommand` + """ + return self._is_confirmed - def get_messages(self, errors='drop'): - # type: (Text) -> List[Text] - """ - Attempts to decipher encoded messages from the transactions in the - bundle. - - :param errors: - How to handle trytes that can't be converted, or bytes that can't - be decoded using UTF-8: - - 'drop': drop the trytes from the result. - - 'strict': raise an exception. - - 'replace': replace with a placeholder character. - - 'ignore': omit the invalid tryte/byte sequence. - """ - decode_errors = 'strict' if errors == 'drop' else errors + @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 - messages = [] + for txn in self: + txn.is_confirmed = new_is_confirmed - for group in self.group_transactions(): - # Ignore inputs. - if group[0].value < 0: - continue + @property + def hash(self): + # type: () -> Optional[BundleHash] + """ + Returns the hash of the bundle. - message_trytes = TryteString(b'') - for txn in group: - message_trytes += txn.signature_message_fragment + This value is determined by inspecting the bundle's tail + transaction, so in a few edge cases, it may be incorrect. - if message_trytes: + If the bundle has no transactions, this method returns ``None``. + """ try: - messages.append(message_trytes.decode(decode_errors)) - except (TrytesDecodeError, UnicodeDecodeError): - if errors != 'drop': - raise - - return messages - - def as_tryte_strings(self, head_to_tail=False): - # type: (bool) -> List[TransactionTrytes] - """ - Returns TryteString representations of the transactions in this - bundle. - - :param head_to_tail: - Determines the order of the transactions: - - - ``True``: head txn first, tail txn last. - - ``False`` (default): tail txn first, head txn last. - - Note that the order is reversed by default, as this is the way - bundles are typically broadcast to the Tangle. - """ - transactions = self if head_to_tail else reversed(self) - return [t.as_tryte_string() for t in 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 group_transactions(self): - # type: () -> List[List[Transaction]] - """ - Groups transactions in the bundle by address. - """ - groups = [] - - if self: - last_txn = self.tail_transaction - current_group = [last_txn] - for current_txn in self.transactions[1:]: - # Transactions are grouped by address, so as long as the - # address stays consistent from one transaction to another, we - # are still in the same group. - if current_txn.address == last_txn.address: - current_group.append(current_txn) - else: - groups.append(current_group) - current_group = [current_txn] - - last_txn = current_txn - - if current_group: - groups.append(current_group) - - return groups - - + return self.tail_transaction.bundle_hash + except IndexError: + return None + + @property + def tail_transaction(self): + # type: () -> Transaction + """ + Returns the tail transaction of the bundle. + """ + return self[0] + + def get_messages(self, errors='drop'): + # type: (Text) -> List[Text] + """ + Attempts to decipher encoded messages from the transactions in + the bundle. + + :param errors: + How to handle trytes that can't be converted, or bytes that + can't be decoded using UTF-8: + + 'drop' + Drop the trytes from the result. + + 'strict' + Raise an exception. + + 'replace' + Replace with a placeholder character. + + 'ignore' + Omit the invalid tryte/byte sequence. + """ + decode_errors = 'strict' if errors == 'drop' else errors + + messages = [] + + for group in self.group_transactions(): + # Ignore inputs. + if group[0].value < 0: + continue + + message_trytes = TryteString(b'') + for txn in group: + message_trytes += txn.signature_message_fragment + + if message_trytes: + try: + messages.append(message_trytes.decode(decode_errors)) + except (TrytesDecodeError, UnicodeDecodeError): + if errors != 'drop': + raise + + return messages + + def as_tryte_strings(self, head_to_tail=False): + # type: (bool) -> List[TransactionTrytes] + """ + Returns TryteString representations of the transactions in this + bundle. + + :param head_to_tail: + Determines the order of the transactions: + + - ``True``: head txn first, tail txn last. + - ``False`` (default): tail txn first, head txn last. + + Note that the order is reversed by default, as this is the + way bundles are typically broadcast to the Tangle. + """ + transactions = self if head_to_tail else reversed(self) + return [t.as_tryte_string() for t in 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 group_transactions(self): + # type: () -> List[List[Transaction]] + """ + Groups transactions in the bundle by address. + """ + groups = [] + + if self: + last_txn = self.tail_transaction + current_group = [last_txn] + for current_txn in self.transactions[1:]: + # Transactions are grouped by address, so as long as the + # address stays consistent from one transaction to + # another, we are still in the same group. + if current_txn.address == last_txn.address: + current_group.append(current_txn) + else: + groups.append(current_group) + current_group = [current_txn] + + last_txn = current_txn + + if current_group: + groups.append(current_group) + + return groups diff --git a/iota/transaction/creation.py b/iota/transaction/creation.py index 9dd1d16..e4c89fd 100644 --- a/iota/transaction/creation.py +++ b/iota/transaction/creation.py @@ -2,8 +2,7 @@ from __future__ import absolute_import, division, print_function, \ unicode_literals -from typing import Iterable, Iterator, List, MutableSequence, Optional, \ - Sequence, Tuple +from typing import Iterable, Iterator, List, Optional, Sequence from six import PY2 @@ -33,8 +32,14 @@ class ProposedTransaction(Transaction): tangle and publish/store. """ - def __init__(self, address, value, tag=None, message=None, timestamp=None): - # type: (Address, int, Optional[Tag], Optional[TryteString], Optional[int]) -> None + def __init__( + self, + address, # type: Address + value, # type: int + tag=None, # type: Optional[Tag] + message=None, # type: Optional[TryteString] + timestamp=None, # type: Optional[int] + ): if not timestamp: timestamp = get_current_timestamp() @@ -44,7 +49,8 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): timestamp=timestamp, value=value, - # These values will be populated when the bundle is finalized. + # These values will be populated when the bundle is + # finalized. bundle_hash=None, current_index=None, hash_=None, @@ -54,8 +60,8 @@ def __init__(self, address, value, tag=None, message=None, timestamp=None): attachment_timestamp_lower_bound=0, attachment_timestamp_upper_bound=0, - # These values start out empty; they will be populated when the - # node does PoW. + # These values start out empty; they will be populated when + # the node does PoW. branch_transaction_hash=TransactionHash(b''), nonce=Nonce(b''), trunk_transaction_hash=TransactionHash(b''), @@ -91,10 +97,12 @@ def increment_legacy_tag(self): bundle hashes when finalizing a bundle. References: - - https://github.com/iotaledger/iota.lib.py/issues/84 + + - https://github.com/iotaledger/iota.lib.py/issues/84 """ - self._legacy_tag =\ + self._legacy_tag = ( Tag.from_trits(add_trits(self.legacy_tag.as_trits(), [1])) + ) Transfer = ProposedTransaction @@ -110,8 +118,12 @@ class ProposedBundle(Bundle, Sequence[ProposedTransaction]): unit when attached to the Tangle. """ - def __init__(self, transactions=None, inputs=None, change_address=None): - # type: (Optional[Iterable[ProposedTransaction]], Optional[Iterable[Address]], Optional[Address]) -> None + def __init__( + self, + transactions=None, # type: Optional[Iterable[ProposedTransaction]] + inputs=None, # type: Optional[Iterable[Address]] + change_address=None, # type: Optional[Address] + ): super(ProposedBundle, self).__init__() self._transactions = [] # type: List[ProposedTransaction] @@ -168,12 +180,13 @@ def balance(self): 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. + - 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) @@ -185,7 +198,6 @@ def tag(self): """ for txn in reversed(self): # type: ProposedTransaction if txn.tag: - # noinspection PyTypeChecker return txn.tag return Tag(b'') @@ -196,7 +208,8 @@ def as_json_compatible(self): Returns a JSON-compatible representation of the object. References: - - :py:class:`iota.json.JsonEncoder`. + + - :py:class:`iota.json.JsonEncoder`. """ return [txn.as_json_compatible() for txn in self] @@ -242,14 +255,15 @@ def add_inputs(self, inputs): """ Adds inputs to spend in the bundle. - Note that each input may require multiple transactions, in order to - hold the entire signature. + Note that each input may require multiple transactions, in order + to hold the entire signature. :param inputs: - Addresses to use as the inputs for this bundle. + 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. + .. 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.') @@ -340,13 +354,13 @@ def finalize(self): sponge = Kerl() last_index = len(self) - 1 - for (i, txn) in enumerate(self): # type: Tuple[int, ProposedTransaction] + for i, txn in enumerate(self): txn.current_index = i txn.last_index = last_index sponge.absorb(txn.get_signature_validation_trytes().as_trits()) - bundle_hash_trits = [0] * HASH_LENGTH # type: MutableSequence[int] + bundle_hash_trits = [0] * HASH_LENGTH sponge.squeeze(bundle_hash_trits) bundle_hash = BundleHash.from_trits(bundle_hash_trits) @@ -355,7 +369,9 @@ def finalize(self): # https://github.com/iotaledger/iota.lib.py/issues/84 if any(13 in part for part in normalize(bundle_hash)): # Increment the legacy tag and try again. - tail_transaction = self.tail_transaction # type: ProposedTransaction + tail_transaction = ( + self.tail_transaction + ) # type: ProposedTransaction tail_transaction.increment_legacy_tag() else: break @@ -381,12 +397,13 @@ def sign_inputs(self, key_generator): 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. + # 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 ' + 'Unable to sign input {input}; ' + '``key_index`` is None ' '(``exc.context`` has more info).'.format( input=txn.address, ), @@ -400,7 +417,8 @@ def sign_inputs(self, key_generator): if txn.address.security_level is None: raise with_context( exc=ValueError( - 'Unable to sign input {input}; ``security_level`` is None ' + 'Unable to sign input {input}; ' + '``security_level`` is None ' '(``exc.context`` has more info).'.format( input=txn.address, ), @@ -415,8 +433,8 @@ def sign_inputs(self, key_generator): i += txn.address.security_level else: - # No signature needed (nor even possible, in some cases); skip - # this transaction. + # No signature needed (nor even possible, in some + # cases); skip this transaction. i += 1 def sign_input_at(self, start_index, private_key): @@ -425,18 +443,20 @@ def sign_input_at(self, start_index, private_key): Signs the input at the specified index. :param start_index: - The index of the first input transaction. + The index of the first input transaction. - If necessary, the resulting signature will be split across - multiple transactions automatically (i.e., if an input has - ``security_level=2``, you still only need to call - :py:meth:`sign_input_at` once). + If necessary, the resulting signature will be split across + multiple transactions automatically (i.e., if an input has + ``security_level=2``, you still only need to call + :py:meth:`sign_input_at` once). :param private_key: - The private key that will be used to generate the signature. + The private key that will be used to generate the signature. - Important: be sure that the private key was generated using the - correct seed, or the resulting signature will be invalid! + .. important:: + Be sure that the private key was generated using the + correct seed, or the resulting signature will be + invalid! """ if not self.hash: raise RuntimeError('Cannot sign inputs until bundle is finalized.') @@ -452,8 +472,8 @@ def _create_input_transactions(self, addy): address=addy, tag=self.tag, - # Spend the entire address balance; if necessary, we will add a - # change transaction to the bundle. + # Spend the entire address balance; if necessary, we will + # add a change transaction to the bundle. value=-addy.balance, )) diff --git a/iota/transaction/types.py b/iota/transaction/types.py index 3a32c57..b92da78 100644 --- a/iota/transaction/types.py +++ b/iota/transaction/types.py @@ -1,97 +1,98 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from iota.crypto import FRAGMENT_LENGTH from iota.exceptions import with_context from iota.types import Hash, TryteString, TrytesCompatible __all__ = [ - 'BundleHash', - 'Fragment', - 'TransactionHash', - 'TransactionTrytes', - 'Nonce' + 'BundleHash', + 'Fragment', + 'TransactionHash', + 'TransactionTrytes', + 'Nonce' ] class BundleHash(Hash): - """ - A TryteString that acts as a bundle hash. - """ - pass + """ + A TryteString that acts as a bundle hash. + """ + pass class TransactionHash(Hash): - """ - A TryteString that acts as a transaction hash. - """ - pass + """ + A TryteString that acts as a transaction hash. + """ + pass class Fragment(TryteString): - """ - A signature/message fragment in a transaction. - """ - LEN = FRAGMENT_LENGTH + """ + A signature/message fragment in a transaction. + """ + LEN = FRAGMENT_LENGTH - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(Fragment, self).__init__(trytes, pad=self.LEN) + 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 - )), + 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, - }, - ) + context={ + 'trytes': trytes, + }, + ) class TransactionTrytes(TryteString): - """ - A TryteString representation of a Transaction. - """ - LEN = 2673 - - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(TransactionTrytes, 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, - }, - ) + """ + A TryteString representation of a Transaction. + """ + LEN = 2673 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(TransactionTrytes, 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 Nonce(TryteString): - """ - A TryteString that acts as a transaction nonce. - """ - LEN = 27 - - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(Nonce, 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, - }, - ) \ No newline at end of file + """ + A TryteString that acts as a transaction nonce. + """ + LEN = 27 + + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Nonce, 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, + }, + ) diff --git a/iota/transaction/utils.py b/iota/transaction/utils.py index e7a6297..b9505f8 100644 --- a/iota/transaction/utils.py +++ b/iota/transaction/utils.py @@ -1,73 +1,77 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from calendar import timegm as unix_timestamp from datetime import datetime - from typing import Text from iota import STANDARD_UNITS from iota.exceptions import with_context __all__ = [ - 'convert_value_to_standard_unit', - 'get_current_timestamp', + 'convert_value_to_standard_unit', + 'get_current_timestamp', ] def convert_value_to_standard_unit(value, symbol='i'): - # type: (Text, Text) -> float - """ + # type: (Text, Text) -> float + """ Converts between any two standard units of iota. :param value: - Value (affixed) to convert. For example: '1.618 Mi'. + Value (affixed) to convert. For example: '1.618 Mi'. :param symbol: - Unit symbol of iota to convert to. For example: 'Gi'. + Unit symbol of iota to convert to. For example: 'Gi'. :return: - Float as units of given symbol to convert to. - """ - try: - # Get input value - value_tuple = value.split() - amount = float(value_tuple[0]) - except (ValueError, IndexError, AttributeError): - raise with_context(ValueError('Value to convert is not valid.'), - context = { - 'value': value, - }, - ) + Float as units of given symbol to convert to. + """ + try: + # Get input value + value_tuple = value.split() + amount = float(value_tuple[0]) + except (ValueError, IndexError, AttributeError): + raise with_context( + ValueError('Value to convert is not valid.'), + + context={ + 'value': value, + }, + ) + + try: + # Set unit symbols and find factor/multiplier. + unit_symbol_from = value_tuple[1] + unit_factor_from = float(STANDARD_UNITS[unit_symbol_from]) + unit_factor_to = float(STANDARD_UNITS[symbol]) + except (KeyError, IndexError): + # Invalid symbol or no factor + raise with_context( + ValueError('Invalid IOTA unit.'), + + context={ + 'value': value, + 'symbol': symbol, + }, + ) - try: - # Set unit symbols and find factor/multiplier. - unit_symbol_from = value_tuple[1] - unit_factor_from = float(STANDARD_UNITS[unit_symbol_from]) - unit_factor_to = float(STANDARD_UNITS[symbol]) - except (KeyError, IndexError): - # Invalid symbol or no factor - raise with_context(ValueError('Invalid IOTA unit.'), - context = { - 'value': value, - 'symbol': symbol, - }, - ) + return amount * (unit_factor_from / unit_factor_to) - return amount * (unit_factor_from / unit_factor_to) def get_current_timestamp(): - # type: () -> int - """ - Returns the current timestamp, used to set ``timestamp`` for new - :py:class:`ProposedTransaction` objects. + # 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. - # http://stackoverflow.com/q/2775864/ - return unix_timestamp(datetime.utcnow().timetuple()) + 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. + # http://stackoverflow.com/q/2775864/ + return unix_timestamp(datetime.utcnow().timetuple()) diff --git a/iota/transaction/validator.py b/iota/transaction/validator.py index d1088eb..e9308cd 100644 --- a/iota/transaction/validator.py +++ b/iota/transaction/validator.py @@ -1,19 +1,17 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals -from typing import Generator, List, Optional, Text, Tuple +from typing import Generator, List, Optional, Text from iota.crypto.kerl import Kerl from iota.crypto.signing import validate_signature_fragments from iota.transaction.base import Bundle, Transaction __all__ = [ - 'BundleValidator', + 'BundleValidator', ] - -# # In very rare cases, the IOTA protocol may switch hash algorithms. # When this happens, the IOTA Foundation will create a snapshot, so # that all new objects on the Tangle use the new hash algorithm. @@ -21,235 +19,250 @@ # However, the snapshot will still contain references to addresses # created using the legacy hash algorithm, so the bundle validator has # to be able to use that as a fallback when validation fails. -# -SUPPORTED_SPONGE = Kerl -LEGACY_SPONGE = None # Curl +SUPPORTED_SPONGE = Kerl +LEGACY_SPONGE = None # Curl 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 a bundle and its transactions for problems. """ - 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, None, None] - """ - Creates a generator that does all the work. - """ - # Group transactions by address to make it easier to iterate over - # inputs. - grouped_transactions = self.bundle.group_transactions() - - # Define a few expected values. - bundle_hash = self.bundle.hash - last_index = len(self.bundle) - 1 - - # Track a few others as we go along. - balance = 0 - - # Check indices and balance first. - # Note that we use a counter to keep track of the current index, - # since at this point we can't trust that the transactions have - # correct ``current_index`` values. - counter = 0 - for group in grouped_transactions: - for txn in group: - balance += txn.value - - if txn.bundle_hash != bundle_hash: - yield 'Transaction {i} has invalid bundle hash.'.format( - i = counter, - ) - - if txn.current_index != counter: - yield ( - 'Transaction {i} has invalid current index value ' - '(expected {i}, actual {actual}).'.format( - actual = txn.current_index, - i = counter, - ) - ) - - 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 = counter, + 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. + """ + 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, None, None] + """ + Creates a generator that does all the work. + """ + # Group transactions by address to make it easier to iterate + # over inputs. + grouped_transactions = self.bundle.group_transactions() + + # Define a few expected values. + bundle_hash = self.bundle.hash + last_index = len(self.bundle) - 1 + + # Track a few others as we go along. + balance = 0 + + # Check indices and balance first. + # Note that we use a counter to keep track of the current index, + # since at this point we can't trust that the transactions have + # correct ``current_index`` values. + counter = 0 + for group in grouped_transactions: + for txn in group: + balance += txn.value + + if txn.bundle_hash != bundle_hash: + yield 'Transaction {i} has invalid bundle hash.'.format( + i=counter, + ) + + if txn.current_index != counter: + yield ( + 'Transaction {i} has invalid current index value ' + '(expected {i}, actual {actual}).'.format( + actual=txn.current_index, + i=counter, + ) + ) + + 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=counter, + ) + ) + + counter += 1 + + # Bundle must be balanced (spends must match inputs). + if balance != 0: + yield ( + 'Bundle has invalid balance ' + '(expected 0, actual {actual}).'.format( + actual=balance, + ) ) - ) - counter += 1 - - # Bundle must be balanced (spends must match inputs). - if balance != 0: - yield ( - 'Bundle has invalid balance (expected 0, actual {actual}).'.format( - actual = balance, + # Signature validation is only meaningful if the transactions + # are otherwise valid. + if not self._errors: + signature_validation_queue = [] # type: List[List[Transaction]] + + for group in grouped_transactions: + # Signature validation only applies to inputs. + if group[0].value >= 0: + continue + + validate_group_signature = True + for j, txn in enumerate(group): + if (j > 0) and (txn.value != 0): + # Input is malformed; signature fragments after + # the first should have zero value. + yield ( + 'Transaction {i} has invalid value ' + '(expected 0, actual {actual}).'.format( + actual=txn.value, + + # If we get to this point, we know that + # the ``current_index`` value for each + # transaction can be trusted. + i=txn.current_index, + ) + ) + + # We won't be able to validate the signature, + # but continue anyway, so that we can check that + # the other transactions in the group have the + # correct ``value``. + validate_group_signature = False + continue + + # After collecting the signature fragment from each + # transaction in the group, queue them up to run through + # the validator. + # + # We have to perform signature validation separately so + # that we can try different algorithms (for + # backwards-compatibility). + # + # References: + # + # - https://github.com/iotaledger/kerl#kerl-integration-in-iota + if validate_group_signature: + signature_validation_queue.append(group) + + # Once we've finished checking the attributes from each + # transaction in the bundle, go back and validate + # signatures. + if signature_validation_queue: + # ``yield from`` is an option here, but for + # compatibility with Python 2 clients, we will do it the + # old-fashioned way. + for error in self._get_bundle_signature_errors( + signature_validation_queue + ): + yield error + + def _get_bundle_signature_errors(self, groups): + # type: (List[List[Transaction]]) -> List[Text] + """ + Validates the signature fragments in the bundle. + + :return: + List of error messages. + If empty, signature fragments are valid. + """ + # Start with the currently-supported hash algo. + current_pos = None + current_errors = [] + for current_pos, group in enumerate(groups): + error = self._get_group_signature_error(group, SUPPORTED_SPONGE) + if error: + current_errors.append(error) + + # Pause and retry with the legacy algo. + break + + # If validation failed, then go back and try with the legacy + # algo (only applies if we are currently transitioning to a new + # algo). + if current_errors and LEGACY_SPONGE: + for group in groups: + # noinspection PyTypeChecker + if self._get_group_signature_error(group, LEGACY_SPONGE): + # Legacy algo doesn't work, either; no point in + # continuing. + break + else: + # If we get here, then we were able to validate the + # signature fragments successfully using the legacy + # algorithm. + return [] + + # If we get here, then validation also failed when using the + # legacy algorithm. + + # At this point, we know that the bundle is invalid, but we will + # continue validating with the supported algorithm anyway, so + # that we can return an error message for every invalid input. + current_errors.extend(filter(None, ( + self._get_group_signature_error(group, SUPPORTED_SPONGE) + for group in groups[current_pos + 1:] + ))) + + return current_errors + + @staticmethod + def _get_group_signature_error(group, sponge_type): + # type: (List[Transaction], type) -> Optional[Text] + """ + Validates the signature fragments for a group of transactions + using the specified sponge type. + + Note: this method assumes that the transactions in the group + have already passed basic validation (see + :py:meth:`_create_validator`). + + :return: + - ``None``: Indicates that the signature fragments are valid. + - ``Text``: Error message indicating the fragments are invalid. + """ + validate_group_signature = validate_signature_fragments( + fragments=[txn.signature_message_fragment for txn in group], + hash_=group[0].bundle_hash, + public_key=group[0].address, + sponge_type=sponge_type, ) - ) - - # Signature validation is only meaningful if the transactions are - # otherwise valid. - if not self._errors: - signature_validation_queue = [] # type: List[List[Transaction]] - - for group in grouped_transactions: - # Signature validation only applies to inputs. - if group[0].value >= 0: - continue - - validate_group_signature = True - for j, txn in enumerate(group): # type: Tuple[int, Transaction] - if (j > 0) and (txn.value != 0): - # Input is malformed; signature fragments after the first - # should have zero value. - yield ( - 'Transaction {i} has invalid value ' - '(expected 0, actual {actual}).'.format( - actual = txn.value, - - # If we get to this point, we know that the - # ``current_index`` value for each transaction can be - # trusted. - i = txn.current_index, - ) - ) - # We won't be able to validate the signature, but continue - # anyway, so that we can check that the other transactions - # in the group have the correct ``value``. - validate_group_signature = False - continue - - # After collecting the signature fragment from each transaction - # in the group, queue them up to run through the validator. - # - # We have to perform signature validation separately so that we - # can try different algorithms (for backwards-compatibility). - # - # References: - # - https://github.com/iotaledger/kerl#kerl-integration-in-iota if validate_group_signature: - signature_validation_queue.append(group) - - # Once we've finished checking the attributes from each - # transaction in the bundle, go back and validate signatures. - if signature_validation_queue: - for error in self._get_bundle_signature_errors(signature_validation_queue): - yield error + return None - def _get_bundle_signature_errors(self, groups): - # type: (List[List[Transaction]]) -> List[Text] - """ - Validates the signature fragments in the bundle. - - :return: - List of error messages. If empty, signature fragments are valid. - """ - # Start with the currently-supported hash algo. - current_pos = None - current_errors = [] - for current_pos, group in enumerate(groups): # type: Tuple[int, List[Transaction]] - error = self._get_group_signature_error(group, SUPPORTED_SPONGE) - if error: - current_errors.append(error) - - # Pause and retry with the legacy algo. - break - - # If validation failed, then go back and try with the legacy algo - # (only applies if we are currently transitioning to a new algo). - if current_errors and LEGACY_SPONGE: - for group in groups: - # noinspection PyTypeChecker - if self._get_group_signature_error(group, LEGACY_SPONGE): - # Legacy algo doesn't work, either; no point in continuing. - break - else: - # If we get here, then we were able to validate the signature - # fragments successfully using the legacy algorithm. - return [] - - # If we get here, then validation also failed when using the legacy - # algorithm. - - # At this point, we know that the bundle is invalid, but we will - # continue validating with the supported algorithm anyway, so that - # we can return an error message for every invalid input. - current_errors.extend(filter(None, ( - self._get_group_signature_error(group, SUPPORTED_SPONGE) - for group in groups[current_pos+1:] - ))) - - return current_errors - - @staticmethod - def _get_group_signature_error(group, sponge_type): - # type: (List[Transaction], type) -> Optional[Text] - """ - Validates the signature fragments for a group of transactions using - the specified sponge type. - - Note: this method assumes that the transactions in the group have - already passed basic validation (see :py:meth:`_create_validator`). - - :return: - - ``None``: Indicates that the signature fragments are valid. - - ``Text``: Error message indicating the fragments are invalid. - """ - validate_group_signature =\ - validate_signature_fragments( - fragments = [txn.signature_message_fragment for txn in group], - hash_ = group[0].bundle_hash, - public_key = group[0].address, - sponge_type = sponge_type, - ) - - if validate_group_signature: - return None - - return ( - 'Transaction {i} has invalid signature ' - '(using {fragments} fragments).'.format( - fragments = len(group), - i = group[0].current_index, - ) - ) + return ( + 'Transaction {i} has invalid signature ' + '(using {fragments} fragments).'.format( + fragments=len(group), + i=group[0].current_index, + ) + ) From a74ec33ffe9b8b522757004450e53f0443f41802 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 16 Jun 2018 11:04:15 +1200 Subject: [PATCH 16/32] =?UTF-8?q?[#145]=20Reformatted=20API=20module=20for?= =?UTF-8?q?=20PEP-8=20=F0=9F=A4=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iota/__init__.py | 3 +- iota/api.py | 1911 ++++++++++++++++++++++++---------------------- 2 files changed, 1002 insertions(+), 912 deletions(-) diff --git a/iota/__init__.py b/iota/__init__.py index ad412f1..6a48287 100644 --- a/iota/__init__.py +++ b/iota/__init__.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals # Define a few magic constants. DEFAULT_PORT = 14265 @@ -27,7 +27,6 @@ 'Pi': 1000000000000000 } - # Activate codecs. from .codecs import * diff --git a/iota/api.py b/iota/api.py index 7c2ad69..c451791 100644 --- a/iota/api.py +++ b/iota/api.py @@ -1,1004 +1,1095 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Dict, Iterable, Optional, Text +from six import with_metaclass + from iota import AdapterSpec, Address, ProposedTransaction, Tag, \ - TransactionHash, TransactionTrytes, TryteString, TrytesCompatible + TransactionHash, TransactionTrytes, TryteString, TrytesCompatible from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import BaseCommand, CustomCommand, core, \ - discover_commands, extended + discover_commands, extended from iota.commands.extended.helpers import Helpers from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed -from six import with_metaclass __all__ = [ - 'InvalidCommand', - 'Iota', - 'StrictIota', + 'InvalidCommand', + 'Iota', + 'StrictIota', ] class InvalidCommand(ValueError): - """ - Indicates that an invalid command name was specified. - """ - pass - - -class ApiMeta(type): - """ - Manages command registries for IOTA API base classes. - """ - def __init__(cls, name, bases=None, attrs=None): - super(ApiMeta, cls).__init__(name, bases, attrs) - - if not hasattr(cls, 'commands'): - cls.commands = {} - - # Copy command registry from base class to derived class, but - # in the event of a conflict, preserve the derived class' - # commands. - commands = {} - for base in bases: - if isinstance(base, ApiMeta): - commands.update(base.commands) - - if commands: - commands.update(cls.commands) - cls.commands = commands - - -class StrictIota(with_metaclass(ApiMeta)): - """ - 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 - """ - commands = discover_commands('iota.commands.core') - - 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. + Indicates that an invalid command name was specified. """ - super(StrictIota, self).__init__() + pass - if not isinstance(adapter, BaseAdapter): - adapter = resolve_adapter(adapter) - - self.adapter = adapter # type: BaseAdapter - self.testnet = testnet - - def __getattr__(self, command): - # type: (Text) -> BaseCommand - """ - Creates a pre-configured command instance. - This method will only return commands supported by the API class. - - If you want to execute an arbitrary API command, use - :py:meth:`create_command`. - - :param command: - The name of the command to create. - - References: - - https://iota.readme.io/docs/making-requests +class ApiMeta(type): """ - # Fix an error when invoking :py:func:`help`. - # https://github.com/iotaledger/iota.lib.py/issues/41 - if command == '__name__': - # noinspection PyTypeChecker - return None - - try: - command_class = self.commands[command] - except KeyError: - raise InvalidCommand( - '{cls} does not support {command!r} command.'.format( - cls = type(self).__name__, - command = command, - ), - ) - - return command_class(self.adapter) - - def create_command(self, command): - # type: (Text) -> CustomCommand + Manages command registries for IOTA API base classes. """ - Creates a pre-configured CustomCommand instance. - This method is useful for invoking undocumented or experimental - methods, or if you just want to troll your node for awhile. + def __init__(cls, name, bases=None, attrs=None): + super(ApiMeta, cls).__init__(name, bases, attrs) - :param command: - The name of the command to create. - """ - return CustomCommand(self.adapter, command) + if not hasattr(cls, 'commands'): + cls.commands = {} - @property - def default_min_weight_magnitude(self): - # type: () -> int - """ - Returns the default ``min_weight_magnitude`` value to use for API - requests. - """ - return 9 if self.testnet else 14 - - def add_neighbors(self, uris): - # type: (Iterable[Text]) -> dict - """ - Add one or more neighbors to the node. Lasts until the node is - restarted. + # Copy command registry from base class to derived class, but + # in the event of a conflict, preserve the derived class' + # commands. + commands = {} + for base in bases: + if isinstance(base, ApiMeta): + commands.update(base.commands) - :param uris: - Use format ``udp://:``. - Example: ``add_neighbors(['udp://example.com:14265'])`` + if commands: + commands.update(cls.commands) + cls.commands = commands - References: - - https://iota.readme.io/docs/addneighors - """ - return core.AddNeighborsCommand(self.adapter)(uris=uris) - - def attach_to_tangle( - self, - trunk_transaction, - branch_transaction, - trytes, - min_weight_magnitude = None, - ): - # 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 - 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 :py:meth:`broadcast_transactions` and - :py:meth:`store_transactions`. - - References: - - https://iota.readme.io/docs/attachtotangle - """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - - return core.AttachToTangleCommand(self.adapter)( - trunkTransaction = trunk_transaction, - branchTransaction = branch_transaction, - minWeightMagnitude = min_weight_magnitude, - trytes = trytes, - ) - - def broadcast_transactions(self, trytes): - # type: (Iterable[TryteString]) -> dict +class StrictIota(with_metaclass(ApiMeta)): """ - Broadcast a list of transactions to all neighbors. + API to send HTTP requests for communicating with an IOTA node. - The input trytes for this call are provided by - :py:meth:`attach_to_tangle`. + 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/broadcasttransactions - """ - return core.BroadcastTransactionsCommand(self.adapter)(trytes=trytes) - def check_consistency(self, tails): - # type: (Iterable[TransactionHash]) -> dict - """ - Used to ensure tail resolves to a consistent ledger which is necessary to - validate before attempting promotionChecks transaction hashes for - promotability. - - This is called with a pending transaction (or more of them) and it will - tell you if it is still possible for this transaction (or all the - transactions simultaneously if you give more than one) to be confirmed, or - not (because it conflicts with another already confirmed transaction). - - :param tails: - Transaction hashes. Must be tail transactions. - - :return: - Dict containing the following:: - { - 'state': bool, - - 'info': str, - This field will only exist set if `state` is False. - } - """ - return core.CheckConsistencyCommand(self.adapter)( - tails = tails, - ) - - def find_transactions( - self, - bundles = None, - addresses = None, - tags = None, - approvees = None, - ): - # type: (Optional[Iterable[TransactionHash]], Optional[Iterable[Address]], Optional[Iterable[Tag]], Optional[Iterable[TransactionHash]]) -> 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 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 - """ - return core.FindTransactionsCommand(self.adapter)( - bundles = bundles, - addresses = addresses, - tags = tags, - approvees = approvees, - ) - - def get_balances(self, addresses, threshold=100): - # type: (Iterable[Address], int) -> dict + - https://iota.readme.io/docs/getting-started """ - Similar to :py:meth:`get_inclusion_states`. Returns the confirmed - balance which a list of addresses have at the latest confirmed - milestone. + commands = discover_commands('iota.commands.core') + + 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.testnet = testnet + + def __getattr__(self, command): + # type: (Text) -> BaseCommand + """ + Creates a pre-configured command instance. + + This method will only return commands supported by the API + class. + + If you want to execute an arbitrary API command, use + :py:meth:`create_command`. + + :param command: + The name of the command to create. + + References: + + - https://iota.readme.io/docs/making-requests + """ + # Fix an error when invoking :py:func:`help`. + # https://github.com/iotaledger/iota.lib.py/issues/41 + if command == '__name__': + # noinspection PyTypeChecker + return None + + try: + command_class = self.commands[command] + except KeyError: + raise InvalidCommand( + '{cls} does not support {command!r} command.'.format( + cls=type(self).__name__, + command=command, + ), + ) + + return command_class(self.adapter) + + def create_command(self, command): + # type: (Text) -> CustomCommand + """ + Creates a pre-configured CustomCommand instance. + + 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 create. + """ + 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 9 if self.testnet else 14 + + 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 ``://:``. + Example: ``add_neighbors(['udp://example.com:14265'])`` + + .. note:: + These URIs are for node-to-node communication (e.g., + weird things will happen if you specify a node's HTTP + API URI here). + + References: + + - https://iota.readme.io/docs/addneighors + """ + return core.AddNeighborsCommand(self.adapter)(uris=uris) + + def attach_to_tangle( + self, + trunk_transaction, # type: TransactionHash + branch_transaction, # type: TransactionHash + trytes, # type: Iterable[TryteString] + min_weight_magnitude=None, # type: Optional[int] + ): + # type: (...) -> 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 :py:meth:`broadcast_transactions` and + :py:meth:`store_transactions`. + + References: + + - https://iota.readme.io/docs/attachtotangle + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return core.AttachToTangleCommand(self.adapter)( + trunkTransaction=trunk_transaction, + branchTransaction=branch_transaction, + minWeightMagnitude=min_weight_magnitude, + trytes=trytes, + ) + + def broadcast_transactions(self, trytes): + # type: (Iterable[TryteString]) -> dict + """ + Broadcast a list of transactions to all neighbors. + + The input trytes for this call are provided by + :py:meth:`attach_to_tangle`. + + References: + + - https://iota.readme.io/docs/broadcasttransactions + """ + return core.BroadcastTransactionsCommand(self.adapter)(trytes=trytes) + + def check_consistency(self, tails): + # type: (Iterable[TransactionHash]) -> dict + """ + Used to ensure tail resolves to a consistent ledger which is + necessary to validate before attempting promotionChecks + transaction hashes for promotability. + + This is called with a pending transaction (or more of them) and + it will tell you if it is still possible for this transaction + (or all the transactions simultaneously if you give more than + one) to be confirmed, or not (because it conflicts with another + already confirmed transaction). + + :param tails: + Transaction hashes. Must be tail transactions. + + :return: + Dict with the following structure:: + + { + 'state': bool, + + 'info': str, + This field will only exist set if ``state`` is + ``False``. + } + """ + return core.CheckConsistencyCommand(self.adapter)( + tails=tails, + ) + + def find_transactions( + self, + bundles=None, # type: Optional[Iterable[TransactionHash]] + addresses=None, # type: Optional[Iterable[Address]] + tags=None, # type: Optional[Iterable[Tag]] + approvees=None, # type: Optional[Iterable[TransactionHash]] + ): + # type: (...) -> 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 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 + """ + return core.FindTransactionsCommand(self.adapter)( + bundles=bundles, + addresses=addresses, + tags=tags, + approvees=approvees, + ) + + def get_balances(self, addresses, threshold=100): + # type: (Iterable[Address], int) -> dict + """ + 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. 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. + + References: + + - https://iota.readme.io/docs/getbalances + """ + return core.GetBalancesCommand(self.adapter)( + addresses=addresses, + threshold=threshold, + ) + + def get_inclusion_states(self, transactions, tips): + # 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 + 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. + + References: + + - https://iota.readme.io/docs/getinclusionstates + """ + return core.GetInclusionStatesCommand(self.adapter)( + transactions=transactions, + tips=tips, + ) + + 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. + + References: + + - https://iota.readme.io/docs/getneighborsactivity + """ + return core.GetNeighborsCommand(self.adapter)() + + def get_node_info(self): + # type: () -> dict + """ + Returns information about the node. + + References: - 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. + - https://iota.readme.io/docs/getnodeinfo + """ + return core.GetNodeInfoCommand(self.adapter)() - :param addresses: - List of addresses to get the confirmed balance for. + def get_tips(self): + # type: () -> dict + """ + Returns the list of tips (transactions which have no other + transactions referencing them). - :param threshold: - Confirmation threshold. + References: - References: - - https://iota.readme.io/docs/getbalances - """ - return core.GetBalancesCommand(self.adapter)( - addresses = addresses, - threshold = threshold, - ) + - https://iota.readme.io/docs/gettips + - https://iota.readme.io/docs/glossary#iota-terms + """ + return core.GetTipsCommand(self.adapter)() + + def get_transactions_to_approve(self, depth): + # type: (int) -> dict + """ + Tip selection which returns ``trunkTransaction`` and + ``branchTransaction``. - def get_inclusion_states(self, transactions, tips): - # 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 - network or not. You can search for multiple tips (and thus, - milestones) to get past inclusion states of transactions. + :param depth: + Determines how many bundles to go back to when finding the + transactions to approve. - :param transactions: - List of transactions you want to get the inclusion state for. + The higher the depth value, the more "babysitting" the node + will perform for the network (as it will confirm more + transactions that way). - :param tips: - List of tips (including milestones) you want to search for the - inclusion state. + References: - References: - - https://iota.readme.io/docs/getinclusionstates - """ - return core.GetInclusionStatesCommand(self.adapter)( - transactions = transactions, - tips = tips, - ) + - https://iota.readme.io/docs/gettransactionstoapprove + """ + return core.GetTransactionsToApproveCommand(self.adapter)(depth=depth) - def get_neighbors(self): - # type: () -> dict - """ - Returns the set of neighbors the node is connected with, as well as - their activity count. + def get_trytes(self, hashes): + # type: (Iterable[TransactionHash]) -> dict + """ + Returns the raw transaction data (trytes) of one or more + transactions. - The activity counter is reset after restarting IRI. + References: - References: - - https://iota.readme.io/docs/getneighborsactivity - """ - return core.GetNeighborsCommand(self.adapter)() + - https://iota.readme.io/docs/gettrytes + """ + return core.GetTrytesCommand(self.adapter)(hashes=hashes) - def get_node_info(self): - # type: () -> dict - """ - Returns information about the node. + def interrupt_attaching_to_tangle(self): + # type: () -> dict + """ + Interrupts and completely aborts the :py:meth:`attach_to_tangle` + process. - References: - - https://iota.readme.io/docs/getnodeinfo - """ - return core.GetNodeInfoCommand(self.adapter)() + References: - def get_tips(self): - # type: () -> dict - """ - Returns the list of tips (transactions which have no other - transactions referencing them). + - https://iota.readme.io/docs/interruptattachingtotangle + """ + return core.InterruptAttachingToTangleCommand(self.adapter)() - References: - - https://iota.readme.io/docs/gettips - - https://iota.readme.io/docs/glossary#iota-terms - """ - return core.GetTipsCommand(self.adapter)() + def remove_neighbors(self, uris): + # type: (Iterable[Text]) -> dict + """ + Removes one or more neighbors from the node. Lasts until the + node is restarted. - def get_transactions_to_approve(self, depth): - # type: (int) -> dict - """ - Tip selection which returns ``trunkTransaction`` and - ``branchTransaction``. + :param uris: + Use format ``://:``. + Example: `remove_neighbors(['udp://example.com:14265'])` - :param depth: - Determines how many bundles to go back to when finding the - transactions to approve. + References: - The higher the depth value, the more "babysitting" the node will - perform for the network (as it will confirm more transactions - that way). + - https://iota.readme.io/docs/removeneighors + """ + return core.RemoveNeighborsCommand(self.adapter)(uris=uris) - References: - - https://iota.readme.io/docs/gettransactionstoapprove - """ - return core.GetTransactionsToApproveCommand(self.adapter)(depth=depth) + def store_transactions(self, trytes): + # type: (Iterable[TryteString]) -> dict + """ + Store transactions into local storage. - def get_trytes(self, hashes): - # type: (Iterable[TransactionHash]) -> dict - """ - Returns the raw transaction data (trytes) of one or more - transactions. + The input trytes for this call are provided by + :py:meth:`attach_to_tangle`. - References: - - https://iota.readme.io/docs/gettrytes - """ - return core.GetTrytesCommand(self.adapter)(hashes=hashes) + References: - def interrupt_attaching_to_tangle(self): - # type: () -> dict - """ - Interrupts and completely aborts the :py:meth:`attach_to_tangle` - process. + - https://iota.readme.io/docs/storetransactions + """ + return core.StoreTransactionsCommand(self.adapter)(trytes=trytes) - References: - - https://iota.readme.io/docs/interruptattachingtotangle - """ - return core.InterruptAttachingToTangleCommand(self.adapter)() + def were_addresses_spent_from(self, addresses): + # type: (Iterable[Address]) -> dict + """ + Check if a list of addresses was ever spent from, in the current + epoch, or in previous epochs. - def remove_neighbors(self, uris): - # type: (Iterable[Text]) -> dict - """ - Removes one or more neighbors from the node. Lasts until the node - is restarted. + :param addresses: + List of addresses to check. - :param uris: - Use format ``udp://:``. - Example: `remove_neighbors(['udp://example.com:14265'])` + References: - References: - - https://iota.readme.io/docs/removeneighors - """ - return core.RemoveNeighborsCommand(self.adapter)(uris=uris) + - https://iota.readme.io/docs/wereaddressesspentfrom + """ + return core.WereAddressesSpentFromCommand(self.adapter)( + addresses=addresses, + ) - def store_transactions(self, trytes): - # type: (Iterable[TryteString]) -> dict - """ - Store transactions into local storage. - - The input trytes for this call are provided by - :py:meth:`attach_to_tangle`. - References: - - https://iota.readme.io/docs/storetransactions - """ - return core.StoreTransactionsCommand(self.adapter)(trytes=trytes) - - def were_addresses_spent_from(self, addresses): - # type: (Iterable[Address]) -> dict +class Iota(StrictIota): """ - Check if a list of addresses was ever spent from, in the current - epoch, or in previous epochs. - - :param addresses: - List of addresses to check. + Implements the core API, plus additional wrapper methods for common + operations. References: - - https://iota.readme.io/docs/wereaddressesspentfrom - """ - return core.WereAddressesSpentFromCommand(self.adapter)( - addresses = addresses, - ) - - -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 - """ - commands = discover_commands('iota.commands.extended') - - def __init__(self, adapter, seed=None, testnet=False): - # type: (AdapterSpec, Optional[TrytesCompatible], bool) -> 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, testnet) - - self.seed = Seed(seed) if seed else Seed.random() - self.helpers = Helpers(self) - - def broadcast_and_store(self, trytes): - # type: (Iterable[TransactionTrytes]) -> dict - """ - Broadcasts and stores a set of transaction trytes. - - :return: - Dict with the following structure:: - - { - 'trytes': List[TransactionTrytes], - List of TransactionTrytes that were broadcast. - Same as the input ``trytes``. - } - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore - """ - return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes) - - def get_account_data(self, start=0, stop=None, inclusion_states=False): - # type: (int, Optional[int], bool) -> dict - """ - More comprehensive version of :py:meth:`get_transfers` that returns - addresses and account balance in addition to bundles. - - This function is useful in getting all the relevant information of - your account. - - :param start: - Starting key index. - - :param stop: - Stop before this index. - Note that this parameter behaves like the ``stop`` attribute in a - :py:class:`slice` object; the stop index is *not* included in the - result. - - If ``None`` (default), then this method will check every address - until it finds one without any transfers. - - :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: - Dict containing the following values:: - - { - 'addresses': List[Address], - List of generated addresses. - Note that this list may include unused addresses. - - 'balance': int, - Total account balance. Might be 0. - - 'bundles': List[Bundle], - List of bundles with transactions to/from this account. - } - """ - return extended.GetAccountDataCommand(self.adapter)( - seed = self.seed, - start = start, - stop = stop, - inclusionStates = inclusion_states, - ) - - def get_bundles(self, transaction): - # type: (TransactionHash) -> dict - """ - Returns the bundle(s) associated with the specified transaction - hash. - - :param transaction: - Transaction hash. Must be a tail transaction. - - :return: - Dict with the following structure:: - - { - '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 - bundles fails validation. - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle - """ - return extended.GetBundlesCommand(self.adapter)(transaction=transaction) - - def get_inputs(self, start=0, stop=None, threshold=None, security_level=None): - # type: (int, Optional[int], Optional[int], Optional[int]) -> dict - """ - Gets all possible inputs of a seed and returns them with the total - balance. - - This is either done deterministically (by generating all addresses - until :py:meth:`find_transactions` returns an empty result), or by - providing a key range to search. - - :param start: - Starting key index. - Defaults to 0. - - :param stop: - Stop before this index. - Note that this parameter behaves like the ``stop`` attribute in a - :py:class:`slice` object; the stop index is *not* included in the - result. - - If ``None`` (default), then this method will not stop until it - finds an unused address. - - :param threshold: - If set, determines the minimum threshold for a successful result: - - - As soon as this threshold is reached, iteration will stop. - - If the command runs out of addresses before the threshold is - reached, an exception is raised. - - Note that this method does not attempt to "optimize" the result - (e.g., smallest number of inputs, get as close to ``threshold`` - as possible, etc.); it simply accumulates inputs in order until - the threshold is met. - - If ``threshold`` is 0, the first address in the key range with - a non-zero balance will be returned (if it exists). - - If ``threshold`` is ``None`` (default), this method will return - **all** inputs in the specified key range. - - :param security_level: - Number of iterations to use when generating new addresses (see get_new_addresses). - This value must be between 1 and 3, inclusive. - If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 - - :return: - Dict with the following structure:: - - { - 'inputs': List[Address], - Addresses with nonzero balances that can be used as - inputs. - - 'totalBalance': int, - Aggregate balance from all matching addresses. - } - - Note that each Address in the result has its ``balance`` - attribute set. - - Example:: - - response = iota.get_inputs(...) - - input0 = response['inputs'][0] # type: Address - input0.balance # 42 - - :raise: - - :py:class:`iota.adapter.BadApiResponse` if ``threshold`` is not - met. Not applicable if ``threshold`` is ``None``. - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs - """ - return extended.GetInputsCommand(self.adapter)( - seed = self.seed, - start = start, - stop = stop, - threshold = threshold, - securityLevel=security_level - ) - - 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: - Dict with one boolean per transaction hash in ``hashes``:: - - { - : , - ... - } - """ - return extended.GetLatestInclusionCommand(self.adapter)(hashes=hashes) - - def get_new_addresses( - self, - index = 0, - count = 1, - security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL, - checksum = False, - ): - # type: (int, Optional[int], int, bool) -> dict - """ - Generates one or more new addresses from the seed. - - :param index: - Specify the index of the new address (must be >= 1). - - :param count: - Number of addresses to generate (must be >= 1). - - Note: This is more efficient than calling ``get_new_address`` - inside a loop. - - If ``None``, this method will scan the Tangle to find the next - available unused address and return that. - - :param security_level: - Number of iterations to use when generating new addresses. - - Larger values take longer, but the resulting signatures are more - secure. - - This value must be between 1 and 3, inclusive. - - :param checksum: - Specify whether to return the address with the checksum. - Defaults to False. - - :return: - Dict with the following items:: - - { - 'addresses': List[Address], - Always a list, even if only one address was generated. - } - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress """ - return extended.GetNewAddressesCommand(self.adapter)( - count = count, - index = index, - securityLevel = security_level, - checksum = checksum, - seed = self.seed, - ) - - def get_transfers(self, start=0, stop=None, inclusion_states=False): - # type: (int, Optional[int], bool) -> dict - """ - Returns all transfers associated with the seed. - - :param start: - Starting key index. - - :param stop: - Stop before this index. - Note that this parameter behaves like the ``stop`` attribute in a - :py:class:`slice` object; the stop index is *not* included in the - result. + commands = discover_commands('iota.commands.extended') - If ``None`` (default), then this method will check every address - until it finds one without any transfers. + def __init__(self, adapter, seed=None, testnet=False): + # type: (AdapterSpec, Optional[TrytesCompatible], bool) -> None + """ + :param seed: + Seed used to generate new addresses. + If not provided, a random one will be generated. - :param inclusion_states: - Whether to also fetch the inclusion states of the transfers. + .. note:: + This value is never transferred to the node/network. + """ + super(Iota, self).__init__(adapter, testnet) - This requires an additional API call to the node, so it is - disabled by default. + self.seed = Seed(seed) if seed else Seed.random() + self.helpers = Helpers(self) - :return: - Dict containing the following values:: + def broadcast_and_store(self, trytes): + # type: (Iterable[TransactionTrytes]) -> dict + """ + Broadcasts and stores a set of transaction trytes. - { - 'bundles': List[Bundle], - Matching bundles, sorted by tail transaction timestamp. - } + :return: + Dict with the following structure:: + + { + 'trytes': List[TransactionTrytes], + List of TransactionTrytes that were broadcast. + Same as the input ``trytes``. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#broadcastandstore + """ + return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes) + + def get_account_data(self, start=0, stop=None, inclusion_states=False): + # type: (int, Optional[int], bool) -> dict + """ + More comprehensive version of :py:meth:`get_transfers` that + returns addresses and account balance in addition to bundles. + + This function is useful in getting all the relevant information + of your account. + + :param start: + Starting key index. + + :param stop: + Stop before this index. + + Note that this parameter behaves like the ``stop`` attribute + in a :py:class:`slice` object; the stop index is *not* + included in the result. + + If ``None`` (default), then this method will check every + address until it finds one without any transfers. + + :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: + Dict with the following structure:: + + { + 'addresses': List[Address], + List of generated addresses. + + Note that this list may include unused + addresses. + + 'balance': int, + Total account balance. Might be 0. - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers - """ - return extended.GetTransfersCommand(self.adapter)( - seed = self.seed, - start = start, - stop = stop, - inclusionStates = inclusion_states, - ) - - def prepare_transfer(self, transfers, inputs=None, change_address=None, security_level=None): - # type: (Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], Optional[int]) -> dict - """ - Prepares transactions to be broadcast to the Tangle, by generating - the correct bundle, as well as choosing and signing the inputs (for - value transfers). + 'bundles': List[Bundle], + List of bundles with transactions to/from this + account. + } + """ + return extended.GetAccountDataCommand(self.adapter)( + seed=self.seed, + start=start, + stop=stop, + inclusionStates=inclusion_states, + ) - :param transfers: - Transaction objects to prepare. + def get_bundles(self, transaction): + # type: (TransactionHash) -> dict + """ + Returns the bundle(s) associated with the specified transaction + hash. - :param inputs: - List of addresses used to fund the transfer. - Ignored for zero-value transfers. + :param transaction: + Transaction hash. Must be a tail transaction. - If not provided, addresses will be selected automatically by - scanning the Tangle for unspent inputs. Note: this could take - awhile to complete. + :return: + Dict with the following structure:: - :param change_address: - If inputs are provided, any unspent amount will be sent to this - address. + { + 'bundles': List[Bundle], + List of matching bundles. Note that this value is + always a list, even if only one bundle was found. + } - If not specified, a change address will be generated - automatically. + :raise: + - :py:class:`iota.adapter.BadApiResponse` if any of the + bundles fails validation. - :param security_level: - Number of iterations to use when generating new addresses (see get_new_addresses). - This value must be between 1 and 3, inclusive. - If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 + References: - :return: - Dict containing the following values:: + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle + """ + return extended.GetBundlesCommand(self.adapter)(transaction=transaction) - { - 'trytes': List[TransactionTrytes], - Raw trytes for the transactions in the bundle, ready to - be provided to :py:meth:`send_trytes`. - } + def get_inputs( + self, + start=0, + stop=None, + threshold=None, + security_level=None, + ): + # type: (int, Optional[int], Optional[int], Optional[int]) -> dict + """ + Gets all possible inputs of a seed and returns them, along with + the total balance. - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#preparetransfers - """ - return extended.PrepareTransferCommand(self.adapter)( - seed = self.seed, - transfers = transfers, - inputs = inputs, - changeAddress = change_address, - securityLevel = security_level, - ) - - def promote_transaction( - self, - transaction, - depth, - min_weight_magnitude = None, - ): - # type: (TransactionHash, int, Optional[int]) -> dict - """ - Promotes a transaction by adding spam on top of it. + This is either done deterministically (by generating all + addresses until :py:meth:`find_transactions` returns an empty + result), or by providing a key range to search. - :return: - Dict containing the following values:: + :param start: + Starting key index. + Defaults to 0. + + :param stop: + Stop before this index. - { - 'bundle': Bundle, - The newly-published bundle. - } - """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - - return extended.PromoteTransactionCommand(self.adapter)( - transaction = transaction, - depth = depth, - minWeightMagnitude = min_weight_magnitude, - ) - - def replay_bundle( - self, - transaction, - depth, - min_weight_magnitude = None, - ): - # type: (TransactionHash, int, Optional[int]) -> dict - """ - 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. - - :param depth: - Depth at which to attach the bundle. + Note that this parameter behaves like the ``stop`` attribute + in a :py:class:`slice` object; the stop index is *not* + included in the result. + + If ``None`` (default), then this method will not stop until + it finds an unused address. + + :param threshold: + If set, determines the minimum threshold for a successful + result: + + - As soon as this threshold is reached, iteration will stop. + - If the command runs out of addresses before the threshold + is reached, an exception is raised. + + .. note:: + This method does not attempt to "optimize" the result + (e.g., smallest number of inputs, get as close to + ``threshold`` as possible, etc.); it simply accumulates + inputs in order until the threshold is met. + + If ``threshold`` is 0, the first address in the key range + with a non-zero balance will be returned (if it exists). + + If ``threshold`` is ``None`` (default), this method will + return **all** inputs in the specified key range. + + :param security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + + :return: + Dict with the following structure:: + + { + 'inputs': List[Address], + Addresses with nonzero balances that can be used + as inputs. + + 'totalBalance': int, + Aggregate balance from all matching addresses. + } - :param min_weight_magnitude: - Min weight magnitude, used by the node to calibrate Proof of - Work. + Note that each Address in the result has its ``balance`` + attribute set. + + Example: + + .. code-block:: python + + response = iota.get_inputs(...) + + input0 = response['inputs'][0] # type: Address + input0.balance # 42 + + :raise: + - :py:class:`iota.adapter.BadApiResponse` if ``threshold`` + is not met. Not applicable if ``threshold`` is ``None``. + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getinputs + """ + return extended.GetInputsCommand(self.adapter)( + seed=self.seed, + start=start, + stop=stop, + threshold=threshold, + securityLevel=security_level + ) + + 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. - If not provided, a default value will be used. + Effectively, this is ``getNodeInfo`` + ``getInclusionStates``. - :return: - Dict containing the following values:: + :param hashes: + Iterable of transaction hashes. - { - 'trytes': List[TransactionTrytes], - Raw trytes that were published to the Tangle. - } + :return: + Dict with the following structure:: - 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 extended.ReplayBundleCommand(self.adapter)( - transaction = transaction, - depth = depth, - minWeightMagnitude = min_weight_magnitude, - ) - - def send_transfer( - self, - depth, - transfers, - inputs = None, - change_address = None, - min_weight_magnitude = None, - security_level = None, - ): - # type: (int, Iterable[ProposedTransaction], Optional[Iterable[Address]], Optional[Address], Optional[int]) -> dict - """ - Prepares a set of transfers and creates the bundle, then attaches - the bundle to the Tangle, and broadcasts and stores the - transactions. + { + "states": Dict[TransactionHash, bool] + Dict with one boolean per transaction hash in + ``hashes``. + } + """ + return extended.GetLatestInclusionCommand(self.adapter)(hashes=hashes) - :param depth: - Depth at which to attach the bundle. + def get_new_addresses( + self, + index=0, + count=1, + security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL, + checksum=False, + ): + # type: (int, Optional[int], int, bool) -> dict + """ + Generates one or more new addresses from the seed. + + :param index: + The key index of the first new address to generate (must be + >= 1). + + :param count: + Number of addresses to generate (must be >= 1). + + .. tip:: + This is more efficient than calling ``get_new_address`` + inside a loop. + + If ``None``, this method will progressively generate + addresses and scan the Tangle until it finds one that has no + transactions referencing it. + + :param security_level: + Number of iterations to use when generating new addresses. + + Larger values take longer, but the resulting signatures are + more secure. + + This value must be between 1 and 3, inclusive. + + :param checksum: + Specify whether to return the address with the checksum. + Defaults to ``False``. + + :return: + Dict with the following structure:: + + { + 'addresses': List[Address], + Always a list, even if only one address was + generated. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getnewaddress + """ + return extended.GetNewAddressesCommand(self.adapter)( + count=count, + index=index, + securityLevel=security_level, + checksum=checksum, + seed=self.seed, + ) + + def get_transfers(self, start=0, stop=None, inclusion_states=False): + # type: (int, Optional[int], bool) -> dict + """ + Returns all transfers associated with the seed. + + :param start: + Starting key index. + + :param stop: + Stop before this index. + + Note that this parameter behaves like the ``stop`` attribute + in a :py:class:`slice` object; the stop index is *not* + included in the result. + + If ``None`` (default), then this method will check every + address until it finds one without any transfers. + + :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: + Dict with the following structure:: + + { + 'bundles': List[Bundle], + Matching bundles, sorted by tail transaction + timestamp. + + This value is always a list, even if only one + bundle was found. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#gettransfers + """ + return extended.GetTransfersCommand(self.adapter)( + seed=self.seed, + start=start, + stop=stop, + inclusionStates=inclusion_states, + ) + + def prepare_transfer( + self, + transfers, # type: Iterable[ProposedTransaction] + inputs=None, # type: Optional[Iterable[Address]] + change_address=None, # type: Optional[Address] + security_level=None, # type: Optional[int] + ): + # type: (...) -> dict + """ + 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. + Ignored for zero-value transfers. + + If not provided, addresses will be selected automatically by + scanning the Tangle for unspent inputs. Depending on how + many transfers you've already sent with your seed, this + process could take awhile. + + :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 security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + + :return: + Dict with the following structure:: + + { + 'trytes': List[TransactionTrytes], + 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 + """ + return extended.PrepareTransferCommand(self.adapter)( + seed=self.seed, + transfers=transfers, + inputs=inputs, + changeAddress=change_address, + securityLevel=security_level, + ) + + def promote_transaction( + self, + transaction, + depth, + min_weight_magnitude=None, + ): + # type: (TransactionHash, int, Optional[int]) -> dict + """ + Promotes a transaction by adding spam on top of it. + + :return: + Dict with the following structure:: + + { + 'bundle': Bundle, + The newly-published bundle. + } + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return extended.PromoteTransactionCommand(self.adapter)( + transaction=transaction, + depth=depth, + minWeightMagnitude=min_weight_magnitude, + ) + + def replay_bundle( + self, + transaction, + depth, + min_weight_magnitude=None, + ): + # type: (TransactionHash, int, Optional[int]) -> dict + """ + 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. + + :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. + + If not provided, a default value will be used. + + :return: + Dict with the following structure:: + + { + 'trytes': List[TransactionTrytes], + Raw trytes that were published to the Tangle. + } + + 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 extended.ReplayBundleCommand(self.adapter)( + transaction=transaction, + depth=depth, + minWeightMagnitude=min_weight_magnitude, + ) + + def send_transfer( + self, + depth, # type: int + transfers, # type: Iterable[ProposedTransaction] + inputs=None, # type: Optional[Iterable[Address]] + change_address=None, # type: Optional[Address] + min_weight_magnitude=None, # type: Optional[int] + security_level=None, # type: Optional[int] + ): + # type: (...) -> dict + """ + 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. + + If not provided, a default value will be used. + + :param security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + + :return: + Dict with the following structure:: + + { + 'bundle': Bundle, + The newly-published 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 extended.SendTransferCommand(self.adapter)( + seed=self.seed, + depth=depth, + transfers=transfers, + inputs=inputs, + changeAddress=change_address, + minWeightMagnitude=min_weight_magnitude, + securityLevel=security_level, + ) + + def send_trytes(self, trytes, depth, min_weight_magnitude=None): + # type: (Iterable[TransactionTrytes], int, Optional[int]) -> dict + """ + 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. + + If not provided, a default value will be used. + + :return: + Dict with the following structure:: + + { + 'trytes': List[TransactionTrytes], + Raw trytes that were published to the Tangle. + } + + References: + + - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes + """ + if min_weight_magnitude is None: + min_weight_magnitude = self.default_min_weight_magnitude + + return extended.SendTrytesCommand(self.adapter)( + trytes=trytes, + depth=depth, + minWeightMagnitude=min_weight_magnitude, + ) + + def is_reattachable(self, addresses): + # type: (Iterable[Address]) -> dict + """ + This API function helps you to determine whether you should + replay a transaction or make a new one (either with the same + input, or a different one). - :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. - - If not provided, a default value will be used. - - :param security_level: - Number of iterations to use when generating new addresses (see get_new_addresses). - This value must be between 1 and 3, inclusive. - If not set, defaults to AddressGenerator.DEFAULT_SECURITY_LEVEL = 2 - - :return: - Dict containing the following values:: - - { - 'bundle': Bundle, - The newly-published 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 extended.SendTransferCommand(self.adapter)( - seed = self.seed, - depth = depth, - transfers = transfers, - inputs = inputs, - changeAddress = change_address, - minWeightMagnitude = min_weight_magnitude, - securityLevel = security_level, - ) - - def send_trytes(self, trytes, depth, min_weight_magnitude=None): - # type: (Iterable[TransactionTrytes], int, Optional[int]) -> dict - """ - 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. - - If not provided, a default value will be used. - - :return: - Dict containing the following values:: - - { - 'trytes': List[TransactionTrytes], - Raw trytes that were published to the Tangle. - } - - References: - - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#sendtrytes - """ - if min_weight_magnitude is None: - min_weight_magnitude = self.default_min_weight_magnitude - - return extended.SendTrytesCommand(self.adapter)( - trytes = trytes, - depth = depth, - minWeightMagnitude = min_weight_magnitude, - ) - - def is_reattachable(self, addresses): - # type: (Iterable[Address]) -> dict - """ - This API function helps you to determine whether you should replay a - transaction or make a new one (either with the same input, or a different one). - What this function does, is it takes one or more input addresses (i.e. from spent transactions) - as input and then checks whether any transactions with a value transferred are confirmed. - If yes, it means that this input address has already been successfully used in a different - transaction and as such you should no longer replay the transaction. - - :param addresses: - List of addresses. - - :return: - Dict containing the following values:: - { - 'reattachable': List[bool], - Always a list, even if only one address was queried. - } - """ - return extended.IsReattachableCommand(self.adapter)( - addresses=addresses - ) + This method takes one or more input addresses (i.e. from spent + transactions) as input and then checks whether any transactions + with a value transferred are confirmed. + + If yes, it means that this input address has already been + successfully used in a different transaction, and as such you + should no longer replay the transaction. + + :param addresses: + List of addresses. + + :return: + Dict with the following structure:: + + { + 'reattachable': List[bool], + Always a list, even if only one address was queried. + } + """ + return extended.IsReattachableCommand(self.adapter)( + addresses=addresses + ) From 1dcad0a1bc7e7a1e37f311bf8fd540ae3d9265e0 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 16 Jun 2018 11:06:05 +1200 Subject: [PATCH 17/32] [#145] Reformatted codecs module for PEP-8. --- iota/codecs.py | 363 +++++++++++++++++++++++++------------------------ 1 file changed, 185 insertions(+), 178 deletions(-) diff --git a/iota/codecs.py b/iota/codecs.py index 640eb08..afbddb0 100644 --- a/iota/codecs.py +++ b/iota/codecs.py @@ -1,213 +1,220 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from codecs import Codec, CodecInfo, register as lookup_function from warnings import warn -from iota.exceptions import with_context from six import PY3, binary_type +from iota.exceptions import with_context + __all__ = [ - 'AsciiTrytesCodec', - 'TrytesDecodeError', + 'AsciiTrytesCodec', + 'TrytesDecodeError', ] class TrytesDecodeError(ValueError): - """ - Indicates that a tryte string could not be decoded to bytes. - """ - pass + """ + Indicates that a tryte string could not be decoded to bytes. + """ + pass class AsciiTrytesCodec(Codec): - """ - Legacy codec for converting byte strings into trytes, and vice versa. - - This method encodes each pair of trytes as an ASCII code point (and - vice versa when decoding). - - The end result requires more space than if the trytes were converted - mathematically, but because the result is ASCII, it's easier to work - with. - - Think of this kind of like Base 64 for balanced ternary (: - """ - name = 'trytes_ascii' - - compat_name = 'trytes' - """ - Old name for this codec. - Note: Will be removed in PyOTA v2.1! - """ - - # :bc: Without the bytearray cast, Python 2 will populate the dict - # with characters instead of integers. - # noinspection SpellCheckingInspection - 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): - """ - Returns information used by the codecs library to configure the - codec for use. """ - codec = cls() + Legacy codec for converting byte strings into trytes, and vice + versa. - codec_info = { - 'encode': codec.encode, - 'decode': codec.decode, - } + This method encodes each pair of trytes as an ASCII code point (and + vice versa when decoding). - # 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 + The end result requires more space than if the trytes were converted + mathematically, but because the result is ASCII, it's easier to work + with. - return CodecInfo(**codec_info) + Think of this kind of like Base 64 for balanced ternary (: + """ + name = 'trytes_ascii' - # noinspection PyShadowingBuiltins - def encode(self, input, errors='strict'): + compat_name = 'trytes' """ - Encodes a byte string into trytes. + Old name for this codec. + Note: Will be removed in PyOTA v2.1! """ - if isinstance(input, memoryview): - input = input.tobytes() - if not isinstance(input, (binary_type, bytearray)): - raise with_context( - exc = TypeError("Can't encode {type}; byte string expected.".format( - type = type(input).__name__, - )), - - context = { - 'input': input, - }, - ) + # :bc: Without the bytearray cast, Python 2 will populate the dict + # with characters instead of integers. + # noinspection SpellCheckingInspection + alphabet = dict(enumerate(bytearray(b'9ABCDEFGHIJKLMNOPQRSTUVWXYZ'))) + """ + Used to encode bytes into trytes. + """ - # :bc: In Python 2, iterating over a byte string yields characters - # instead of integers. - if not isinstance(input, bytearray): - input = bytearray(input) + index = dict(zip(alphabet.values(), alphabet.keys())) + """ + Used to decode trytes into bytes. + """ - trytes = bytearray() + @classmethod + def get_codec_info(cls): + """ + Returns information used by the codecs library to configure the + codec for use. + """ + codec = cls() + + 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'): + """ + Encodes a byte string into trytes. + """ + if isinstance(input, memoryview): + input = input.tobytes() + + if not isinstance(input, (binary_type, bytearray)): + 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. + if not isinstance(input, bytearray): + input = bytearray(input) + + trytes = bytearray() + + 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 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. + 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 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'?' + + 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 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'?' + + return binary_type(bytes_), len(input) - for c in input: - second, first = divmod(c, len(self.alphabet)) - trytes.append(self.alphabet[first]) - trytes.append(self.alphabet[second]) +@lookup_function +def check_trytes_codec(encoding): + """ + Determines which codec to use for the specified encoding. - return binary_type(trytes), len(input) + References: - # noinspection PyShadowingBuiltins - def decode(self, input, errors='strict'): - """ - Decodes a tryte string into bytes. + - https://docs.python.org/3/library/codecs.html#codecs.register """ - if isinstance(input, memoryview): - input = input.tobytes() - - if not isinstance(input, (binary_type, bytearray)): - 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. - 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 with_context( - exc = TrytesDecodeError( - "'{name}' codec can't decode value; " - "tryte sequence has odd length.".format( - name = self.name, - ), + if encoding == AsciiTrytesCodec.name: + return AsciiTrytesCodec.get_codec_info() + + elif encoding == AsciiTrytesCodec.compat_name: + warn( + '"{old_codec}" codec will be removed in PyOTA v2.1. ' + 'Use "{new_codec}" instead.'.format( + new_codec=AsciiTrytesCodec.name, + old_codec=AsciiTrytesCodec.compat_name, ), - context = { - 'input': input, - }, - ) - elif errors == 'replace': - bytes_ += b'?' - - continue - - try: - bytes_.append( - self.index[first] - + (self.index[second] * len(self.index)) + DeprecationWarning, ) - except ValueError: - # This combination of trytes yields a value > 255 when - # decoded. Naturally, we can't represent this using ASCII. - if errors == 'strict': - 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'?' + return AsciiTrytesCodec.get_codec_info() - return binary_type(bytes_), len(input) - - -@lookup_function -def check_trytes_codec(encoding): - """ - Determines which codec to use for the specified encoding. - - References: - - https://docs.python.org/3/library/codecs.html#codecs.register - """ - if encoding == AsciiTrytesCodec.name: - return AsciiTrytesCodec.get_codec_info() - - elif encoding == AsciiTrytesCodec.compat_name: - warn( - '"{old_codec}" codec will be removed in PyOTA v2.1. ' - 'Use "{new_codec}" instead.'.format( - new_codec = AsciiTrytesCodec.name, - old_codec = AsciiTrytesCodec.compat_name, - ), - - DeprecationWarning, - ) - return AsciiTrytesCodec.get_codec_info() - - return None + return None From 1879c0109b83053ebffd89c6636586044f5c4bac Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 16 Jun 2018 11:25:01 +1200 Subject: [PATCH 18/32] [#145] Reformatted rest of `iota` package for PEP-8. --- iota/exceptions.py | 41 +- iota/filters.py | 335 +++++----- iota/json.py | 102 +-- iota/trits.py | 160 ++--- iota/types.py | 1518 +++++++++++++++++++++++--------------------- 5 files changed, 1121 insertions(+), 1035 deletions(-) diff --git a/iota/exceptions.py b/iota/exceptions.py index eaa71e1..31129c7 100644 --- a/iota/exceptions.py +++ b/iota/exceptions.py @@ -1,25 +1,36 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals + +__all__ = [ + 'with_context', +] def with_context(exc, context): - # type: (Exception, dict) -> Exception - """ - Attaches a ``context`` value to an Exception. + # type: (Exception, dict) -> Exception + """ + Attaches a ``context`` value to an Exception. + + Before: + + .. code-block:: python - Before:: + exc = Exception('Frog blast the vent core!') + exc.context = { ... } + raise exc - exc = Exception('Frog blast the vent core!') - exc.context = { ... } - raise exc + After: - After:: + .. code-block:: python - raise with_context(Exception('Frog blast the vent core!'), { ... }) - """ - if not hasattr(exc, 'context'): - exc.context = {} + raise with_context( + exc=Exception('Frog blast the vent core!'), + context={ ... }, + ) + """ + if not hasattr(exc, 'context'): + exc.context = {} - exc.context.update(context) - return exc + exc.context.update(context) + return exc diff --git a/iota/filters.py b/iota/filters.py index 1c1ffdb..2c98067 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Text @@ -13,192 +13,201 @@ 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' - CODE_NO_SECURITY_LEVEL = 'no_security_level' + """ + Validates an incoming value as a generated :py:class:`Address` (must + have ``key_index`` set). + """ + CODE_NO_KEY_INDEX = 'no_key_index' + CODE_NO_SECURITY_LEVEL = 'no_security_level' - templates = { - CODE_NO_KEY_INDEX: - 'Address must have ``key_index`` attribute set.', + templates = { + CODE_NO_KEY_INDEX: + 'Address must have ``key_index`` attribute set.', - CODE_NO_SECURITY_LEVEL: - 'Address must have ``security_level`` attribute set.', - } + CODE_NO_SECURITY_LEVEL: + 'Address must have ``security_level`` attribute set.', + } - def _apply(self, value): - value = self._filter(value, f.Type(Address)) # type: Address + def _apply(self, value): + value = self._filter(value, f.Type(Address)) # type: Address - if self._has_errors: - return None + if self._has_errors: + return None - if value.key_index is None: - return self._invalid_value(value, self.CODE_NO_KEY_INDEX) + if value.key_index is None: + return self._invalid_value(value, self.CODE_NO_KEY_INDEX) - if value.security_level is None: - return self._invalid_value(value, self.CODE_NO_SECURITY_LEVEL) + if value.security_level is None: + return self._invalid_value(value, self.CODE_NO_SECURITY_LEVEL) - return value + return value class NodeUri(f.BaseFilter): - """ - Validates a string as a node URI. - """ - SCHEMES = {'tcp', 'udp'} - """ - Allowed schemes for node URIs. - """ + """ + Validates a string as a node URI. + """ + SCHEMES = {'tcp', 'udp'} + """ + Allowed schemes for node URIs. + """ - CODE_NOT_NODE_URI = 'not_node_uri' + CODE_NOT_NODE_URI = 'not_node_uri' - templates = { - CODE_NOT_NODE_URI: 'This value does not appear to be a valid node URI.', - } + templates = { + CODE_NOT_NODE_URI: + '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 + def _apply(self, value): + value = self._filter(value, f.Type(text_type)) # type: Text - if self._has_errors: - return None + if self._has_errors: + return None - parsed = compat.urllib_parse.urlparse(value) + parsed = compat.urllib_parse.urlparse(value) - if parsed.scheme not in self.SCHEMES: - return self._invalid_value(value, self.CODE_NOT_NODE_URI) + if parsed.scheme not in self.SCHEMES: + return self._invalid_value(value, self.CODE_NOT_NODE_URI) - return value + return value +# noinspection PyPep8Naming @filter_macro def SecurityLevel(): - """ - Generates a filter chain for validating a security level. - """ - return ( - f.Type(int) | - f.Min(1) | - f.Max(3) | - f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL) - ) + """ + Generates a filter chain for validating a security level. + """ + return ( + f.Type(int) | + f.Min(1) | + f.Max(3) | + f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL) + ) class Trytes(f.BaseFilter): - """ - Validates a sequence as a sequence of trytes. - """ - 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): - # noinspection PyTypeChecker - value =\ - self._filter( - filter_chain = f.Type((binary_type, bytearray, text_type, TryteString)), - value = value, - ) # 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: - return self._invalid_value(value, self.CODE_NOT_TRYTES, exc_info=True) - - 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: - return self._invalid_value( - value = value, - reason = self.CODE_WRONG_FORMAT, - exc_info = True, - - template_vars = { - 'result_type': self.result_type.__name__, - }, - ) + """ + 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): + # noinspection PyTypeChecker + value = self._filter( + filter_chain=f.Type( + (binary_type, bytearray, text_type, TryteString) + ), + + value=value, + ) # 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: + return self._invalid_value( + value=value, + reason=self.CODE_NOT_TRYTES, + exc_info=True, + ) + + 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: + return self._invalid_value( + value=value, + reason=self.CODE_WRONG_FORMAT, + exc_info=True, + + template_vars={ + 'result_type': self.result_type.__name__, + }, + ) class AddressNoChecksum(Trytes): - """ - Validates a sequence as an Address then chops off the checksum if it exists - """ - ADDRESS_BAD_CHECKSUM = 'address_bad_checksum' - - templates = { - ADDRESS_BAD_CHECKSUM: - 'Checksum is {supplied_checksum}, should be {expected_checksum}?', - } - - def __init__(self): - super(AddressNoChecksum, self).__init__(result_type=Address) - - def _apply(self, value): - super(AddressNoChecksum, self)._apply(value) - - if self._has_errors: - return None - - # Possible it's still just a TryteString - if not isinstance(value, Address): - value = Address(value) - - # Bail out if we have a bad checksum - if value.checksum and not value.is_checksum_valid(): - return self._invalid_value( - value = value, - reason = self.ADDRESS_BAD_CHECKSUM, - exc_info = True, - - context = { - 'supplied_checksum': value.checksum, - 'expected_checksum': value.with_valid_checksum().checksum, - }, - ) - - return Address(value.address) + """ + Validates a sequence as an Address, then chops off the checksum if + present. + """ + ADDRESS_BAD_CHECKSUM = 'address_bad_checksum' + + templates = { + ADDRESS_BAD_CHECKSUM: + 'Checksum is {supplied_checksum}, should be {expected_checksum}?', + } + + def __init__(self): + super(AddressNoChecksum, self).__init__(result_type=Address) + + def _apply(self, value): + super(AddressNoChecksum, self)._apply(value) + + if self._has_errors: + return None + + # Possible it's still just a TryteString. + if not isinstance(value, Address): + value = Address(value) + + # Bail out if we have a bad checksum. + if value.checksum and not value.is_checksum_valid(): + return self._invalid_value( + value=value, + reason=self.ADDRESS_BAD_CHECKSUM, + exc_info=True, + + context={ + 'supplied_checksum': value.checksum, + 'expected_checksum': value.with_valid_checksum().checksum, + }, + ) + + return Address(value.address) diff --git a/iota/json.py b/iota/json.py index eab810b..61e6c7f 100644 --- a/iota/json.py +++ b/iota/json.py @@ -1,67 +1,75 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from abc import ABCMeta, abstractmethod as abstract_method from json.encoder import JSONEncoder as BaseJsonEncoder -from typing import Iterable, Mapping, Text +from typing import Iterable, Mapping 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`. + Interface for classes that can be safely converted to JSON. """ - raise NotImplementedError( - 'Not implemented in {cls}.'.format(cls=type(self).__name__), - ) - def _repr_pretty_(self, p, cycle): - """ - Makes JSON-serializable objects play nice with IPython's default - pretty-printer. + @abstract_method + def as_json_compatible(self): + """ + Returns a JSON-compatible representation of the object. - Sadly, :py:func:`pprint.pprint` does not have a similar mechanism. + References: - References: - - http://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html - - :py:meth:`IPython.lib.pretty.RepresentationPrinter.pretty` - - :py:func:`pprint._safe_repr` - """ - # type: (JsonSerializable, bool) -> Text - class_name = type(self).__name__ + - :py:class:`iota.json.JsonEncoder`. + """ + raise NotImplementedError( + 'Not implemented in {cls}.'.format(cls=type(self).__name__), + ) + + def _repr_pretty_(self, p, cycle): + """ + Makes JSON-serializable objects play nice with IPython's default + pretty-printer. + + Sadly, :py:func:`pprint.pprint` does not have a similar + mechanism. + + References: - if cycle: - p.text('{cls}(...)'.format( - cls = class_name, - )) - else: - with p.group(len(class_name)+1, '{cls}('.format(cls=class_name), ')'): - prepared = self.as_json_compatible() - if isinstance(prepared, Mapping): - p.text('**') - elif isinstance(prepared, Iterable): - p.text('*') + - http://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html + - :py:meth:`IPython.lib.pretty.RepresentationPrinter.pretty` + - :py:func:`pprint._safe_repr` + """ + class_name = type(self).__name__ - p.pretty(prepared) + if cycle: + p.text('{cls}(...)'.format( + cls=class_name, + )) + else: + with p.group( + len(class_name) + 1, + '{cls}('.format(cls=class_name), + ')', + ): + prepared = self.as_json_compatible() + + if isinstance(prepared, Mapping): + p.text('**') + elif isinstance(prepared, Iterable): + p.text('*') + + p.pretty(prepared) -# noinspection PyClassHasNoInit class JsonEncoder(BaseJsonEncoder): - """ - JSON encoder with support for :py:class:`JsonSerializable`. - """ - def default(self, o): - if isinstance(o, JsonSerializable): - return o.as_json_compatible() - - return super(JsonEncoder, self).default(o) + """ + JSON encoder with support for :py:class:`JsonSerializable`. + """ + + def default(self, o): + if isinstance(o, JsonSerializable): + return o.as_json_compatible() + + return super(JsonEncoder, self).default(o) diff --git a/iota/trits.py b/iota/trits.py index 872ad92..189af50 100644 --- a/iota/trits.py +++ b/iota/trits.py @@ -2,127 +2,131 @@ """ Provides functions for manipulating sequences of trits. -Based on: +Based on https://github.com/iotaledger/iota.lib.js/blob/v0.4.2/lib/crypto/helpers/adder.js """ from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import Iterable, List, Optional, Sequence, Tuple __all__ = [ - 'add_trits', - 'int_from_trits', - 'trits_from_int', + 'add_trits', + 'int_from_trits', + 'trits_from_int', ] def add_trits(left, right): - # type: (Sequence[int], Sequence[int]) -> List[int] - """ - Adds two sequences of trits together. + # type: (Sequence[int], Sequence[int]) -> List[int] + """ + Adds two sequences of trits together. - The result is a list of trits equal in length to the longer of the - two sequences. + The result is a list of trits equal in length to the longer of the + two sequences. - Note: Overflow is possible. - For example, ``add_trits([1], [1])`` returns ``[-1]``. - """ - target_len = max(len(left), len(right)) + .. note:: + Overflow is possible. - res = [0] * target_len - left += [0] * (target_len - len(left)) - right += [0] * (target_len - len(right)) + For example, ``add_trits([1], [1])`` returns ``[-1]``. + """ + target_len = max(len(left), len(right)) - carry = 0 - for i in range(len(res)): - res[i], carry = _full_add_trits(left[i], right[i], carry) + res = [0] * target_len + left += [0] * (target_len - len(left)) + right += [0] * (target_len - len(right)) - return res + carry = 0 + for i in range(len(res)): + res[i], carry = _full_add_trits(left[i], right[i], carry) + + return res 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)) + # 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)) def trits_from_int(n, pad=1): - # type: (int, Optional[int]) -> List[int] - """ - Returns a trit representation of an integer value. + # type: (int, Optional[int]) -> List[int] + """ + Returns a trit representation of an integer value. + + :param n: + Integer value to convert. - :param n: - Integer value to convert. + :param pad: + Ensure the result has at least this many trits. - :param pad: - Ensure the result has at least this many trits. + References: - 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: - trits = [] - else: - quotient, remainder = divmod(n, 3) + """ + if n == 0: + 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 + if remainder == 2: + # Lend 1 to the next place so we can make this trit + # negative. + quotient += 1 + remainder = -1 - trits = [remainder] + trits_from_int(quotient, pad=0) + trits = [remainder] + trits_from_int(quotient, pad=0) - if pad: - trits += [0] * max(0, pad - len(trits)) + if pad: + trits += [0] * max(0, pad - len(trits)) - return trits + return trits def _cons_trits(left, right): - # type: (int, int) -> int - """ - Compares two trits. If they have the same value, returns that value. - Otherwise, returns 0. - """ - return left if left == right else 0 + # type: (int, int) -> int + """ + Compares two trits. If they have the same value, returns that + value. Otherwise, returns 0. + """ + return left if left == right else 0 def _add_trits(left, right): - # type: (int, int) -> int - """ - Adds two individual trits together. + # type: (int, int) -> int + """ + Adds two individual trits together. - The result is always a single trit. - """ - res = left + right - return res if -2 < res < 2 else (res < 0) - (res > 0) + The result is always a single trit. + """ + res = left + right + return res if -2 < res < 2 else (res < 0) - (res > 0) def _any_trits(left, right): - # type: (int, int) -> int - """ - Adds two individual trits together and returns a single trit - indicating whether the result is positive or negative. - """ - res = left + right - return (res > 0) - (res < 0) + # type: (int, int) -> int + """ + Adds two individual trits together and returns a single trit + indicating whether the result is positive or negative. + """ + res = left + right + return (res > 0) - (res < 0) def _full_add_trits(left, right, carry): - # type: (int, int, int) -> Tuple[int, int] - """ - Adds two trits together, with support for a carry trit. - """ - sum_both = _add_trits(left, right) - cons_left = _cons_trits(left, right) - cons_right = _cons_trits(sum_both, carry) - - return _add_trits(sum_both, carry), _any_trits(cons_left, cons_right) + # type: (int, int, int) -> Tuple[int, int] + """ + Adds two trits together, with support for a carry trit. + """ + sum_both = _add_trits(left, right) + cons_left = _cons_trits(left, right) + cons_right = _cons_trits(sum_both, carry) + + return _add_trits(sum_both, carry), _any_trits(cons_left, cons_right) diff --git a/iota/types.py b/iota/types.py index 609d1d8..6ebba80 100644 --- a/iota/types.py +++ b/iota/types.py @@ -1,17 +1,17 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from codecs import decode, encode from itertools import chain from math import ceil from random import SystemRandom from typing import Any, AnyStr, Generator, Iterable, Iterator, List, \ - MutableSequence, Optional, Text, Type, TypeVar, Union + MutableSequence, Optional, Text, Type, TypeVar, Union from warnings import warn from six import PY2, binary_type, itervalues, python_2_unicode_compatible, \ - text_type + text_type from iota import AsciiTrytesCodec, TRITS_PER_TRYTE from iota.crypto import HASH_LENGTH @@ -21,845 +21,899 @@ from iota.trits import int_from_trits, trits_from_int __all__ = [ - 'Address', - 'AddressChecksum', - 'Hash', - 'Tag', - 'TrytesCompatible', - 'TryteString', + 'Address', + 'AddressChecksum', + 'Hash', + 'Tag', + 'TrytesCompatible', + 'TryteString', ] - # Custom types for type hints and docstrings. TrytesCompatible = Union[AnyStr, bytearray, 'TryteString'] - T = TypeVar('T', bound='TryteString') + @python_2_unicode_compatible class TryteString(JsonSerializable): - """ - 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 random(cls, length): - # type: (int) -> TryteString - """ - Generates a random sequence of trytes. - - :param length: - Number of trytes to generate. - """ - alphabet = list(itervalues(AsciiTrytesCodec.alphabet)) - generator = SystemRandom() - - # :py:meth:`SystemRandom.choices` wasn't added until Python 3.6; - # for compatibility, we will continue to use ``choice`` in a loop. - # https://docs.python.org/3/library/random.html#random.choices - return cls( - ''.join(chr(generator.choice(alphabet)) for _ in range(length)) - .encode('ascii') - ) - - @classmethod - def from_bytes(cls, bytes_, codec=AsciiTrytesCodec.name, *args, **kwargs): - # type: (Type[T], Union[binary_type, bytearray], Text, *Any, **Any) -> T - """ - Creates a TryteString from a sequence of bytes. - - :param bytes_: - Source bytes. - - :param codec: - Reserved for future use. - - See https://github.com/iotaledger/iota.lib.py/issues/62 for more - information. - - :param args: - Additional positional arguments to pass to the initializer. - - :param kwargs: - Additional keyword arguments to pass to the initializer. """ - return cls(encode(bytes_, codec), *args, **kwargs) + 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 random(cls, length): + # type: (int) -> TryteString + """ + Generates a random sequence of trytes. + + :param length: + Number of trytes to generate. + """ + alphabet = list(itervalues(AsciiTrytesCodec.alphabet)) + generator = SystemRandom() + + # :py:meth:`SystemRandom.choices` wasn't added until Python 3.6; + # for compatibility, we will continue to use ``choice`` in a + # loop. + # https://docs.python.org/3/library/random.html#random.choices + return cls( + ''.join( + chr(generator.choice(alphabet)) for _ in range(length) + ).encode('ascii') + ) - @classmethod - def from_unicode(cls, string, *args, **kwargs): - # type: (Type[T], Text, *Any, **Any) -> T - """ - Creates a TryteString from a Unicode string. + @classmethod + def from_bytes(cls, bytes_, codec=AsciiTrytesCodec.name, *args, **kwargs): + # type: (Type[T], Union[binary_type, bytearray], Text, *Any, **Any) -> T + """ + Creates a TryteString from a sequence of bytes. + + :param bytes_: + Source bytes. + + :param codec: + Reserved for future use. + + See https://github.com/iotaledger/iota.lib.py/issues/62 for + more information. + + :param args: + Additional positional arguments to pass to the initializer. + + :param kwargs: + Additional keyword arguments to pass to the initializer. + """ + return cls(encode(bytes_, codec), *args, **kwargs) + + @classmethod + def from_unicode(cls, string, *args, **kwargs): + # type: (Type[T], Text, *Any, **Any) -> T + """ + Creates a TryteString from a Unicode string. + + :param string: + Source string. + + :param args: + Additional positional arguments to pass to the initializer. + + :param kwargs: + Additional keyword arguments to pass to the initializer. + """ + return cls.from_bytes( + bytes_=string.encode('utf-8'), + codec=AsciiTrytesCodec.name, + *args, + **kwargs + ) - :param string: - Source string. + @classmethod + def from_string(cls, *args, **kwargs): + """ + Deprecated; use :py:meth:`from_unicode` instead. - :param args: - Additional positional arguments to pass to the initializer. + https://github.com/iotaledger/iota.lib.py/issues/90 + """ + warn( + category=DeprecationWarning, - :param kwargs: - Additional keyword arguments to pass to the initializer. - """ - return cls.from_bytes( - bytes_ = string.encode('utf-8'), - codec = AsciiTrytesCodec.name, - *args, - **kwargs - ) - - @classmethod - def from_string(cls, *args, **kwargs): - """ - Deprecated; use :py:meth:`from_unicode` instead. - - https://github.com/iotaledger/iota.lib.py/issues/90 - """ - warn( - message='`from_string()` is deprecated; use `from_unicode()` instead.', - category=DeprecationWarning, - ) - return cls.from_unicode(*args, **kwargs) - - @classmethod - def from_trytes(cls, trytes, *args, **kwargs): - # type: (Type[T], Iterable[Iterable[int]], *Any, **Any) -> T - """ - Creates a TryteString from a sequence of trytes. + message=( + '`from_string()` is deprecated; use `from_unicode()` instead.' + ), + ) + return cls.from_unicode(*args, **kwargs) - :param trytes: - Iterable of tryte values. - In this context, a tryte is defined as a list containing 3 trits. + @classmethod + def from_trytes(cls, trytes, *args, **kwargs): + # type: (Type[T], Iterable[Iterable[int]], *Any, **Any) -> T + """ + Creates a TryteString from a sequence of trytes. - :param args: - Additional positional arguments to pass to the initializer. + :param trytes: + Iterable of tryte values. - :param kwargs: - Additional keyword arguments to pass to the initializer. + In this context, a tryte is defined as a list containing 3 + trits. - References: - - :py:meth:`as_trytes` - """ - chars = bytearray() + :param args: + Additional positional arguments to pass to the initializer. - for t in trytes: - converted = int_from_trits(t) + :param kwargs: + Additional keyword arguments to pass to the initializer. - # :py:meth:`_tryte_from_int` - if converted < 0: - converted += 27 + References: - chars.append(AsciiTrytesCodec.alphabet[converted]) + - :py:meth:`as_trytes` + """ + chars = bytearray() - return cls(chars, *args, **kwargs) + for t in trytes: + converted = int_from_trits(t) - @classmethod - def from_trits(cls, trits, *args, **kwargs): - # type: (Type[T], Iterable[int], *Any, **Any) -> T - """ - Creates a TryteString from a sequence of trits. + # :py:meth:`_tryte_from_int` + if converted < 0: + converted += 27 - :param trits: - Iterable of trit values (-1, 0, 1). + chars.append(AsciiTrytesCodec.alphabet[converted]) - :param args: - Additional positional arguments to pass to the initializer. + return cls(chars, *args, **kwargs) - :param kwargs: - Additional keyword arguments to pass to the initializer. + @classmethod + def from_trits(cls, trits, *args, **kwargs): + # type: (Type[T], Iterable[int], *Any, **Any) -> T + """ + Creates a TryteString from a sequence of trits. - 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) + :param trits: + Iterable of trit values (-1, 0, 1). - if len(trits) % 3: - # Pad the trits so that it is cleanly divisible into trytes. - trits += [0] * (3 - (len(trits) % 3)) + :param args: + Additional positional arguments to pass to the initializer. - return cls.from_trytes( - # :see: http://stackoverflow.com/a/1751478/ - (trits[i:i+3] for i in range(0, len(trits), 3)), + :param kwargs: + Additional keyword arguments to pass to the initializer. - *args, - **kwargs - ) + References: - def __init__(self, trytes, pad=None): - # type: (TrytesCompatible, Optional[int]) -> None - """ - :param trytes: - Byte string or bytearray. + - :py:func:`int_from_trits` + - :py:meth:`as_trits` + """ + # Allow passing a generator or other non-Sized value to this + # method. + trits = list(trits) - :param pad: - Ensure at least this many trytes. + if len(trits) % 3: + # Pad the trits so that it is cleanly divisible into trytes. + trits += [0] * (3 - (len(trits) % 3)) - If there are too few, null trytes will be appended to the - TryteString. + return cls.from_trytes( + # http://stackoverflow.com/a/1751478/ + (trits[i:i + 3] for i in range(0, len(trits), 3)), - Note: If the TryteString is too long, it will _not_ be - truncated! - """ - super(TryteString, self).__init__() - - if isinstance(trytes, (int, float)): - 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): - 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 with_context( - exc = TypeError( - '{cls} cannot be initialized from a(n) {type}.'.format( - type = type(trytes).__name__, - cls = type(self).__name__, - ), - ), + *args, + **kwargs + ) - context = { - 'trytes': trytes, - }, + def __init__(self, trytes, pad=None): + # type: (TrytesCompatible, Optional[int]) -> None + """ + :param trytes: + Byte string or bytearray. + + :param pad: + Ensure at least this many trytes. + + 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, float)): + 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): + 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 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: + if isinstance(trytes, text_type): + trytes = encode(trytes, 'ascii') + + if not isinstance(trytes, bytearray): + trytes = bytearray(trytes) + + for i, ordinal in enumerate(trytes): + if ordinal not in AsciiTrytesCodec.index: + 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: + trytes += b'9' * max(0, pad - len(trytes)) + + 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( + cls=type(self).__name__, + trytes=binary_type(self._trytes), ) - else: - if isinstance(trytes, text_type): - trytes = encode(trytes, 'ascii') - - if not isinstance(trytes, bytearray): - trytes = bytearray(trytes) - - for i, ordinal in enumerate(trytes): - if ordinal not in AsciiTrytesCodec.index: - raise with_context( - exc = ValueError( - 'Invalid character {char!r} at position {i} ' - '(expected A-Z or 9).'.format( - char = chr(ordinal), - i = i, - ), - ), + def __bytes__(self): + """ + Converts the TryteString into an ASCII representation. + + .. note:: + This does not decode the trytes into bytes/characters; it + only returns an ASCII representation of the trytes + themselves! + + If you want to... + + - ... encode trytes into bytes: use :py:meth:`encode`. + - ... decode trytes into Unicode: use :py:meth:`decode`. + """ + return binary_type(self._trytes) + + def __str__(self): + """ + Same as :py:meth:`__bytes__`, except this method returns a + unicode string. + """ + # This causes infinite recursion in Python 2. + # return binary_type(self).decode('ascii') + + return binary_type(self._trytes).decode('ascii') + + def __bool__(self): + # type: () -> bool + return bool(self._trytes) and any(t != b'9' for t in self) + + if PY2: + # Magic methods have different names in Python 2. + __nonzero__ = __bool__ + + def __len__(self): + # type: () -> int + return len(self._trytes) + + def __iter__(self): + # type: () -> Generator[binary_type, None, None] + # :see: http://stackoverflow.com/a/14267935/ + return (binary_type(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, text_type): + return other.encode('ascii') in self._trytes + elif isinstance(other, (binary_type, bytearray)): + return other in self._trytes + else: + 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): + # 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 __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): + return TryteString(self._trytes + other._trytes) + elif isinstance(other, text_type): + return TryteString(self._trytes + other.encode('ascii')) + elif isinstance(other, (binary_type, bytearray)): + return TryteString(self._trytes + other) + else: + 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): + # type: (TrytesCompatible) -> bool + if isinstance(other, TryteString): + return self._trytes == other._trytes + elif isinstance(other, text_type): + return self._trytes == other.encode('ascii') + elif isinstance(other, (binary_type, bytearray)): + return self._trytes == other + else: + 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. + 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 + """ + 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 encode(self, errors='strict', codec=AsciiTrytesCodec.name): + # type: (Text, Text) -> binary_type + """ + Encodes the TryteString into a lower-level primitive (usually + bytes). + + :param errors: + How to handle trytes that can't be converted: + + 'strict' + raise an exception (recommended). + + 'replace' + replace with '?'. + + 'ignore' + omit the tryte from the result. + + :param codec: + Reserved for future use. + + See https://github.com/iotaledger/iota.lib.py/issues/62 for + more information. + + :raise: + - :py:class:`iota.codecs.TrytesDecodeError` if the trytes + cannot be decoded into bytes. + """ + # Converting ASCII-encoded trytes into bytes is considered to be + # a *decode* operation according to + # :py:class:`AsciiTrytesCodec`. + # + # Once we add more codecs, we may need to revisit this. + # See https://github.com/iotaledger/iota.lib.py/issues/62 for + # more information. + return decode(self._trytes, codec, errors) + + def as_bytes(self, *args, **kwargs): + """ + Deprecated; use :py:meth:`encode` instead. + + https://github.com/iotaledger/iota.lib.py/issues/90 + """ + warn( + category=DeprecationWarning, + message='`as_bytes()` is deprecated; use `encode()` instead.', + ) + return self.encode(*args, **kwargs) - context = { - 'trytes': trytes, - }, - ) + def decode(self, errors='strict', strip_padding=True): + # type: (Text, bool) -> Text + """ + Decodes the TryteString into a higher-level abstraction (usually + Unicode characters). - if pad: - trytes += b'9' * max(0, pad - len(trytes)) + :param errors: + How to handle trytes that can't be converted, or bytes that can't + be decoded using UTF-8: - self._trytes = trytes # type: bytearray + 'strict' + raise an exception (recommended). - def __hash__(self): - # type: () -> int - return hash(binary_type(self._trytes)) + 'replace' + replace with a placeholder character. - def __repr__(self): - # type: () -> Text - return '{cls}({trytes!r})'.format( - cls = type(self).__name__, - trytes = binary_type(self._trytes), - ) + 'ignore' + omit the invalid tryte/byte sequence. - def __bytes__(self): - """ - Converts the TryteString into an ASCII representation. + :param strip_padding: + Whether to strip trailing null trytes before converting. - Note: This does not decode the trytes into bytes/characters; it - only returns an ASCII representation of the trytes themselves! + :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. + """ + trytes = self._trytes + if strip_padding and (trytes[-1] == ord(b'9')): + trytes = trytes.rstrip(b'9') - If you want to... - - ... encode trytes into bytes: use :py:meth:`encode`. - - ... decode trytes into Unicode: use :py:meth:`decode`. - """ - return binary_type(self._trytes) + # Put one back to preserve even length for ASCII codec. + trytes += b'9' * (len(trytes) % 2) - def __str__(self): - """ - Same as :py:meth:`__bytes__`, except this method returns a unicode - string. - """ - # This causes infinite recursion in Python 2. - # return binary_type(self).decode('ascii') - return binary_type(self._trytes).decode('ascii') - - def __bool__(self): - # type: () -> bool - return bool(self._trytes) and any(t != b'9' for t in self) - - if PY2: - # Magic methods have different names in Python 2. - __nonzero__ = __bool__ - - def __len__(self): - # type: () -> int - return len(self._trytes) - - def __iter__(self): - # type: () -> Generator[binary_type, None, None] - # :see: http://stackoverflow.com/a/14267935/ - return (binary_type(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, text_type): - return other.encode('ascii') in self._trytes - elif isinstance(other, (binary_type, bytearray)): - return other in self._trytes - else: - 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): - # 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 __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): - return TryteString(self._trytes + other._trytes) - elif isinstance(other, text_type): - return TryteString(self._trytes + other.encode('ascii')) - elif isinstance(other, (binary_type, bytearray)): - return TryteString(self._trytes + other) - else: - 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): - # type: (TrytesCompatible) -> bool - if isinstance(other, TryteString): - return self._trytes == other._trytes - elif isinstance(other, text_type): - return self._trytes == other.encode('ascii') - elif isinstance(other, (binary_type, bytearray)): - return self._trytes == other - else: - 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. - 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). + bytes_ = decode(trytes, AsciiTrytesCodec.name, errors) - :param chunk_size: - Number of trytes per chunk. - """ - return len(self.iter_chunks(chunk_size)) + return bytes_.decode('utf-8', errors) - def iter_chunks(self, chunk_size): - # type: (int) -> ChunkIterator - """ - Iterates over the TryteString, in chunks of constant size. + def as_string(self, *args, **kwargs): + """ + Deprecated; use :py:meth:`decode` instead. - :param chunk_size: - Number of trytes per chunk. - The final chunk will be padded if it is too short. - """ - return ChunkIterator(self, chunk_size) + https://github.com/iotaledger/iota.lib.py/issues/90 + """ + warn( + category=DeprecationWarning, + message='`as_string()` is deprecated; use `decode()` instead.', + ) + return self.decode(*args, **kwargs) - def encode(self, errors='strict', codec=AsciiTrytesCodec.name): - # type: (Text, Text) -> binary_type - """ - Encodes the TryteString into a lower-level primitive (usually - bytes). + def as_json_compatible(self): + # type: () -> Text + """ + Returns a JSON-compatible representation of the object. - :param errors: - How to handle trytes that can't be converted: - - 'strict': raise an exception (recommended). - - 'replace': replace with '?'. - - 'ignore': omit the tryte from the result. + References: - :param codec: - Reserved for future use. + - :py:class:`iota.json.JsonEncoder`. + """ + return self._trytes.decode('ascii') - See https://github.com/iotaledger/iota.lib.py/issues/62 for more - information. + def as_integers(self): + # type: () -> List[int] + """ + Converts the TryteString into a sequence of integers. - :raise: - - :py:class:`iota.codecs.TrytesDecodeError` if the trytes cannot - be decoded into bytes. - """ - # Converting ASCII-encoded trytes into bytes is considered to be a - # *decode* operation according to :py:class:`AsciiTrytesCodec`. - # Once we add more codecs, we may need to revisit this. - # See https://github.com/iotaledger/iota.lib.py/issues/62 for more - # information. - # - # In Python 2, :py:func:`decode` does not accept keyword arguments. - return decode(self._trytes, codec, errors) - - def as_bytes(self, *args, **kwargs): - """ - Deprecated; use :py:meth:`encode` instead. + Each integer is a value between -13 and 13. + """ + return [ + self._normalize(AsciiTrytesCodec.index[c]) + for c in self._trytes + ] - https://github.com/iotaledger/iota.lib.py/issues/90 - """ - warn( - category=DeprecationWarning, - message='`as_bytes()` is deprecated; use `encode()` instead.', - ) - return self.encode(*args, **kwargs) - - def decode(self, errors='strict', strip_padding=True): - # type: (Text, bool) -> Text - """ - Decodes the TryteString into a higher-level abstraction (usually - Unicode characters). - - :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 (recommended). - - 'replace': replace with a placeholder character. - - 'ignore': omit the invalid tryte/byte sequence. - - :param strip_padding: - Whether to strip trailing null trytes before converting. - - :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. - """ - trytes = self._trytes - if strip_padding and (trytes[-1] == ord(b'9')): - trytes = trytes.rstrip(b'9') + def as_trytes(self): + # type: () -> List[List[int]] + """ + Converts the TryteString into a sequence of trytes. - # Put one back to preserve even length for ASCII codec. - trytes += b'9' * (len(trytes) % 2) + Each tryte is represented as a list with 3 trit values. - return decode(trytes, AsciiTrytesCodec.name, errors).decode('utf-8', errors) + See :py:meth:`as_trits` for more info. - def as_string(self, *args, **kwargs): - """ - Deprecated; use :py:meth:`decode` instead. + .. important:: + :py:class:`TryteString` is not a numeric type, so the result + of this method should not be interpreted as an integer! + """ + return [ + trits_from_int(n, pad=3) + for n in self.as_integers() + ] - https://github.com/iotaledger/iota.lib.py/issues/90 - """ - warn( - category=DeprecationWarning, - message='`as_string()` is deprecated; use `decode()` instead.', - ) - return self.decode(*args, **kwargs) - - def as_json_compatible(self): - # type: () -> Text - """ - Returns a JSON-compatible representation of the object. + def as_trits(self): + # type: () -> List[int] + """ + Converts the TryteString into a sequence of trit values. - References: - - :py:class:`iota.json.JsonEncoder`. - """ - return self._trytes.decode('ascii') + A trit may have value 1, 0, or -1. - def as_integers(self): - # type: () -> List[int] - """ - Converts the TryteString into a sequence of integers. + References: - Each integer is a value between -13 and 13. - """ - return [ - self._normalize(AsciiTrytesCodec.index[c]) - for c in self._trytes - ] + - https://en.wikipedia.org/wiki/Balanced_ternary - def as_trytes(self): - # type: () -> List[List[int]] - """ - Converts the TryteString into a sequence of trytes. + .. important:: + :py:class:`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())) - Each tryte is represented as a list with 3 trit values. + def _repr_pretty_(self, p, cycle): + """ + Makes JSON-serializable objects play nice with IPython's default + pretty-printer. - See :py:meth:`as_trits` for more info. + Sadly, :py:func:`pprint.pprint` does not have a similar + mechanism. - IMPORTANT: TryteString is not a numeric type, so the result of this - method should not be interpreted as an integer! - """ - return [ - trits_from_int(n, pad=3) - for n in self.as_integers() - ] + References: - def as_trits(self): - # type: () -> List[int] - """ - Converts the TryteString into a sequence of trit values. + - http://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html + - :py:meth:`IPython.lib.pretty.RepresentationPrinter.pretty` + - :py:func:`pprint._safe_repr` + """ + return p.text(repr(self)) - A trit may have value 1, 0, or -1. + @staticmethod + def _normalize(n): + # type: (int) -> int + if n > 26: + raise ValueError( + '{n} cannot be represented by a single tryte.'.format( + n=n, + )) - References: - - https://en.wikipedia.org/wiki/Balanced_ternary + # For values greater than 13, trigger an overflow. + # E.g., 14 => -13, 15 => -12, etc. + return (n - 27) if n > 13 else n - 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 _repr_pretty_(self, p, cycle): +class ChunkIterator(Iterator[TryteString]): """ - Makes JSON-serializable objects play nice with IPython's default - pretty-printer. - - Sadly, :py:func:`pprint.pprint` does not have a similar mechanism. - - References: - - http://ipython.readthedocs.io/en/stable/api/generated/IPython.lib.pretty.html - - :py:meth:`IPython.lib.pretty.RepresentationPrinter.pretty` - - :py:func:`pprint._safe_repr` + Iterates over a TryteString, in chunks of constant size. """ - return p.text(repr(self)) - @staticmethod - def _normalize(n): - # type: (int) -> int - if n > 26: - raise ValueError('{n} cannot be represented by a single tryte.'.format( - n = n, - )) + def __init__(self, trytes, chunk_size): + # type: (TryteString, int) -> None + """ + :param trytes: + :py:class:`TryteString` to iterate over. - # For values greater than 13, trigger an overflow. - # E.g., 14 => -13, 15 => -12, etc. - return (n - 27) if n > 13 else n + :param chunk_size: + Number of trytes per chunk. + The final chunk will be padded if it is too short. + """ + super(ChunkIterator, self).__init__() -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. + self.trytes = trytes + self.chunk_size = chunk_size - :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 - self._offset = 0 + def __iter__(self): + # type: () -> ChunkIterator + return self - def __iter__(self): - # type: () -> ChunkIterator - return self + def __len__(self): + # type: () -> int + """ + Returns how many chunks this iterator will return. - 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)) + .. 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 - """ - Returns the next chunk in the iterator. + 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 + :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)) + 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 + self._offset += self.chunk_size - return chunk + return chunk - if PY2: - # In Python 2, iterator methods are named a little differently. - next = __next__ + 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. - """ - # Divide by 3 to convert trits to trytes. - LEN = HASH_LENGTH // TRITS_PER_TRYTE + """ + A TryteString that is exactly one hash long. + """ + # Divide by 3 to convert trits to trytes. + LEN = HASH_LENGTH // TRITS_PER_TRYTE - def __init__(self, trytes): - # type: (TrytesCompatible) -> None - super(Hash, self).__init__(trytes, pad=self.LEN) + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Hash, 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 - )), + 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, - }, - ) + context={ + 'trytes': trytes, + }, + ) class Address(TryteString): - """ - A TryteString that acts as an address, with support for generating - and validating checksums. - """ - LEN = Hash.LEN - - def __init__(self, trytes, balance=None, key_index=None, security_level=None): - # type: (TrytesCompatible, Optional[int], Optional[int], Optional[int]) -> None - super(Address, self).__init__(trytes, pad=self.LEN) - - self.checksum = None - if len(self._trytes) == (self.LEN + AddressChecksum.LEN): - self.checksum = AddressChecksum(self[self.LEN:]) # type: Optional[AddressChecksum] - - elif len(self._trytes) > self.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. - self.address = self[:self.LEN] # type: TryteString - - self.balance = balance - """ - 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` """ + A TryteString that acts as an address, with support for generating + and validating checksums. + """ + LEN = Hash.LEN + + def __init__( + self, + trytes, # type: TrytesCompatible + balance=None, # type: Optional[int] + key_index=None, # type: Optional[int] + security_level=None, # type: Optional[int] + ): + # type: (...) -> None + super(Address, self).__init__(trytes, pad=self.LEN) + + self.checksum = None + if len(self._trytes) == (self.LEN + AddressChecksum.LEN): + self.checksum = AddressChecksum( + self[self.LEN:] + ) # type: Optional[AddressChecksum] + + elif len(self._trytes) > self.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. + self.address = self[:self.LEN] # type: TryteString + + self.balance = balance + """ + Balance owned by this address. + Defaults to ``None``; usually set via the ``getInputs`` command. + + References: + + - :py:class:`iota.commands.extended.get_inputs` + - :py:meth:`ProposedBundle.add_inputs` + """ + + self.key_index = key_index + """ + Index of the key used to generate this address. + Defaults to ``None``; usually set via ``AddressGenerator``. + + References: + + - :py:class:`iota.crypto.addresses.AddressGenerator` + """ + + self.security_level = security_level + """ + Number of hashes in the digest that was used to generate this + address. + """ + + def as_json_compatible(self): + # type: () -> dict + return { + 'trytes': self._trytes.decode('ascii'), + 'balance': self.balance, + 'key_index': self.key_index, + 'security_level': self.security_level, + } + + def is_checksum_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_valid_checksum(self): + # type: () -> Address + """ + Returns the address with a valid checksum attached. + """ + return Address( + trytes=self.address + self._generate_checksum(), + + # Make sure to copy all of the ancillary attributes, too! + balance=self.balance, + key_index=self.key_index, + security_level=self.security_level, + ) - self.key_index = key_index - """ - Index of the key used to generate this address. - Must be set manually via ``AddressGenerator``. + def _generate_checksum(self): + # type: () -> AddressChecksum + """ + Generates the correct checksum for this address. + """ + checksum_trits = [] # type: MutableSequence[int] - References: - - :py:class:`iota.crypto.addresses.AddressGenerator` - """ + sponge = Kerl() + sponge.absorb(self.address.as_trits()) + sponge.squeeze(checksum_trits) - self.security_level = security_level - """ - Number of hashes in the digest that was used to generate this - address. - """ + checksum_length = AddressChecksum.LEN * TRITS_PER_TRYTE - def as_json_compatible(self): - # type: () -> dict - return { - 'trytes': self._trytes.decode('ascii'), - 'balance': self.balance, - 'key_index': self.key_index, - 'security_level': self.security_level, - } - - def is_checksum_valid(self): - # type: () -> bool - """ - Returns whether this address has a valid checksum. - """ - if self.checksum: - return self.checksum == self._generate_checksum() + return AddressChecksum.from_trits(checksum_trits[-checksum_length:]) - return False - def with_valid_checksum(self): - # type: () -> Address +class AddressChecksum(TryteString): """ - Returns the address with a valid checksum attached. + A TryteString that acts as an address checksum. """ - return Address( - trytes = self.address + self._generate_checksum(), + LEN = 9 - # Make sure to copy all of the ancillary attributes, too! - balance = self.balance, - key_index = self.key_index, - security_level = self.security_level, - ) - - def _generate_checksum(self): - # type: () -> AddressChecksum - """ - Generates the correct checksum for this address. - """ - checksum_trits = [] # type: MutableSequence[int] + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(AddressChecksum, self).__init__(trytes, pad=None) - sponge = Kerl() - sponge.absorb(self.address.as_trits()) - sponge.squeeze(checksum_trits) + if len(self._trytes) != self.LEN: + raise with_context( + exc=ValueError( + '{cls} values must be exactly {len} trytes long.'.format( + cls=type(self).__name__, + len=self.LEN, + ), + ), - checksum_length = AddressChecksum.LEN * TRITS_PER_TRYTE + context={ + 'trytes': trytes, + }, + ) - return AddressChecksum.from_trits(checksum_trits[-checksum_length:]) +class Tag(TryteString): + """ + A TryteString that acts as a transaction tag. + """ + LEN = 27 -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, pad=None) - - if len(self._trytes) != 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, - }, - ) + def __init__(self, trytes): + # type: (TrytesCompatible) -> None + super(Tag, 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 + )), -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 with_context( - exc = ValueError('{cls} values must be {len} trytes long.'.format( - cls = type(self).__name__, - len = self.LEN - )), - - context = { - 'trytes': trytes, - }, - ) + context={ + 'trytes': trytes, + }, + ) From 3f0f132f472996a9c352a06eed8dd593422912f7 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 23 Jun 2018 11:12:45 +1200 Subject: [PATCH 19/32] [#145] Reformatted examples for PEP-8. --- examples/address_generator.py | 205 ++++++++-------- examples/hello_world.py | 65 ++--- examples/mam_js_send.py | 418 +++++++++++++++++---------------- examples/multisig.py | 129 +++++----- examples/routingwrapper_pow.py | 61 ++--- examples/sandbox.py | 73 +++--- examples/send_transfer.py | 59 ++--- 7 files changed, 504 insertions(+), 506 deletions(-) diff --git a/examples/address_generator.py b/examples/address_generator.py index 01043e6..3bb7876 100644 --- a/examples/address_generator.py +++ b/examples/address_generator.py @@ -4,128 +4,133 @@ """ from __future__ import absolute_import, division, print_function, \ - unicode_literals + 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 six import binary_type, moves as compat, text_type + +from iota import Iota, __version__ from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed -from six import binary_type, moves as compat, text_type def main(uri, index, count, security, checksum): - # type: (Text, int, Optional[int], Optional[int], bool) -> None - seed = get_seed() + # type: (Text, int, Optional[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) + # 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) + # 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('') + print('Generating addresses. This may take a few minutes...') + print('') - # Here's where all the magic happens! - api_response = api.get_new_addresses(index, count, security, checksum) - for addy in api_response['addresses']: - print(binary_type(addy).decode('ascii')) + # Here's where all the magic happens! + api_response = api.get_new_addresses(index, count, security, checksum) + for addy in api_response['addresses']: + print(binary_type(addy).decode('ascii')) - print('') + 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') + # 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('') + # 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 = 'http://localhost:14265/', - - help = - 'URI of the node to connect to ' - '(defaults to http://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.' - ) - - parser.add_argument( - '--security', - type = int, - default = AddressGenerator.DEFAULT_SECURITY_LEVEL, - help = 'Security level to be used for the private key / address. ' - 'Can be 1, 2 or 3', - ) - - parser.add_argument( - '--with-checksum', - action = 'store_true', - default = False, - dest = 'checksum', - help = 'List the address with the checksum.', - ) - - main(**vars(parser.parse_args(argv[1:]))) + parser = ArgumentParser( + description=__doc__, + epilog='PyOTA v{version}'.format(version=__version__), + ) + + parser.add_argument( + '--uri', + type=text_type, + default='http://localhost:14265/', + + help=( + 'URI of the node to connect to ' + '(defaults to http://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.' + ), + ) + + parser.add_argument( + '--security', + type=int, + default=AddressGenerator.DEFAULT_SECURITY_LEVEL, + help=( + 'Security level to be used for the private key / address. ' + 'Can be 1, 2 or 3' + ), + ) + + parser.add_argument( + '--with-checksum', + action='store_true', + default=False, + dest='checksum', + help='List the address with the checksum.', + ) + + main(**vars(parser.parse_args(argv[1:]))) diff --git a/examples/hello_world.py b/examples/hello_world.py index e1a2667..bf24d34 100644 --- a/examples/hello_world.py +++ b/examples/hello_world.py @@ -5,7 +5,7 @@ """ from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from argparse import ArgumentParser from pprint import pprint @@ -19,36 +19,39 @@ def main(uri): - # type: (Text) -> None - api = StrictIota(uri) - - try: - node_info = api.get_node_info() - except ConnectionError as e: - print("Hm. {uri} isn't responding. Is the node running?".format(uri=uri)) - print(e) - except BadApiResponse as e: - print("Looks like {uri} isn't very talkative today ):".format(uri=uri)) - print(e) - else: - print('Hello {uri}!'.format(uri=uri)) - pprint(node_info) + # type: (Text) -> None + api = StrictIota(uri) + + try: + node_info = api.get_node_info() + except ConnectionError as e: + print( + "Hm. {uri} isn't responding; is the node running?".format(uri=uri) + ) + print(e) + except BadApiResponse as e: + print("Looks like {uri} isn't very talkative today ):".format(uri=uri)) + print(e) + else: + print('Hello {uri}!'.format(uri=uri)) + pprint(node_info) if __name__ == '__main__': - parser = ArgumentParser( - description = __doc__, - epilog = 'PyOTA v{version}'.format(version=__version__), - ) - - parser.add_argument( - '--uri', - type = text_type, - default = 'http://localhost:14265/', - - help = - 'URI of the node to connect to ' - '(defaults to http://localhost:14265/).', - ) - - main(**vars(parser.parse_args(argv[1:]))) + parser = ArgumentParser( + description=__doc__, + epilog='PyOTA v{version}'.format(version=__version__), + ) + + parser.add_argument( + '--uri', + type=text_type, + default='http://localhost:14265/', + + help=( + 'URI of the node to connect to ' + '(defaults to http://localhost:14265/).' + ), + ) + + main(**vars(parser.parse_args(argv[1:]))) diff --git a/examples/mam_js_send.py b/examples/mam_js_send.py index 71a3a8c..23039eb 100644 --- a/examples/mam_js_send.py +++ b/examples/mam_js_send.py @@ -1,6 +1,6 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals import codecs import json @@ -14,223 +14,225 @@ from iota import Bundle, Iota, TransactionTrytes from iota.bin import IotaCommandLineApp -from iota.json import JsonEncoder from iota.crypto.addresses import AddressGenerator from iota.filters import Trytes +from iota.json import JsonEncoder class IotaMamExample(IotaCommandLineApp): - """ - Shows how to integrate the ``mam.client.js`` Javascript library into a - Python script, until MAM functionality is implemented in PyOTA. - - In order to execute this script, you must install Node and the - ``mam.client.js`` library. - - See https://github.com/iotaledger/mam.client.js for more information. - """ - def execute(self, api, **arguments): - # type: (Iota, ...) -> int - channel_key_index = arguments['channel_key_index'] # type: int - count = arguments['count'] # type: int - depth = arguments['depth'] # type: int - dry_run = arguments['dry_run'] # type: bool - mam_encrypt_path = arguments['mam_encrypt_path'] # type: Text - min_weight_magnitude = arguments['min_weight_magnitude'] # type: int - message_encoding = arguments['message_encoding'] # type: Text - message_file = arguments['message_file'] # type: Optional[Text] - security_level = arguments['security_level'] # type: int - start = arguments['start'] # type: int - - if message_file: - with codecs.open(message_file, 'r', message_encoding) as f_: # type: codecs.StreamReaderWriter - message = f_.read() - - else: - self.stdout.write( - 'Enter message to send. Press Ctrl-D on a blank line when done.\n\n', - ) - - message = self.stdin.read().strip() - self.stdout.write('\n') - - # Generating the encrypted message may take a little while, so we - # should provide some feedback to the user so that they know that - # their input is being processed (this is especially important if - # the user typed in their message, so that they don't press ^D - # again, thinking that the program didn't register the first one). - self.stdout.write('Encrypting message...\n') - - proc =\ - run( - args = [ - # mam_encrypt.js - mam_encrypt_path, - - # Required arguments - binary_type(api.seed), - message, - - # Options - '--channel-key-index', text_type(channel_key_index), - '--start', text_type(start), - '--count', text_type(count), - '--security-level', text_type(security_level), - ], - - check = True, - stdout = PIPE, - stderr = self.stderr, - ) - - # The output of the JS script is a collection of transaction - # trytes, encoded as JSON. - filter_ =\ - f.FilterRunner( - starting_filter = - f.Required - | f.Unicode - | f.JsonDecode - | f.Array - | f.FilterRepeater( - f.ByteString(encoding='ascii') - | Trytes(result_type=TransactionTrytes) + """ + Shows how to integrate the ``mam.client.js`` Javascript library into + a Python script, until MAM functionality is implemented in PyOTA. + + In order to execute this script, you must install Node and the + ``mam.client.js`` library. + + See https://github.com/iotaledger/mam.client.js for more + information. + """ + + def execute(self, api, **arguments): + # type: (Iota, ...) -> int + channel_key_index = arguments['channel_key_index'] # type: int + count = arguments['count'] # type: int + depth = arguments['depth'] # type: int + dry_run = arguments['dry_run'] # type: bool + mam_encrypt_path = arguments['mam_encrypt_path'] # type: Text + min_weight_magnitude = arguments['min_weight_magnitude'] # type: int + message_encoding = arguments['message_encoding'] # type: Text + message_file = arguments['message_file'] # type: Optional[Text] + security_level = arguments['security_level'] # type: int + start = arguments['start'] # type: int + + if message_file: + with codecs.open(message_file, 'r', message_encoding) as f_: + message = f_.read() + + else: + self.stdout.write( + 'Enter message to send. ' + 'Press Ctrl-D on a blank line when done.\n\n', + ) + + message = self.stdin.read().strip() + self.stdout.write('\n') + + # Generating the encrypted message may take a little while, so + # we should provide some feedback to the user so that they know + # that their input is being processed (this is especially + # important if the user typed in their message, so that they + # don't press ^D again, thinking that the program didn't + # register the first one). + self.stdout.write('Encrypting message...\n') + + proc = run( + args=[ + # mam_encrypt.js + mam_encrypt_path, + + # Required arguments + binary_type(api.seed), + message, + + # Options + '--channel-key-index', text_type(channel_key_index), + '--start', text_type(start), + '--count', text_type(count), + '--security-level', text_type(security_level), + ], + + check=True, + stdout=PIPE, + stderr=self.stderr, + ) + + # The output of the JS script is a collection of transaction + # trytes, encoded as JSON. + filter_ = f.FilterRunner( + f.Required | f.Unicode | f.JsonDecode | f.Array | f.FilterRepeater( + f.ByteString(encoding='ascii') | Trytes(TransactionTrytes), + ), + + incoming_data=proc.stdout, + ) + + if not filter_.is_valid(): + self.stderr.write( + 'Invalid output from {mam_encrypt_path}:\n' + '\n' + 'Output:\n' + '{output}\n' + '\n' + 'Errors:\n' + '{errors}\n'.format( + errors=pformat(filter_.get_errors(with_context=True)), + mam_encrypt_path=mam_encrypt_path, + output=proc.stdout, + ), + ) + + return 2 + + transaction_trytes = ( + filter_.cleaned_data + ) # type: List[TransactionTrytes] + + bundle = Bundle.from_tryte_strings(transaction_trytes) + + if dry_run: + self.stdout.write('Transactions:\n\n') + self.stdout.write(json.dumps(bundle, cls=JsonEncoder, indent=2)) + else: + api.send_trytes( + depth=depth, + trytes=transaction_trytes, + min_weight_magnitude=min_weight_magnitude, + ) + + self.stdout.write('Message broadcast successfully!\n') + self.stdout.write( + 'Bundle ID: {bundle_hash}\n'.format( + bundle_hash=bundle.hash, + ), + ) + + return 0 + + def create_argument_parser(self): + # type: () -> ArgumentParser + parser = super(IotaMamExample, self).create_argument_parser() + + parser.add_argument( + 'mam_encrypt_path', + + help='Path to `mam_encrypt.js` script.', + ) + + parser.add_argument( + '--channel-key-index', + default=0, + dest='channel_key_index', + type=int, + + help='Index of the key used to establish the channel.', + ) + + parser.add_argument( + '--start', + default=0, + type=int, + + help='Index of the first key used to encrypt the message.', + ) + + parser.add_argument( + '--count', + default=1, + type=int, + + help='Number of keys to use to encrypt the message.', + ) + + parser.add_argument( + '--security-level', + default=AddressGenerator.DEFAULT_SECURITY_LEVEL, + type=int, + + help='Number of iterations to use when generating keys.', + ) + + parser.add_argument( + '--message-file', + dest='message_file', + + help=( + 'Path to file containing the message to send. ' + 'If not provided, you will be prompted for the message ' + 'via stdin.' ), + ) + + parser.add_argument( + '--message-encoding', + dest='message_encoding', + default='utf-8', + + help='Encoding used to interpret message.', + ) - incoming_data = proc.stdout, - ) - - if not filter_.is_valid(): - self.stderr.write( - 'Invalid output from {mam_encrypt_path}:\n' - '\n' - 'Output:\n' - '{output}\n' - '\n' - 'Errors:\n' - '{errors}\n'.format( - errors = pformat(filter_.get_errors(with_context=True)), - mam_encrypt_path = mam_encrypt_path, - output = proc.stdout, - ), - ) - - return 2 - - transaction_trytes = filter_.cleaned_data # type: List[TransactionTrytes] - - bundle = Bundle.from_tryte_strings(transaction_trytes) - - if dry_run: - self.stdout.write('Transactions:\n\n') - self.stdout.write(json.dumps(bundle, cls=JsonEncoder, indent=2)) - else: - api.send_trytes( - depth = depth, - trytes = transaction_trytes, - min_weight_magnitude = min_weight_magnitude, - ) - - self.stdout.write('Message broadcast successfully!\n') - self.stdout.write( - 'Bundle ID: {bundle_hash}\n'.format( - bundle_hash = bundle.hash, - ), - ) - - return 0 - - def create_argument_parser(self): - # type: () -> ArgumentParser - parser = super(IotaMamExample, self).create_argument_parser() - - parser.add_argument( - 'mam_encrypt_path', - - help = 'Path to `mam_encrypt.js` script.', - ) - - parser.add_argument( - '--channel-key-index', - default = 0, - dest = 'channel_key_index', - type = int, - - help = 'Index of the key used to establish the channel.', - ) - - parser.add_argument( - '--start', - default = 0, - type = int, - - help = 'Index of the first key used to encrypt the message.', - ) - - parser.add_argument( - '--count', - default = 1, - type = int, - - help = 'Number of keys to use to encrypt the message.', - ) - - parser.add_argument( - '--security-level', - default = AddressGenerator.DEFAULT_SECURITY_LEVEL, - type = int, - - help = 'Number of iterations to use when generating keys.', - ) - - parser.add_argument( - '--message-file', - dest = 'message_file', - - help = - 'Path to file containing the message to send. ' - 'If not provided, you will be prompted for the message via stdin.', - ) - - parser.add_argument( - '--message-encoding', - dest = 'message_encoding', - default = 'utf-8', - - help = 'Encoding used to interpret message.', - ) - - parser.add_argument( - '--depth', - default = 3, - type = int, - - help = 'Depth at which to attach the resulting transactions.', - ) - - parser.add_argument( - '--min-weight-magnitude', - dest = 'min_weight_magnitude', - type = int, + parser.add_argument( + '--depth', + default=3, + type=int, - help = - 'Min weight magnitude, used by the node to calibrate PoW. ' - 'If not provided, a default value will be used.', - ) + help='Depth at which to attach the resulting transactions.', + ) - parser.add_argument( - '--dry-run', - action = 'store_true', - default = False, - dest = 'dry_run', - - help = - 'If set, resulting transactions will be sent to stdout instead of' - 'broadcasting to the Tangle.', - ) + parser.add_argument( + '--min-weight-magnitude', + dest='min_weight_magnitude', + type=int, + + help=( + 'Min weight magnitude, used by the node to calibrate PoW. ' + 'If not provided, a default value will be used.' + ), + ) + + parser.add_argument( + '--dry-run', + action='store_true', + default=False, + dest='dry_run', + + help=( + 'If set, resulting transactions will be sent to stdout ' + 'instead of broadcasting to the Tangle.' + ), + ) - return parser + return parser if __name__ == '__main__': - IotaMamExample().main() + IotaMamExample().main() diff --git a/examples/multisig.py b/examples/multisig.py index dcd0d41..ac1baba 100644 --- a/examples/multisig.py +++ b/examples/multisig.py @@ -11,12 +11,12 @@ """ from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals from typing import List from iota import Address, Bundle, BundleValidator, ProposedTransaction, Tag, \ - TransactionTrytes, TryteString + TransactionTrytes, TryteString from iota.crypto.types import Digest, PrivateKey, Seed from iota.multisig import MultisigIota from iota.multisig.types import MultisigAddress @@ -36,75 +36,67 @@ # Create digest 1 of 3. # # noinspection SpellCheckingInspection -api_1 =\ - MultisigIota( - adapter = 'http://localhost:14265', +api_1 = MultisigIota( + adapter='http://localhost:14265', - seed = - Seed( + seed=Seed( b'TESTVALUE9DONTUSEINPRODUCTION99999XKMYQP' b'OIFGQSMIIWCQVMBSOKZASRQOFSIUSSHNDKVL9PJVS', - ), - ) + ), +) -gd_result =\ - api_1.get_digests( +gd_result = api_1.get_digests( # Starting key index. - index = 0, + index=0, # Number of digests to generate. - count = 1, + count=1, # Security level of the resulting digests. # Must be a value between 1 (faster) and 3 (more secure). - security_level = 3, - ) + security_level=3, +) # ``get_digests`` returns a dict which contains 1 or more digests, # depending on what value we used for ``count``. -digest_1 = gd_result['digests'][0] # type: Digest +digest_1 = gd_result['digests'][0] # type: Digest ## # Create digest 2 of 3. # # noinspection SpellCheckingInspection -api_2 =\ - MultisigIota( - adapter = 'http://localhost:14265', +api_2 = MultisigIota( + adapter='http://localhost:14265', - seed = - Seed( + seed=Seed( b'TESTVALUE9DONTUSEINPRODUCTION99999DDWDKI' b'FFBZVQHHINYDWRSMGGPZUERNLEAYMLFPHRXEWRNST', - ), - ) + ), +) # You can use any starting index that you want. # For maximum security, each index should be used only once. gd_result = api_2.get_digests(index=42, count=1, security_level=3) -digest_2 = gd_result['digests'][0] # type: Digest +digest_2 = gd_result['digests'][0] # type: Digest ## # Create digest 3 of 3. # # noinspection SpellCheckingInspection -api_3 =\ - MultisigIota( - adapter = 'http://localhost:14265', +api_3 = MultisigIota( + adapter='http://localhost:14265', - seed = - Seed( + seed=Seed( b'TESTVALUE9DONTUSEINPRODUCTION99999JYFRTI' b'WMKVVBAIEIYZDWLUVOYTZBKPKLLUMPDF9PPFLO9KT', - ), - ) + ), +) # It is not necessary for every digest to have the same security level. gd_result = api_3.get_digests(index=8, count=1, security_level=2) -digest_3 = gd_result['digests'][0] # type: Digest - +digest_3 = gd_result['digests'][0] # type: Digest """ Step 2: Collect the digests and create a multisig address. @@ -116,13 +108,13 @@ need to ensure that the same order is used to sign inputs! """ -cma_result =\ - api_1.create_multisig_address(digests=[digest_1, digest_2, digest_3]) +cma_result = api_1.create_multisig_address( + digests=[digest_1, digest_2, digest_3], +) # For consistency, every API command returns a dict, even if it only # has a single value. -multisig_address = cma_result['address'] # type: MultisigAddress - +multisig_address = cma_result['address'] # type: MultisigAddress """ Step 3: Prepare the bundle. @@ -137,44 +129,41 @@ """ # noinspection SpellCheckingInspection -pmt_result =\ - api_1.prepare_multisig_transfer( +pmt_result = api_1.prepare_multisig_transfer( # These are the transactions that will spend the IOTAs. # You can divide up the IOTAs to send to multiple addresses if you # want, but to keep this example focused, we will only include a # single spend transaction. - transfers = [ - ProposedTransaction( - address = - Address( - b'TESTVALUE9DONTUSEINPRODUCTION99999NDGYBC' - b'QZJFGGWZ9GBQFKDOLWMVILARZRHJMSYFZETZTHTZR', - ), - - value = 42, - - # If you'd like, you may include an optional tag and/or - # message. - tag = Tag(b'KITTEHS'), - message = TryteString.from_string('thanx fur cheezburgers'), - ), + transfers=[ + ProposedTransaction( + address=Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999NDGYBC' + b'QZJFGGWZ9GBQFKDOLWMVILARZRHJMSYFZETZTHTZR', + ), + + value=42, + + # If you'd like, you may include an optional tag and/or + # message. + tag=Tag(b'KITTEHS'), + message=TryteString.from_string('thanx fur cheezburgers'), + ), ], # Specify our multisig address as the input for the spend # transaction(s). # Note that PyOTA currently only allows one multisig input per - # bundle (although the protocol does not impose a limit). - multisig_input = multisig_address, + # bundle (although the protocol itself does not impose a limit). + multisig_input=multisig_address, # If there will be change from this transaction, you MUST specify # the change address! Unlike regular transfers, multisig transfers - # will NOT automatically generate a change address; that wouldn't - # be fair to the other participants! - change_address = None, - ) - -prepared_trytes = pmt_result['trytes'] # type: List[TransactionTrytes] + # will NOT automatically generate a change address; that wouldn't be + # fair to the other participants! + change_address=None, +) +prepared_trytes = pmt_result['trytes'] # type: List[TransactionTrytes] """ Step 4: Sign the inputs. @@ -205,15 +194,15 @@ # ``get_digests`` method, in order to generate the correct value to # sign the input! gpk_result = api_1.get_private_keys(index=0, count=1, security_level=3) -private_key_1 = gpk_result['keys'][0] # type: PrivateKey +private_key_1 = gpk_result['keys'][0] # type: PrivateKey private_key_1.sign_input_transactions(bundle, 1) gpk_result = api_2.get_private_keys(index=42, count=1, security_level=3) -private_key_2 = gpk_result['keys'][0] # type: PrivateKey +private_key_2 = gpk_result['keys'][0] # type: PrivateKey private_key_2.sign_input_transactions(bundle, 4) gpk_result = api_3.get_private_keys(index=8, count=1, security_level=2) -private_key_3 = gpk_result['keys'][0] # type: PrivateKey +private_key_3 = gpk_result['keys'][0] # type: PrivateKey private_key_3.sign_input_transactions(bundle, 7) # Once we've applied the signatures, convert the Bundle back into tryte @@ -229,11 +218,11 @@ """ validator = BundleValidator(bundle) if not validator.is_valid(): - raise ValueError( - 'Bundle failed validation:\n{errors}'.format( - errors = '\n'.join((' - ' + e) for e in validator.errors), - ), - ) + raise ValueError( + 'Bundle failed validation:\n{errors}'.format( + errors='\n'.join((' - ' + e) for e in validator.errors), + ), + ) """ Step 5: Broadcast the bundle. diff --git a/examples/routingwrapper_pow.py b/examples/routingwrapper_pow.py index 758f41d..d9f75ee 100644 --- a/examples/routingwrapper_pow.py +++ b/examples/routingwrapper_pow.py @@ -1,43 +1,44 @@ # coding=utf-8 """ -Simple example using the RoutingWrapper to route API requests to different nodes. -See: https://github.com/iotaledger/documentation/blob/iota.lib.py/1.2.x/source/includes/_adapters.md#routingwrapper +Simple example using the RoutingWrapper to route API requests to +different nodes. + +References: + +- https://pyota.readthedocs.io/en/develop/adapters.html#routingwrapper """ -from iota import * +from iota import Address, Iota, ProposedTransaction, Tag, TryteString from iota.adapter.wrappers import RoutingWrapper -api =\ - Iota( - # Send PoW requests to local node. - # All other requests go to light wallet node. - RoutingWrapper('http://service.iotasupport.com:14265') - .add_route('attachToTangle', 'http://localhost:14265'), +# Send PoW requests to local node. +# All other requests go to light wallet node. +router = RoutingWrapper('http://service.iotasupport.com:14265') +router.add_route('attachToTangle', 'http://localhost:14265') - # Seed used for cryptographic functions. - seed = b'SEED9GOES9HERE' - ) +api = Iota(router, seed=b'SEED9GOES9HERE') # Example of sending a transfer using the adapter. +# noinspection SpellCheckingInspection bundle = api.send_transfer( - depth = 100, - transfers = [ - ProposedTransaction( - # Recipient of the transfer. - address = - Address( - #b'TESTVALUE9DONTUSEINPRODUCTION99999FBFFTG' - #b'QFWEHEL9KCAFXBJBXGE9HID9XCOHFIDABHDG9AHDR' - ), + depth=100, - # Amount of IOTA to transfer. - # This value may be zero. - value = 1, + transfers=[ + ProposedTransaction( + # Recipient of the transfer. + address=Address( + # b'TESTVALUE9DONTUSEINPRODUCTION99999FBFFTG' + # b'QFWEHEL9KCAFXBJBXGE9HID9XCOHFIDABHDG9AHDR' + ), - # Optional tag to attach to the transfer. - tag = Tag(b'ADAPT'), + # Amount of IOTA to transfer. + # This value may be zero. + value=1, - # Optional message to include with the transfer. - message = TryteString.from_string('Hello!'), - ), - ], + # Optional tag to attach to the transfer. + tag=Tag(b'ADAPT'), + + # Optional message to include with the transfer. + message=TryteString.from_string('Hello!'), + ), + ], ) diff --git a/examples/sandbox.py b/examples/sandbox.py index 348834f..b4201bc 100644 --- a/examples/sandbox.py +++ b/examples/sandbox.py @@ -1,55 +1,52 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals -from iota import * +from iota import Address, Iota, ProposedTransaction, Tag, TryteString from iota.adapter.sandbox import SandboxAdapter - -# Create the API object. -iota =\ - Iota( +# Create the API client. +api = Iota( # To use sandbox mode, inject a ``SandboxAdapter``. - adapter = SandboxAdapter( - # URI of the sandbox node. - uri = 'https://sandbox.iota.org/api/v1/', + adapter=SandboxAdapter( + # URI of the sandbox node. + uri='https://sandbox.iota.org/api/v1/', - # Access token used to authenticate requests. - # Contact the node maintainer to get an access token. - auth_token = 'auth token goes here', + # Access token used to authenticate requests. + # Contact the node maintainer to get an access token. + auth_token='auth token goes here', ), # Seed used for cryptographic functions. # If null, a random seed will be generated. - seed = b'SEED9GOES9HERE', - ) + seed=b'SEED9GOES9HERE', +) # Example of sending a transfer using the sandbox. # For more information, see :py:meth:`Iota.send_transfer`. # noinspection SpellCheckingInspection -iota.send_transfer( - depth = 100, - - # One or more :py:class:`ProposedTransaction` objects to add to the - # bundle. - transfers = [ - ProposedTransaction( - # Recipient of the transfer. - address = - Address( - b'TESTVALUE9DONTUSEINPRODUCTION99999FBFFTG' - b'QFWEHEL9KCAFXBJBXGE9HID9XCOHFIDABHDG9AHDR' +api.send_transfer( + depth=100, + + # One or more :py:class:`ProposedTransaction` objects to add to the + # bundle. + transfers=[ + ProposedTransaction( + # Recipient of the transfer. + address=Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999FBFFTG' + b'QFWEHEL9KCAFXBJBXGE9HID9XCOHFIDABHDG9AHDR' + ), + + # Amount of IOTA to transfer. + # This value may be zero. + value=42, + + # Optional tag to attach to the transfer. + tag=Tag(b'EXAMPLE'), + + # Optional message to include with the transfer. + message=TryteString.from_string('Hello, Tangle!'), ), - - # Amount of IOTA to transfer. - # This value may be zero. - value = 42, - - # Optional tag to attach to the transfer. - tag = Tag(b'EXAMPLE'), - - # Optional message to include with the transfer. - message = TryteString.from_string('Hello, Tangle!'), - ), - ], + ], ) diff --git a/examples/send_transfer.py b/examples/send_transfer.py index 91b92b9..6d2ee8f 100644 --- a/examples/send_transfer.py +++ b/examples/send_transfer.py @@ -2,44 +2,45 @@ """ Example script that shows how to use PyOTA to send a transfer to an address. """ -from iota import * +from iota import Address, Iota, ProposedTransaction, Tag, TryteString -SEED1 = b"THESEEDOFTHEWALLETSENDINGGOESHERE999999999999999999999999999999999999999999999999" -ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2 = b"RECEIVINGWALLETADDRESSGOESHERE9WITHCHECKSUMANDSECURITYLEVELB999999999999999999999999999999" +SEED1 = b"THE9SEED9OF9THE9WALLET9SENDING9GOES9HERE" + +ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2 = ( + b"RECEIVING9WALLET9ADDRESS9GOES9HERE9WITH9CHECKSUM9AND9SECURITY9LEVEL9B" +) # Create the API instance. -api =\ - Iota( +api = Iota( # URI of a locally running node. 'http://localhost:14265/', # Seed used for cryptographic functions. - seed = SEED1 - ) + seed=SEED1, +) # For more information, see :py:meth:`Iota.send_transfer`. api.send_transfer( - depth = 100, - - # One or more :py:class:`ProposedTransaction` objects to add to the - # bundle. - transfers = [ - ProposedTransaction( - # Recipient of the transfer. - address = - Address( - ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2, + depth=100, + + # One or more :py:class:`ProposedTransaction` objects to add to the + # bundle. + transfers=[ + ProposedTransaction( + # Recipient of the transfer. + address=Address( + ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2, + ), + + # Amount of IOTA to transfer. + # This value may be zero. + value=1, + + # Optional tag to attach to the transfer. + tag=Tag(b'EXAMPLE'), + + # Optional message to include with the transfer. + message=TryteString.from_string('Hello!'), ), - - # Amount of IOTA to transfer. - # This value may be zero. - value = 1, - - # Optional tag to attach to the transfer. - tag = Tag(b'EXAMPLE'), - - # Optional message to include with the transfer. - message = TryteString.from_string('Hello!'), - ), - ], + ], ) From f17d6e30fc48cb33d551e002f91e89ac85a32746 Mon Sep 17 00:00:00 2001 From: Oskar Hladky Date: Wed, 27 Jun 2018 12:59:29 +0200 Subject: [PATCH 20/32] usage of bundle hash by find_transactions --- iota/api.py | 6 ++--- iota/commands/core/find_transactions.py | 4 ++-- test/commands/core/find_transactions_test.py | 24 ++++++++++---------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/iota/api.py b/iota/api.py index c451791..741c6d4 100644 --- a/iota/api.py +++ b/iota/api.py @@ -7,7 +7,7 @@ from six import with_metaclass from iota import AdapterSpec, Address, ProposedTransaction, Tag, \ - TransactionHash, TransactionTrytes, TryteString, TrytesCompatible + BundleHash, TransactionHash, TransactionTrytes, TryteString, TrytesCompatible from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import BaseCommand, CustomCommand, core, \ discover_commands, extended @@ -242,7 +242,7 @@ def check_consistency(self, tails): def find_transactions( self, - bundles=None, # type: Optional[Iterable[TransactionHash]] + bundles=None, # type: Optional[Iterable[BundleHash]] addresses=None, # type: Optional[Iterable[Address]] tags=None, # type: Optional[Iterable[Tag]] approvees=None, # type: Optional[Iterable[TransactionHash]] @@ -260,7 +260,7 @@ def find_transactions( the values. :param bundles: - List of transaction IDs. + List of bundle IDs. :param addresses: List of addresses. diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index a8411fb..6bcd66a 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -5,7 +5,7 @@ import filters as f from six import iteritems -from iota import Tag, TransactionHash +from iota import Tag, TransactionHash, BundleHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import AddressNoChecksum, Trytes @@ -56,7 +56,7 @@ def __init__(self): 'bundles': f.Array | f.FilterRepeater( f.Required | - Trytes(TransactionHash) | + Trytes(BundleHash) | f.Unicode(encoding='ascii', normalize=False), ), diff --git a/test/commands/core/find_transactions_test.py b/test/commands/core/find_transactions_test.py index efb431f..960b72b 100644 --- a/test/commands/core/find_transactions_test.py +++ b/test/commands/core/find_transactions_test.py @@ -8,7 +8,7 @@ from filters.test import BaseFilterTestCase from six import text_type -from iota import Address, Iota, Tag, TransactionHash, TryteString +from iota import Address, Iota, Tag, BundleHash, TransactionHash, TryteString from iota.adapter import MockAdapter from iota.commands.core.find_transactions import FindTransactionsCommand, \ FindTransactionsRequestFilter @@ -36,8 +36,8 @@ def test_pass_all_parameters(self): # Raw trytes are extracted to match the IRI's JSON protocol. request = { 'bundles': [ - text_type(TransactionHash(self.trytes1)), - text_type(TransactionHash(self.trytes2)), + text_type(BundleHash(self.trytes1)), + text_type(BundleHash(self.trytes2)), ], 'addresses': [ @@ -69,7 +69,7 @@ def test_pass_compatible_types(self): filter_ = self._filter({ 'bundles': [ self.trytes1.encode('ascii'), - TransactionHash(self.trytes2), + BundleHash(self.trytes2), ], 'addresses': [ @@ -95,8 +95,8 @@ def test_pass_compatible_types(self): { # Raw trytes are extracted to match the IRI's JSON protocol. 'bundles': [ - text_type(TransactionHash(self.trytes1)), - text_type(TransactionHash(self.trytes2)), + text_type(BundleHash(self.trytes1)), + text_type(BundleHash(self.trytes2)), ], 'addresses': [ @@ -122,8 +122,8 @@ def test_pass_bundles_only(self): """ request = { 'bundles': [ - TransactionHash(self.trytes1), - TransactionHash(self.trytes2), + BundleHash(self.trytes1), + BundleHash(self.trytes2), ], } @@ -135,8 +135,8 @@ def test_pass_bundles_only(self): { 'bundles': [ - text_type(TransactionHash(self.trytes1)), - text_type(TransactionHash(self.trytes2)), + text_type(BundleHash(self.trytes1)), + text_type(BundleHash(self.trytes2)), ], # Null criteria are not included in the request. @@ -292,7 +292,7 @@ def test_fail_unexpected_parameters(self): { 'addresses': [Address(self.trytes1)], 'approvees': [TransactionHash(self.trytes1)], - 'bundles': [TransactionHash(self.trytes1)], + 'bundles': [BundleHash(self.trytes1)], 'tags': [Tag(self.trytes1)], # Hey, you're not allowed in he-argh! @@ -310,7 +310,7 @@ def test_fail_bundles_wrong_type(self): """ self.assertFilterErrors( { - 'bundles': TransactionHash(self.trytes1), + 'bundles': BundleHash(self.trytes1), }, { From 04f2312b191b367ee8ddf5336dd9a08a91d5c144 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Thu, 28 Jun 2018 17:00:10 +0900 Subject: [PATCH 21/32] [#145] Maintain PEP-8 on `develop` branch. --- iota/api.py | 4 ++-- iota/commands/core/find_transactions.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iota/api.py b/iota/api.py index 741c6d4..d17eee0 100644 --- a/iota/api.py +++ b/iota/api.py @@ -6,8 +6,8 @@ from six import with_metaclass -from iota import AdapterSpec, Address, ProposedTransaction, Tag, \ - BundleHash, TransactionHash, TransactionTrytes, TryteString, TrytesCompatible +from iota import AdapterSpec, Address, BundleHash, ProposedTransaction, Tag, \ + TransactionHash, TransactionTrytes, TryteString, TrytesCompatible from iota.adapter import BaseAdapter, resolve_adapter from iota.commands import BaseCommand, CustomCommand, core, \ discover_commands, extended diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index 6bcd66a..d2fe266 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -5,7 +5,7 @@ import filters as f from six import iteritems -from iota import Tag, TransactionHash, BundleHash +from iota import BundleHash, Tag, TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter from iota.filters import AddressNoChecksum, Trytes From 09dbc94b209ff23f563f4af6b35b4d3a2d7aac8c Mon Sep 17 00:00:00 2001 From: Oskar Hladky Date: Thu, 28 Jun 2018 12:38:52 +0200 Subject: [PATCH 22/32] #181 security_level into get_account_data --- iota/api.py | 14 ++++++++++++-- iota/commands/extended/get_account_data.py | 12 +++++++----- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/iota/api.py b/iota/api.py index d17eee0..a932ab1 100644 --- a/iota/api.py +++ b/iota/api.py @@ -511,8 +511,8 @@ def broadcast_and_store(self, trytes): """ return extended.BroadcastAndStoreCommand(self.adapter)(trytes=trytes) - def get_account_data(self, start=0, stop=None, inclusion_states=False): - # type: (int, Optional[int], bool) -> dict + def get_account_data(self, start=0, stop=None, inclusion_states=False, security_level=None): + # type: (int, Optional[int], bool, Optional[int]) -> dict """ More comprehensive version of :py:meth:`get_transfers` that returns addresses and account balance in addition to bundles. @@ -539,6 +539,15 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False): This requires an additional API call to the node, so it is disabled by default. + :param security_level: + Number of iterations to use when generating new addresses + (see :py:meth:`get_new_addresses`). + + This value must be between 1 and 3, inclusive. + + If not set, defaults to + :py:attr:`AddressGenerator.DEFAULT_SECURITY_LEVEL`. + :return: Dict with the following structure:: @@ -562,6 +571,7 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False): start=start, stop=stop, inclusionStates=inclusion_states, + security_level=security_level ) def get_bundles(self, transaction): diff --git a/iota/commands/extended/get_account_data.py b/iota/commands/extended/get_account_data.py index 12ef466..7235d10 100644 --- a/iota/commands/extended/get_account_data.py +++ b/iota/commands/extended/get_account_data.py @@ -15,7 +15,7 @@ iter_used_addresses from iota.crypto.addresses import AddressGenerator from iota.crypto.types import Seed -from iota.filters import Trytes +from iota.filters import Trytes, SecurityLevel __all__ = [ 'GetAccountDataCommand', @@ -41,19 +41,20 @@ def _execute(self, request): seed = request['seed'] # type: Seed start = request['start'] # type: int stop = request['stop'] # type: Optional[int] + security_level = request['security_level'] # type: Optional[int] if stop is None: my_addresses = [] # type: List[Address] my_hashes = [] # type: List[TransactionHash] - for addy, hashes in iter_used_addresses(self.adapter, seed, start): + for addy, hashes in iter_used_addresses(self.adapter, seed, start, security_level): my_addresses.append(addy) my_hashes.extend(hashes) else: ft_command = FindTransactionsCommand(self.adapter) my_addresses = ( - AddressGenerator(seed).get_addresses(start, stop - start) + AddressGenerator(seed, security_level).get_addresses(start, stop - start) ) my_hashes = ft_command(addresses=my_addresses).get('hashes') or [] @@ -103,14 +104,15 @@ def __init__(self): # Optional parameters. 'stop': f.Type(int) | f.Min(0), 'start': f.Type(int) | f.Min(0) | f.Optional(0), - 'inclusionStates': f.Type(bool) | f.Optional(False), + 'security_level': SecurityLevel }, allow_missing_keys={ 'stop', - 'inclusionStates', 'start', + 'inclusionStates', + 'security_level' }, ) From 492de3716f81f357168c760d8e25c3945b7c18b3 Mon Sep 17 00:00:00 2001 From: Oskar Hladky Date: Thu, 28 Jun 2018 13:05:05 +0200 Subject: [PATCH 23/32] #181 security_level into get_account_data test fixes --- test/commands/extended/get_account_data_test.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/test/commands/extended/get_account_data_test.py b/test/commands/extended/get_account_data_test.py index f48a039..fd743f7 100644 --- a/test/commands/extended/get_account_data_test.py +++ b/test/commands/extended/get_account_data_test.py @@ -37,6 +37,7 @@ def test_pass_happy_path(self): 'start': 0, 'stop': 10, 'inclusionStates': True, + 'security_level': 2 } filter_ = self._filter(request) @@ -58,6 +59,7 @@ def test_pass_compatible_types(self): 'start': 42, 'stop': 86, 'inclusionStates': True, + 'security_level': 2 }) self.assertFilterPasses(filter_) @@ -69,6 +71,7 @@ def test_pass_compatible_types(self): 'start': 42, 'stop': 86, 'inclusionStates': True, + 'security_level': 2 }, ) @@ -89,6 +92,7 @@ def test_pass_optional_parameters_excluded(self): 'start': 0, 'stop': None, 'inclusionStates': False, + 'security_level': 2 } ) @@ -369,7 +373,7 @@ def test_happy_path(self): Loading account data for an account. """ # noinspection PyUnusedLocal - def mock_iter_used_addresses(adapter, seed, start): + def mock_iter_used_addresses(adapter, seed, start, security_level): """ Mocks the ``iter_used_addresses`` function, so that we can simulate its functionality without actually connecting to the From cef03e7037d320b1175a948b549e6dd78a4814e3 Mon Sep 17 00:00:00 2001 From: "CZPR-PZIZKA\\pzizka" Date: Wed, 4 Jul 2018 09:53:39 +0200 Subject: [PATCH 24/32] fixes #191 --- examples/routingwrapper_pow.py | 2 +- examples/sandbox.py | 2 +- examples/send_transfer.py | 2 +- .../core/get_transactions_to_approve_test.py | 12 ++--- .../extended/promote_transaction_test.py | 20 +++---- test/commands/extended/replay_bundle_test.py | 24 ++++----- test/commands/extended/send_transfer_test.py | 52 +++++++++---------- test/commands/extended/send_trytes_test.py | 26 +++++----- 8 files changed, 70 insertions(+), 70 deletions(-) diff --git a/examples/routingwrapper_pow.py b/examples/routingwrapper_pow.py index 758f41d..e209c3c 100644 --- a/examples/routingwrapper_pow.py +++ b/examples/routingwrapper_pow.py @@ -19,7 +19,7 @@ # Example of sending a transfer using the adapter. bundle = api.send_transfer( - depth = 100, + depth = 3, transfers = [ ProposedTransaction( # Recipient of the transfer. diff --git a/examples/sandbox.py b/examples/sandbox.py index 348834f..239694a 100644 --- a/examples/sandbox.py +++ b/examples/sandbox.py @@ -28,7 +28,7 @@ # For more information, see :py:meth:`Iota.send_transfer`. # noinspection SpellCheckingInspection iota.send_transfer( - depth = 100, + depth = 3, # One or more :py:class:`ProposedTransaction` objects to add to the # bundle. diff --git a/examples/send_transfer.py b/examples/send_transfer.py index 91b92b9..24eb564 100644 --- a/examples/send_transfer.py +++ b/examples/send_transfer.py @@ -19,7 +19,7 @@ # For more information, see :py:meth:`Iota.send_transfer`. api.send_transfer( - depth = 100, + depth = 3, # One or more :py:class:`ProposedTransaction` objects to add to the # bundle. diff --git a/test/commands/core/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py index 5875fdc..bb56631 100644 --- a/test/commands/core/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -32,7 +32,7 @@ def test_pass_happy_path_without_reference(self): Request is valid without reference. """ request = { - 'depth': 100, + 'depth': 3, } filter_ = self._filter(request) @@ -72,7 +72,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, + 'depth': 3, # I knew I should have taken that left turn at Albuquerque. 'foo': 'bar', @@ -103,7 +103,7 @@ def test_fail_depth_float(self): """ self.assertFilterErrors( { - 'depth': 100.0, + 'depth': 3.0, }, { @@ -117,7 +117,7 @@ def test_fail_depth_string(self): """ self.assertFilterErrors( { - 'depth': '100', + 'depth': '3', }, { @@ -147,7 +147,7 @@ def test_fail_reference_wrong_type(self): { 'reference': 42, - 'depth': 100, + 'depth': 3, }, { @@ -163,7 +163,7 @@ def test_fail_reference_not_trytes(self): { 'reference': b'not valid; must contain only uppercase and "9"', - 'depth': 100, + 'depth': 3, }, { diff --git a/test/commands/extended/promote_transaction_test.py b/test/commands/extended/promote_transaction_test.py index 21f5b81..85c0f50 100644 --- a/test/commands/extended/promote_transaction_test.py +++ b/test/commands/extended/promote_transaction_test.py @@ -33,7 +33,7 @@ def test_pass_happy_path(self): Request is valid. """ request = { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), } @@ -53,7 +53,7 @@ def test_pass_compatible_types(self): 'transaction': binary_type(self.trytes1), # These values must still be ints, however. - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }) @@ -88,7 +88,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), @@ -109,7 +109,7 @@ def test_fail_transaction_null(self): { 'transaction': None, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -126,7 +126,7 @@ def test_fail_transaction_wrong_type(self): { 'transaction': 42, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -143,7 +143,7 @@ def test_fail_transaction_not_trytes(self): { 'transaction': b'not valid; must contain only uppercase and "9"', - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -230,7 +230,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, @@ -248,7 +248,7 @@ def test_fail_min_weight_magnitude_string(self): # It's gotta be an int! 'minWeightMagnitude': '18', - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, @@ -266,7 +266,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, float values are not valid. 'minWeightMagnitude': 18.0, - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, @@ -283,7 +283,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, diff --git a/test/commands/extended/replay_bundle_test.py b/test/commands/extended/replay_bundle_test.py index 27e14a8..d5b3a21 100644 --- a/test/commands/extended/replay_bundle_test.py +++ b/test/commands/extended/replay_bundle_test.py @@ -34,7 +34,7 @@ def test_pass_happy_path(self): Request is valid. """ request = { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), } @@ -54,7 +54,7 @@ def test_pass_compatible_types(self): 'transaction': binary_type(self.trytes1), # These values must still be ints, however. - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }) @@ -63,7 +63,7 @@ def test_pass_compatible_types(self): filter_.cleaned_data, { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), }, @@ -89,7 +89,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), @@ -110,7 +110,7 @@ def test_fail_transaction_null(self): { 'transaction': None, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -127,7 +127,7 @@ def test_fail_transaction_wrong_type(self): { 'transaction': 42, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -144,7 +144,7 @@ def test_fail_transaction_not_trytes(self): { 'transaction': b'not valid; must contain only uppercase and "9"', - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -231,7 +231,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, @@ -249,7 +249,7 @@ def test_fail_min_weight_magnitude_string(self): # It's gotta be an int! 'minWeightMagnitude': '18', - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, @@ -267,7 +267,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, float values are not valid. 'minWeightMagnitude': 18.0, - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, @@ -284,7 +284,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 100, + 'depth': 3, 'transaction': TransactionHash(self.trytes1), }, @@ -616,7 +616,7 @@ def mock_send_trytes(_,request): mock_send_trytes, ): response = self.command( - depth = 100, + depth = 3, minWeightMagnitude = 18, transaction = bundle[0].hash, ) diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index aaedac8..b1c1c14 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -75,7 +75,7 @@ def test_pass_happy_path(self): """ request = { 'changeAddress': Address(self.trytes1), - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes2), @@ -120,7 +120,7 @@ def test_pass_compatible_types(self): self.transfer2 ], - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'securityLevel': None, }) @@ -131,7 +131,7 @@ def test_pass_compatible_types(self): { 'changeAddress': Address(self.trytes1), - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes2), 'reference': TransactionHash(self.trytes1), @@ -154,7 +154,7 @@ def test_pass_optional_parameters_omitted(self): Request omits optional parameters. """ filter_ = self._filter({ - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 13, 'seed': Seed(self.trytes2), @@ -173,7 +173,7 @@ def test_pass_optional_parameters_omitted(self): 'inputs': None, 'reference': None, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 13, 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL, 'seed': Seed(self.trytes2), @@ -206,7 +206,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -229,7 +229,7 @@ def test_fail_seed_null(self): { 'seed': None, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -247,7 +247,7 @@ def test_fail_seed_wrong_type(self): { 'seed': 42, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -265,7 +265,7 @@ def test_fail_seed_not_trytes(self): { 'seed': b'not valid; must contain only uppercase and "9"', - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -283,7 +283,7 @@ def test_fail_transfers_wrong_type(self): { 'transfers': self.transfer1, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), }, @@ -301,7 +301,7 @@ def test_fail_transfers_empty(self): { 'transfers': [], - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), }, @@ -327,7 +327,7 @@ def test_fail_transfers_contents_invalid(self): {'address': Address(self.trytes2), 'value': 42}, ], - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), }, @@ -346,7 +346,7 @@ def test_fail_change_address_wrong_type(self): { 'changeAddress': 42, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -365,7 +365,7 @@ def test_fail_change_address_not_trytes(self): { 'changeAddress': b'not valid; must contain only uppercase and "9"', - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -385,7 +385,7 @@ def test_fail_inputs_wrong_type(self): # Must be an array, even if there's only one input. 'inputs': Address(self.trytes4), - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -416,7 +416,7 @@ def test_fail_inputs_contents_invalid(self): b'9' * 82, ], - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -476,7 +476,7 @@ def test_fail_depth_float(self): self.assertFilterErrors( { # Even with an empty fpart, floats are invalid. - 'depth': 100.0, + 'depth': 3.0, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), @@ -514,7 +514,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 100, + 'depth': 3, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -533,7 +533,7 @@ def test_fail_min_weight_magnitude_string(self): # Nope; it's gotta be an int. 'minWeightMagnitude': '18', - 'depth': 100, + 'depth': 3, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -552,7 +552,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, floats are invalid. 'minWeightMagnitude': 18.0, - 'depth': 100, + 'depth': 3, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -570,7 +570,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 100, + 'depth': 3, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -589,7 +589,7 @@ def test_fail_reference_wrong_type(self): 'reference': 42, 'seed': Seed(self.trytes1), - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -608,7 +608,7 @@ def test_fail_reference_not_trytes(self): 'reference': b'not valid; must contain only uppercase and "9"', 'seed': Seed(self.trytes1), - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -624,7 +624,7 @@ def test_fail_wrong_security_level(self): """ self.assertFilterErrors( { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -645,7 +645,7 @@ def test_fail_wrong_security_level_type(self): """ self.assertFilterErrors( { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -747,7 +747,7 @@ def test_happy_path(self): mock_send_trytes, ): response = self.command( - depth = 100, + depth = 3, minWeightMagnitude = 18, seed = Seed.random(), diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index b2d4d02..d90b7f5 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -33,7 +33,7 @@ def test_pass_happy_path(self): Request is valid. """ request = { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'trytes': [ @@ -63,7 +63,7 @@ def test_pass_compatible_types(self): 'reference': binary_type(self.trytes2), # These still have to be ints, however. - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }) @@ -72,7 +72,7 @@ def test_pass_compatible_types(self): filter_.cleaned_data, { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'trytes': [ @@ -103,7 +103,7 @@ def test_fail_request_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, 'trytes': [TryteString(self.trytes1)], @@ -194,7 +194,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 100, + 'depth': 3, 'trytes': [TryteString(self.trytes1)], }, @@ -212,7 +212,7 @@ def test_fail_min_weight_magnitude_string(self): # It's gotta be an int! 'minWeightMagnitude': '18', - 'depth': 100, + 'depth': 3, 'trytes': [TryteString(self.trytes1)], }, @@ -230,7 +230,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, float values are not valid. 'minWeightMagnitude': 18.0, - 'depth': 100, + 'depth': 3, 'trytes': [TryteString(self.trytes1)], }, @@ -247,7 +247,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 100, + 'depth': 3, 'trytes': [TryteString(self.trytes1)], }, @@ -264,7 +264,7 @@ def test_fail_trytes_null(self): { 'trytes': None, - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -283,7 +283,7 @@ def test_fail_trytes_wrong_type(self): # send. 'trytes': TryteString(self.trytes1), - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -300,7 +300,7 @@ def test_fail_trytes_empty(self): { 'trytes': [], - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -330,7 +330,7 @@ def test_fail_trytes_contents_invalid(self): b'9' * (TransactionTrytes.LEN + 1), ], - 'depth': 100, + 'depth': 3, 'minWeightMagnitude': 18, }, @@ -403,7 +403,7 @@ def test_happy_path(self): response = self.command( trytes = trytes, - depth = 100, + depth = 3, minWeightMagnitude = 18, ) From 3751bb02a3e15c3cdc60cbe2b98edf5a502a3e68 Mon Sep 17 00:00:00 2001 From: hribek25 Date: Thu, 5 Jul 2018 08:45:51 +0200 Subject: [PATCH 25/32] Reverting test changes --- .../core/get_transactions_to_approve_test.py | 12 ++--- .../extended/promote_transaction_test.py | 20 +++---- test/commands/extended/replay_bundle_test.py | 24 ++++----- test/commands/extended/send_transfer_test.py | 52 +++++++++---------- test/commands/extended/send_trytes_test.py | 26 +++++----- 5 files changed, 67 insertions(+), 67 deletions(-) diff --git a/test/commands/core/get_transactions_to_approve_test.py b/test/commands/core/get_transactions_to_approve_test.py index bb56631..5875fdc 100644 --- a/test/commands/core/get_transactions_to_approve_test.py +++ b/test/commands/core/get_transactions_to_approve_test.py @@ -32,7 +32,7 @@ def test_pass_happy_path_without_reference(self): Request is valid without reference. """ request = { - 'depth': 3, + 'depth': 100, } filter_ = self._filter(request) @@ -72,7 +72,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 3, + 'depth': 100, # I knew I should have taken that left turn at Albuquerque. 'foo': 'bar', @@ -103,7 +103,7 @@ def test_fail_depth_float(self): """ self.assertFilterErrors( { - 'depth': 3.0, + 'depth': 100.0, }, { @@ -117,7 +117,7 @@ def test_fail_depth_string(self): """ self.assertFilterErrors( { - 'depth': '3', + 'depth': '100', }, { @@ -147,7 +147,7 @@ def test_fail_reference_wrong_type(self): { 'reference': 42, - 'depth': 3, + 'depth': 100, }, { @@ -163,7 +163,7 @@ def test_fail_reference_not_trytes(self): { 'reference': b'not valid; must contain only uppercase and "9"', - 'depth': 3, + 'depth': 100, }, { diff --git a/test/commands/extended/promote_transaction_test.py b/test/commands/extended/promote_transaction_test.py index 85c0f50..21f5b81 100644 --- a/test/commands/extended/promote_transaction_test.py +++ b/test/commands/extended/promote_transaction_test.py @@ -33,7 +33,7 @@ def test_pass_happy_path(self): Request is valid. """ request = { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), } @@ -53,7 +53,7 @@ def test_pass_compatible_types(self): 'transaction': binary_type(self.trytes1), # These values must still be ints, however. - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }) @@ -88,7 +88,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), @@ -109,7 +109,7 @@ def test_fail_transaction_null(self): { 'transaction': None, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -126,7 +126,7 @@ def test_fail_transaction_wrong_type(self): { 'transaction': 42, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -143,7 +143,7 @@ def test_fail_transaction_not_trytes(self): { 'transaction': b'not valid; must contain only uppercase and "9"', - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -230,7 +230,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, @@ -248,7 +248,7 @@ def test_fail_min_weight_magnitude_string(self): # It's gotta be an int! 'minWeightMagnitude': '18', - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, @@ -266,7 +266,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, float values are not valid. 'minWeightMagnitude': 18.0, - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, @@ -283,7 +283,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, diff --git a/test/commands/extended/replay_bundle_test.py b/test/commands/extended/replay_bundle_test.py index d5b3a21..27e14a8 100644 --- a/test/commands/extended/replay_bundle_test.py +++ b/test/commands/extended/replay_bundle_test.py @@ -34,7 +34,7 @@ def test_pass_happy_path(self): Request is valid. """ request = { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), } @@ -54,7 +54,7 @@ def test_pass_compatible_types(self): 'transaction': binary_type(self.trytes1), # These values must still be ints, however. - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }) @@ -63,7 +63,7 @@ def test_pass_compatible_types(self): filter_.cleaned_data, { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), }, @@ -89,7 +89,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transaction': TransactionHash(self.trytes1), @@ -110,7 +110,7 @@ def test_fail_transaction_null(self): { 'transaction': None, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -127,7 +127,7 @@ def test_fail_transaction_wrong_type(self): { 'transaction': 42, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -144,7 +144,7 @@ def test_fail_transaction_not_trytes(self): { 'transaction': b'not valid; must contain only uppercase and "9"', - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -231,7 +231,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, @@ -249,7 +249,7 @@ def test_fail_min_weight_magnitude_string(self): # It's gotta be an int! 'minWeightMagnitude': '18', - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, @@ -267,7 +267,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, float values are not valid. 'minWeightMagnitude': 18.0, - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, @@ -284,7 +284,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 3, + 'depth': 100, 'transaction': TransactionHash(self.trytes1), }, @@ -616,7 +616,7 @@ def mock_send_trytes(_,request): mock_send_trytes, ): response = self.command( - depth = 3, + depth = 100, minWeightMagnitude = 18, transaction = bundle[0].hash, ) diff --git a/test/commands/extended/send_transfer_test.py b/test/commands/extended/send_transfer_test.py index b1c1c14..aaedac8 100644 --- a/test/commands/extended/send_transfer_test.py +++ b/test/commands/extended/send_transfer_test.py @@ -75,7 +75,7 @@ def test_pass_happy_path(self): """ request = { 'changeAddress': Address(self.trytes1), - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes2), @@ -120,7 +120,7 @@ def test_pass_compatible_types(self): self.transfer2 ], - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'securityLevel': None, }) @@ -131,7 +131,7 @@ def test_pass_compatible_types(self): { 'changeAddress': Address(self.trytes1), - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes2), 'reference': TransactionHash(self.trytes1), @@ -154,7 +154,7 @@ def test_pass_optional_parameters_omitted(self): Request omits optional parameters. """ filter_ = self._filter({ - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 13, 'seed': Seed(self.trytes2), @@ -173,7 +173,7 @@ def test_pass_optional_parameters_omitted(self): 'inputs': None, 'reference': None, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 13, 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL, 'seed': Seed(self.trytes2), @@ -206,7 +206,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -229,7 +229,7 @@ def test_fail_seed_null(self): { 'seed': None, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -247,7 +247,7 @@ def test_fail_seed_wrong_type(self): { 'seed': 42, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -265,7 +265,7 @@ def test_fail_seed_not_trytes(self): { 'seed': b'not valid; must contain only uppercase and "9"', - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -283,7 +283,7 @@ def test_fail_transfers_wrong_type(self): { 'transfers': self.transfer1, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), }, @@ -301,7 +301,7 @@ def test_fail_transfers_empty(self): { 'transfers': [], - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), }, @@ -327,7 +327,7 @@ def test_fail_transfers_contents_invalid(self): {'address': Address(self.trytes2), 'value': 42}, ], - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), }, @@ -346,7 +346,7 @@ def test_fail_change_address_wrong_type(self): { 'changeAddress': 42, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -365,7 +365,7 @@ def test_fail_change_address_not_trytes(self): { 'changeAddress': b'not valid; must contain only uppercase and "9"', - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -385,7 +385,7 @@ def test_fail_inputs_wrong_type(self): # Must be an array, even if there's only one input. 'inputs': Address(self.trytes4), - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -416,7 +416,7 @@ def test_fail_inputs_contents_invalid(self): b'9' * 82, ], - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -476,7 +476,7 @@ def test_fail_depth_float(self): self.assertFilterErrors( { # Even with an empty fpart, floats are invalid. - 'depth': 3.0, + 'depth': 100.0, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), @@ -514,7 +514,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 3, + 'depth': 100, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -533,7 +533,7 @@ def test_fail_min_weight_magnitude_string(self): # Nope; it's gotta be an int. 'minWeightMagnitude': '18', - 'depth': 3, + 'depth': 100, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -552,7 +552,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, floats are invalid. 'minWeightMagnitude': 18.0, - 'depth': 3, + 'depth': 100, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -570,7 +570,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 3, + 'depth': 100, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], }, @@ -589,7 +589,7 @@ def test_fail_reference_wrong_type(self): 'reference': 42, 'seed': Seed(self.trytes1), - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -608,7 +608,7 @@ def test_fail_reference_not_trytes(self): 'reference': b'not valid; must contain only uppercase and "9"', 'seed': Seed(self.trytes1), - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'transfers': [self.transfer1], }, @@ -624,7 +624,7 @@ def test_fail_wrong_security_level(self): """ self.assertFilterErrors( { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -645,7 +645,7 @@ def test_fail_wrong_security_level_type(self): """ self.assertFilterErrors( { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'seed': Seed(self.trytes1), 'transfers': [self.transfer1], @@ -747,7 +747,7 @@ def test_happy_path(self): mock_send_trytes, ): response = self.command( - depth = 3, + depth = 100, minWeightMagnitude = 18, seed = Seed.random(), diff --git a/test/commands/extended/send_trytes_test.py b/test/commands/extended/send_trytes_test.py index d90b7f5..b2d4d02 100644 --- a/test/commands/extended/send_trytes_test.py +++ b/test/commands/extended/send_trytes_test.py @@ -33,7 +33,7 @@ def test_pass_happy_path(self): Request is valid. """ request = { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'trytes': [ @@ -63,7 +63,7 @@ def test_pass_compatible_types(self): 'reference': binary_type(self.trytes2), # These still have to be ints, however. - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }) @@ -72,7 +72,7 @@ def test_pass_compatible_types(self): filter_.cleaned_data, { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'trytes': [ @@ -103,7 +103,7 @@ def test_fail_request_unexpected_parameters(self): """ self.assertFilterErrors( { - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, 'trytes': [TryteString(self.trytes1)], @@ -194,7 +194,7 @@ def test_fail_min_weight_magnitude_null(self): { 'minWeightMagnitude': None, - 'depth': 3, + 'depth': 100, 'trytes': [TryteString(self.trytes1)], }, @@ -212,7 +212,7 @@ def test_fail_min_weight_magnitude_string(self): # It's gotta be an int! 'minWeightMagnitude': '18', - 'depth': 3, + 'depth': 100, 'trytes': [TryteString(self.trytes1)], }, @@ -230,7 +230,7 @@ def test_fail_min_weight_magnitude_float(self): # Even with an empty fpart, float values are not valid. 'minWeightMagnitude': 18.0, - 'depth': 3, + 'depth': 100, 'trytes': [TryteString(self.trytes1)], }, @@ -247,7 +247,7 @@ def test_fail_min_weight_magnitude_too_small(self): { 'minWeightMagnitude': 0, - 'depth': 3, + 'depth': 100, 'trytes': [TryteString(self.trytes1)], }, @@ -264,7 +264,7 @@ def test_fail_trytes_null(self): { 'trytes': None, - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -283,7 +283,7 @@ def test_fail_trytes_wrong_type(self): # send. 'trytes': TryteString(self.trytes1), - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -300,7 +300,7 @@ def test_fail_trytes_empty(self): { 'trytes': [], - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -330,7 +330,7 @@ def test_fail_trytes_contents_invalid(self): b'9' * (TransactionTrytes.LEN + 1), ], - 'depth': 3, + 'depth': 100, 'minWeightMagnitude': 18, }, @@ -403,7 +403,7 @@ def test_happy_path(self): response = self.command( trytes = trytes, - depth = 3, + depth = 100, minWeightMagnitude = 18, ) From fbc1f61e660d4ef51c14fa43d82e3e9ed402a7ae Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 7 Jul 2018 08:56:04 +1200 Subject: [PATCH 26/32] [#145] Reformat setup.py for PEP-8. --- setup.py | 140 +++++++++++++++++++++++++++---------------------------- 1 file changed, 68 insertions(+), 72 deletions(-) diff --git a/setup.py b/setup.py index 444dfc9..717cd83 100644 --- a/setup.py +++ b/setup.py @@ -5,103 +5,99 @@ from __future__ import absolute_import, division, print_function from codecs import StreamReader, open +from distutils.version import LooseVersion import setuptools -from setuptools import find_packages, setup - ## # Because of the way PyOTA declares its dependencies, it requires a # more recent version of setuptools. # https://www.python.org/dev/peps/pep-0508/#environment-markers -from distutils.version import LooseVersion if LooseVersion(setuptools.__version__) < LooseVersion('20.5'): import sys - sys.exit('Installation failed: Upgrade setuptools to version 20.5 or later') + sys.exit('Installation failed: Upgrade setuptools to version 20.5 or later') ## -# Load long description for PyPi. -with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader - long_description = f.read() - +# Load long description for PyPI. +with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader + long_description = f.read() ## # Declare test dependencies separately, so that they can be installed # either automatically (``python setup.py test``) or manually # (``pip install -e .[test-runner]``). tests_require = [ - 'mock; python_version < "3.0"', - 'nose', + 'mock; python_version < "3.0"', + 'nose', ] - ## # Off we go! # noinspection SpellCheckingInspection -setup( - name = 'PyOTA', - description = 'IOTA API library for Python', - url = 'https://github.com/iotaledger/iota.lib.py', - version = '2.0.6', - - long_description = long_description, - - packages = - find_packages('.', exclude=( - 'examples', 'examples.*', - 'test', 'test.*', +setuptools.setup( + name='PyOTA', + description='IOTA API library for Python', + url='https://github.com/iotaledger/iota.lib.py', + version='2.0.6', + + long_description=long_description, + + packages=setuptools.find_packages('.', exclude=( + 'examples', 'examples.*', + 'test', 'test.*', )), - include_package_data = True, + include_package_data=True, + + # http://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point + entry_points={ + 'console_scripts': [ + 'pyota-cli=iota.bin.repl:main', + ], + }, + + install_requires=[ + 'filters', + 'pysha3', + + # ``security`` extra wasn't introduced until 2.4.1 + # http://docs.python-requests.org/en/latest/community/updates/#id35 + 'requests[security] >= 2.4.1', - # http://python-packaging.readthedocs.io/en/latest/command-line-scripts.html#the-console-scripts-entry-point - entry_points = { - 'console_scripts': [ - 'pyota-cli=iota.bin.repl:main', + 'six', + 'typing; python_version < "3.0"', ], - }, - - install_requires = [ - 'filters', - 'pysha3', - - # ``security`` extra wasn't introduced until 2.4.1 - # http://docs.python-requests.org/en/latest/community/updates/#id35 - 'requests[security] >= 2.4.1', - - 'six', - 'typing; python_version < "3.0"', - ], - - extras_require = { - 'ccurl': ['pyota-ccurl'], - 'docs-builder': ['sphinx', 'sphinx_rtd_theme'], - 'test-runner': ['detox'] + tests_require, - }, - - test_suite = 'test', - test_loader = 'nose.loader:TestLoader', - tests_require = tests_require, - - license = 'MIT', - - classifiers = [ - 'Development Status :: 5 - Production/Stable', - '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', - ], - - keywords = - 'iota,tangle,iot,internet of things,api,library,cryptocurrency,' - 'balanced ternary', - - author = 'Phoenix Zerin', - author_email = 'phx@phx.ph', + + extras_require={ + 'ccurl': ['pyota-ccurl'], + 'docs-builder': ['sphinx', 'sphinx_rtd_theme'], + 'test-runner': ['detox'] + tests_require, + }, + + test_suite='test', + test_loader='nose.loader:TestLoader', + tests_require=tests_require, + + license='MIT', + + classifiers=[ + 'Development Status :: 5 - Production/Stable', + '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', + ], + + keywords=( + 'iota,tangle,iot,internet of things,api,library,cryptocurrency,' + 'balanced ternary' + ), + + author='Phoenix Zerin', + author_email='phx@phx.ph', ) From f072b2fc3fbf7ac8dfd97cb8f3a5012d94626c69 Mon Sep 17 00:00:00 2001 From: Tyler Baker Date: Fri, 6 Jul 2018 14:04:02 -0700 Subject: [PATCH 27/32] examples/send_transfer: cleanups for better user experience Some notable changes: * Add argparse interface * Add hook for retrieving PyOTA library version * Clean up IOTA library imports * Ensure seed is not displayed in cleartext * Fix invalid depth * Misc PEP8 formatting fixes Signed-off-by: Tyler Baker --- examples/send_transfer.py | 136 +++++++++++++++++++++++++++++--------- 1 file changed, 103 insertions(+), 33 deletions(-) diff --git a/examples/send_transfer.py b/examples/send_transfer.py index 50820ec..22487da 100644 --- a/examples/send_transfer.py +++ b/examples/send_transfer.py @@ -2,45 +2,115 @@ """ Example script that shows how to use PyOTA to send a transfer to an address. """ -from iota import Address, Iota, ProposedTransaction, Tag, TryteString +from argparse import ArgumentParser +from sys import argv -SEED1 = b"THE9SEED9OF9THE9WALLET9SENDING9GOES9HERE" - -ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2 = ( - b"RECEIVING9WALLET9ADDRESS9GOES9HERE9WITH9CHECKSUM9AND9SECURITY9LEVEL9B" +from iota import ( + __version__, + Address, + Iota, + ProposedTransaction, + Tag, + TryteString, ) +from six import text_type +from address_generator import get_seed, output_seed -# Create the API instance. -api = Iota( - # URI of a locally running node. - 'http://localhost:14265/', - # Seed used for cryptographic functions. - seed=SEED1, -) +def main(address, depth, message, tag, uri, value): + # Ensure seed is not displayed in cleartext. + seed = get_seed() + # Create the API instance. + api = Iota(uri, seed) + + if not seed: + print('A random seed has been generated. Press return to see it.') + output_seed(api.seed) + + print('Starting transfer.') + # For more information, see :py:meth:`Iota.send_transfer`. + api.send_transfer( + depth=depth, + # One or more :py:class:`ProposedTransaction` objects to add to the + # bundle. + transfers=[ + ProposedTransaction( + # Recipient of the transfer. + address=Address(address), -# For more information, see :py:meth:`Iota.send_transfer`. -api.send_transfer( - depth=3, - - # One or more :py:class:`ProposedTransaction` objects to add to the - # bundle. - transfers=[ - ProposedTransaction( - # Recipient of the transfer. - address=Address( - ADDRESS_WITH_CHECKSUM_SECURITY_LEVEL_2, + # Amount of IOTA to transfer. + # By default this is a zero value transfer. + value=value, + + # Optional tag to attach to the transfer. + tag=Tag(tag), + + # Optional message to include with the transfer. + message=TryteString.from_string(message), ), + ], + ) + print('Transfer complete.') - # Amount of IOTA to transfer. - # This value may be zero. - value=1, +if __name__ == '__main__': + parser = ArgumentParser( + description=__doc__, + epilog='PyOTA v{version}'.format(version=__version__), + ) - # Optional tag to attach to the transfer. - tag=Tag(b'EXAMPLE'), + parser.add_argument( + '--address', + type=text_type, + default=b'RECEIVINGWALLETADDRESSGOESHERE9WITHCHECKSUMANDSECURITYLEVELB999999999999999999999999999999', + help= + 'Receiving address' + '(defaults to RECEIVINGWALLETADDRESSGOESHERE9WITHCHECKSUMANDSECURITYLEVELB999999999999999999999999999999).', + ) + + parser.add_argument( + '--depth', + type=int, + default=3, + help= + 'Depth at which to attach the bundle.' + '(defaults to 3).', + ) + + parser.add_argument( + '--message', + type=text_type, + default='Hello World!', + help= + 'Transfer message.' + '(defaults to Hello World!).', + ) + + parser.add_argument( + '--tag', + type=text_type, + default=b'EXAMPLE', + help= + 'Transfer tag' + '(defaults to EXAMPLE).', + ) + + parser.add_argument( + '--uri', + type=text_type, + default='http://localhost:14265/', + help= + 'URI of the node to connect to.' + '(defaults to http://localhost:14265/).', + ) + + parser.add_argument( + '--value', + type=int, + default=0, + help= + 'Value to transfer' + '(defaults to 0).', + ) + + main(**vars(parser.parse_args(argv[1:]))) - # Optional message to include with the transfer. - message=TryteString.from_string('Hello!'), - ), - ], -) From 11b727e0442278f9a65abc6ce816a9ed069e6db4 Mon Sep 17 00:00:00 2001 From: Marko Kosmerl Date: Wed, 18 Jul 2018 21:42:25 +0200 Subject: [PATCH 28/32] Setting depth default value to 3 --- iota/api.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/iota/api.py b/iota/api.py index a932ab1..c7e3c4c 100644 --- a/iota/api.py +++ b/iota/api.py @@ -894,7 +894,7 @@ def prepare_transfer( def promote_transaction( self, transaction, - depth, + depth=3, min_weight_magnitude=None, ): # type: (TransactionHash, int, Optional[int]) -> dict @@ -921,7 +921,7 @@ def promote_transaction( def replay_bundle( self, transaction, - depth, + depth=3, min_weight_magnitude=None, ): # type: (TransactionHash, int, Optional[int]) -> dict @@ -935,6 +935,7 @@ def replay_bundle( :param depth: Depth at which to attach the bundle. + Defaults to 3. :param min_weight_magnitude: Min weight magnitude, used by the node to calibrate Proof of @@ -965,8 +966,8 @@ def replay_bundle( def send_transfer( self, - depth, # type: int transfers, # type: Iterable[ProposedTransaction] + depth=3, # type: int inputs=None, # type: Optional[Iterable[Address]] change_address=None, # type: Optional[Address] min_weight_magnitude=None, # type: Optional[int] @@ -978,12 +979,13 @@ def send_transfer( 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 depth: + Depth at which to attach the bundle. + Defaults to 3. + :param inputs: List of inputs used to fund the transfer. Not needed for zero-value transfers. @@ -1035,7 +1037,7 @@ def send_transfer( securityLevel=security_level, ) - def send_trytes(self, trytes, depth, min_weight_magnitude=None): + def send_trytes(self, trytes, depth=3, min_weight_magnitude=None): # type: (Iterable[TransactionTrytes], int, Optional[int]) -> dict """ Attaches transaction trytes to the Tangle, then broadcasts and @@ -1046,6 +1048,7 @@ def send_trytes(self, trytes, depth, min_weight_magnitude=None): :param depth: Depth at which to attach the bundle. + Defaults to 3. :param min_weight_magnitude: Min weight magnitude, used by the node to calibrate Proof of From 3032c5768e4476075a03f91cd089667cd7c332bf Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 24 Jul 2018 19:16:11 +1200 Subject: [PATCH 29/32] Added missing symbols to public API. --- iota/adapter/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/iota/adapter/__init__.py b/iota/adapter/__init__.py index d5e77e9..13ce691 100644 --- a/iota/adapter/__init__.py +++ b/iota/adapter/__init__.py @@ -21,7 +21,11 @@ 'API_VERSION', 'AdapterSpec', 'BadApiResponse', + 'BaseAdapter', + 'HttpAdapter', 'InvalidUri', + 'MockAdapter', + 'resolve_adapter', ] if PY2: From 8d04b45224260ea70a188dc5fcdafc8ec8bcdc56 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 4 Nov 2018 12:22:30 +1300 Subject: [PATCH 30/32] [#206] Fix exception when adding client instance to abstract class. --- iota/api.py | 6 ++ test/api_test.py | 221 +++++++++++++++++++++++++---------------------- 2 files changed, 125 insertions(+), 102 deletions(-) diff --git a/iota/api.py b/iota/api.py index c7e3c4c..f6ce7d7 100644 --- a/iota/api.py +++ b/iota/api.py @@ -107,6 +107,12 @@ def __getattr__(self, command): # noinspection PyTypeChecker return None + # Fix an error when invoking dunder methods. + # https://github.com/iotaledger/iota.lib.py/issues/206 + if command.startswith("__"): + # noinspection PyUnresolvedReferences + return super(StrictIota, self).__getattr__(command) + try: command_class = self.commands[command] except KeyError: diff --git a/test/api_test.py b/test/api_test.py index 6bc42a9..89c4fba 100644 --- a/test/api_test.py +++ b/test/api_test.py @@ -1,137 +1,154 @@ # coding=utf-8 from __future__ import absolute_import, division, print_function, \ - unicode_literals + unicode_literals +from abc import ABCMeta from unittest import TestCase -from iota import InvalidCommand, Iota, StrictIota +from six import with_metaclass + +from iota import InvalidCommand, StrictIota from iota.adapter import MockAdapter from iota.commands import CustomCommand from iota.commands.core.get_node_info import GetNodeInfoCommand class CustomCommandTestCase(TestCase): - def setUp(self): - super(CustomCommandTestCase, self).setUp() + def setUp(self): + super(CustomCommandTestCase, self).setUp() - self.name = 'helloWorld' - self.adapter = MockAdapter() - self.command = CustomCommand(self.adapter, self.name) + self.name = 'helloWorld' + self.adapter = MockAdapter() + self.command = CustomCommand(self.adapter, self.name) - def test_call(self): - """ - Sending a custom command. - """ - expected_response = {'message': 'Hello, IOTA!'} + def test_call(self): + """ + Sending a custom command. + """ + expected_response = {'message': 'Hello, IOTA!'} - self.adapter.seed_response('helloWorld', expected_response) + self.adapter.seed_response('helloWorld', expected_response) - response = self.command() + response = self.command() - self.assertEqual(response, expected_response) - self.assertTrue(self.command.called) + self.assertEqual(response, expected_response) + self.assertTrue(self.command.called) - self.assertListEqual( - self.adapter.requests, - [{'command': 'helloWorld'}], - ) + self.assertListEqual( + self.adapter.requests, + [{'command': 'helloWorld'}], + ) - def test_call_with_parameters(self): - """ - Sending a custom command with parameters. - """ - expected_response = {'message': 'Hello, IOTA!'} + def test_call_with_parameters(self): + """ + Sending a custom command with parameters. + """ + expected_response = {'message': 'Hello, IOTA!'} - self.adapter.seed_response('helloWorld', expected_response) + self.adapter.seed_response('helloWorld', expected_response) - response = self.command(foo='bar', baz='luhrmann') + response = self.command(foo='bar', baz='luhrmann') - self.assertEqual(response, expected_response) - self.assertTrue(self.command.called) + self.assertEqual(response, expected_response) + self.assertTrue(self.command.called) - self.assertListEqual( - self.adapter.requests, - [{'command': 'helloWorld', 'foo': 'bar', 'baz': 'luhrmann'}], - ) + self.assertListEqual( + self.adapter.requests, + [{'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() + 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): - self.command(extra='params') + with self.assertRaises(RuntimeError): + self.command(extra='params') - self.assertDictEqual(self.command.request, {'command': 'helloWorld'}) + 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.seed_response('helloWorld', {'message': 'Hello, IOTA!'}) - self.command() + def test_call_reset(self): + """ + Resetting a command allows it to be called more than once. + """ + self.adapter.seed_response('helloWorld', {'message': 'Hello, IOTA!'}) + self.command() - self.command.reset() + self.command.reset() - self.assertFalse(self.command.called) - self.assertIsNone(self.command.request) - self.assertIsNone(self.command.response) + self.assertFalse(self.command.called) + self.assertIsNone(self.command.request) + self.assertIsNone(self.command.response) - expected_response = {'message': 'Welcome back!'} - self.adapter.seed_response('helloWorld', expected_response) - response = self.command(foo='bar') + expected_response = {'message': 'Welcome back!'} + self.adapter.seed_response('helloWorld', expected_response) + response = self.command(foo='bar') - self.assertDictEqual(response, expected_response) - self.assertDictEqual(self.command.response, expected_response) + self.assertDictEqual(response, expected_response) + self.assertDictEqual(self.command.response, expected_response) - self.assertDictEqual( - self.command.request, + self.assertDictEqual( + self.command.request, - { - 'command': 'helloWorld', - 'foo': 'bar', - }, - ) + { + 'command': 'helloWorld', + 'foo': 'bar', + }, + ) class IotaApiTestCase(TestCase): - def test_init_with_uri(self): - """ - Passing a URI to the initializer instead of an adapter instance. - """ - api = StrictIota('mock://') - self.assertIsInstance(api.adapter, MockAdapter) - - def test_registered_command(self): - """ - Preparing a documented command. - """ - api = StrictIota(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_unregistered_command(self): - """ - Attempting to create an unsupported command. - """ - api = StrictIota(MockAdapter()) - - with self.assertRaises(InvalidCommand): - # noinspection PyStatementEffect - api.helloWorld - - def test_create_command(self): - """ - Preparing an experimental/undocumented command. - """ - api = StrictIota(MockAdapter()) - - custom_command = api.create_command('helloWorld') - - self.assertIsInstance(custom_command, CustomCommand) - self.assertEqual(custom_command.command, 'helloWorld') + def test_init_with_uri(self): + """ + Passing a URI to the initializer instead of an adapter instance. + """ + api = StrictIota('mock://') + self.assertIsInstance(api.adapter, MockAdapter) + + def test_registered_command(self): + """ + Preparing a documented command. + """ + api = StrictIota(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_unregistered_command(self): + """ + Attempting to create an unsupported command. + """ + api = StrictIota(MockAdapter()) + + with self.assertRaises(InvalidCommand): + # noinspection PyStatementEffect + api.helloWorld + + def test_create_command(self): + """ + Preparing an experimental/undocumented command. + """ + api = StrictIota(MockAdapter()) + + custom_command = api.create_command('helloWorld') + + self.assertIsInstance(custom_command, CustomCommand) + self.assertEqual(custom_command.command, 'helloWorld') + + def test_use_in_abstract_class(self): + """ + Tests a regression where adding a client instance to an abstract + class definition raises an exception. + + References: + - https://github.com/iotaledger/iota.lib.py/issues/206 + """ + # This statement will raise an exception if the regression is + # present; no assertions necessary. + # noinspection PyUnusedLocal + class CustomClient(with_metaclass(ABCMeta)): + client = StrictIota(MockAdapter()) From 6310d7e5e197370eaed5352e90d66970a61ff2f4 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 4 Nov 2018 15:02:34 +1300 Subject: [PATCH 31/32] Bumping version number. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 717cd83..8559bfc 100644 --- a/setup.py +++ b/setup.py @@ -39,7 +39,7 @@ name='PyOTA', description='IOTA API library for Python', url='https://github.com/iotaledger/iota.lib.py', - version='2.0.6', + version='2.0.7', long_description=long_description, From 6c44d4f30b0caa6187c8004fd096dd137ea56421 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 4 Nov 2018 15:16:20 +1300 Subject: [PATCH 32/32] Fixed typo in documentation. --- iota/adapter/wrappers.py | 1 - 1 file changed, 1 deletion(-) diff --git a/iota/adapter/wrappers.py b/iota/adapter/wrappers.py index f815298..eba9e63 100644 --- a/iota/adapter/wrappers.py +++ b/iota/adapter/wrappers.py @@ -57,7 +57,6 @@ class RoutingWrapper(BaseWrapper): RoutingWrapper('http://12.34.56.78:14265') .add_route('attachToTangle', 'http://localhost:14265') .add_route('interruptAttachingToTangle', 'http://localhost:14265') - ), ) """