From 53b7a9ef1783c7816ad528180bcf9800d34b93ae Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 15 Oct 2017 13:30:07 +1300 Subject: [PATCH 01/27] [#62] Fixed incorrect documentation. --- iota/types.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/iota/types.py b/iota/types.py index 4754430..22ecd00 100644 --- a/iota/types.py +++ b/iota/types.py @@ -122,9 +122,9 @@ def from_bytes(cls, bytes_, codec=AsciiTrytesCodec.name, *args, **kwargs): :param codec: Which codec to use: - - 'binary': Converts each byte into a sequence of trits with - the same value (this is usually what you want). - - 'ascii': Uses the legacy ASCII codec. + - 'trytes_binary': Converts each byte into a sequence of trits + with the same value (this is usually what you want). + - 'trytes_ascii': Uses the legacy ASCII codec. :param args: Additional positional arguments to pass to the initializer. @@ -503,9 +503,9 @@ def as_bytes(self, errors='strict', codec=AsciiTrytesCodec.name): :param codec: Which codec to use: - - 'binary': Converts each sequence of 5 trits into a byte with - the same value (this is usually what you want). - - 'ascii': Uses the legacy ASCII codec. + - 'trytes_binary': Converts each sequence of 5 trits into a byte + with the same value (this is usually what you want). + - 'trytes_ascii': Uses the legacy ASCII codec. :raise: - :py:class:`iota.codecs.TrytesDecodeError` if the trytes cannot From 3bc9d50a847842208c3a7eab7e2c524a7d08ef20 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 18 Nov 2017 10:44:27 +1300 Subject: [PATCH 02/27] Make `--debug` more verbose in REPL. --- iota/bin/repl.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/iota/bin/repl.py b/iota/bin/repl.py index 72cbfae..459dae3 100755 --- a/iota/bin/repl.py +++ b/iota/bin/repl.py @@ -11,6 +11,7 @@ from sys import stderr from six import text_type +from six.moves import http_client # Import all IOTA symbols into module scope, so that it's more # convenient for the user. @@ -42,6 +43,7 @@ def execute(self, api, **arguments): # 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__) @@ -49,6 +51,9 @@ def execute(self, api, **arguments): 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: From 38e6aedf96d85723a6a1dc0224a3df79215555ff Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sat, 18 Nov 2017 10:46:15 +1300 Subject: [PATCH 03/27] Fixed whitespace. Your momma taught you better than that! --- iota/bin/repl.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/bin/repl.py b/iota/bin/repl.py index 459dae3..ac73da8 100755 --- a/iota/bin/repl.py +++ b/iota/bin/repl.py @@ -52,7 +52,7 @@ def execute(self, api, **arguments): api.adapter.set_logger(logger) # Turn on debugging for the underlying HTTP library. - http_client.HTTPConnection.debuglevel=1 + http_client.HTTPConnection.debuglevel = 1 try: self._start_repl(api) From 692b9c0220472d486bc885a0f5356cfdb119a37c Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Mon, 27 Nov 2017 08:44:28 +1300 Subject: [PATCH 04/27] Fixed a typo. --- iota/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/api.py b/iota/api.py index 251bfa4..fbac10a 100644 --- a/iota/api.py +++ b/iota/api.py @@ -470,7 +470,7 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False): 'balance': int, Total account balance. Might be 0. - 'bundles': List[Bundles], + 'bundles': List[Bundle], List of bundles with transactions to/from this account. } """ From bf5a86c1b443ab44d566a544d108fc9f170cb104 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 28 Nov 2017 13:18:37 +1300 Subject: [PATCH 05/27] Fixed a typo. --- iota/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/api.py b/iota/api.py index fbac10a..fa679d2 100644 --- a/iota/api.py +++ b/iota/api.py @@ -433,7 +433,7 @@ 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 + # type: (int, Optional[int], bool) -> dict """ More comprehensive version of :py:meth:`get_transfers` that returns addresses and account balance in addition to bundles. From 92b6c35bc8b649eb55df6323d658e1a2b689bea2 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Tue, 28 Nov 2017 13:19:10 +1300 Subject: [PATCH 06/27] =?UTF-8?q?Fixed=20a=20typo=20=F0=9F=98=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- iota/api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iota/api.py b/iota/api.py index fbac10a..fa679d2 100644 --- a/iota/api.py +++ b/iota/api.py @@ -433,7 +433,7 @@ 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 + # type: (int, Optional[int], bool) -> dict """ More comprehensive version of :py:meth:`get_transfers` that returns addresses and account balance in addition to bundles. From 184708f392011c3d277b6d3ccaddbe7ecbd3d727 Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Wed, 29 Nov 2017 13:14:09 +0100 Subject: [PATCH 07/27] Converted and fixed **adapters** documentation --- docs/adapters.rst | 221 ++++++++++++++++++++++++++++++++++++++++++++++ docs/index.rst | 3 + 2 files changed, 224 insertions(+) create mode 100644 docs/adapters.rst diff --git a/docs/adapters.rst b/docs/adapters.rst new file mode 100644 index 0000000..7551d72 --- /dev/null +++ b/docs/adapters.rst @@ -0,0 +1,221 @@ +Adapters and Wrappers +===================== + +The ``Iota`` class defines the API methods that are available for +interacting with the node, but it delegates the actual interaction to +another set of classes: Adapters and Wrappers. + +AdapterSpec +----------- + +In a few places in the PyOTA codebase, you may see references to a +meta-type called ``AdapterSpec``. + +``AdapterSpec`` is a placeholder that means "URI or adapter instance". + +For example, the first argument of ``Iota.__init__`` is an +``AdapterSpec``. This means that you can initialize an ``Iota`` object +using either a node URI, or an adapter instance: + +- Node URI: ``Iota('http://localhost:14265')`` +- Adapter instance: ``Iota(HttpAdapter('http://localhost:14265'))`` + +Adapters +-------- + +Adapters are responsible for sending requests to the node and returning +the response. + +PyOTA ships with a few adapters: + +HttpAdapter +~~~~~~~~~~~ + +.. code:: python + + from iota import Iota + from iota.adapter import HttpAdapter + + # Use HTTP: + api = Iota('http://localhost:14265') + api = Iota(HttpAdapter('http://localhost:14265')) + + # Use HTTPS: + api = Iota('https://service.iotasupport.com:14265') + api = Iota(HttpAdapter('https://service.iotasupport.com:14265')) + +``HttpAdapter`` uses the HTTP protocol to send requests to the node. + +To configure an ``Iota`` instance to use ``HttpAdapter``, specify an +``http://`` or ``https://`` URI, or provide an ``HttpAdapter`` instance. + +The ``HttpAdapter`` raises a ``BadApiResponse`` exception if the server +sends back an error response (due to invalid request parameters, for +example). + +Debugging HTTP Requests +^^^^^^^^^^^^^^^^^^^^^^^ + +.. code:: python + + from logging import getLogger + + from iota import Iota + + api = Iota('http://localhost:14265') + api.adapter.set_logger(getLogger(__name__)) + +To see all HTTP requests and responses as they happen, attach a +``logging.Logger`` instance to the adapter via its ``set_logger`` +method. + +Any time the ``HttpAdapter`` sends a request or receives a response, it +will first generate a log message. Note: if the response is an error +response (e.g., due to invalid request parameters), the ``HttpAdapter`` +will log the request before raising ``BadApiResponse``. + +.. note:: + + ``HttpAdapter`` generates log messages with ``DEBUG`` level, so make sure that your logger's ``level`` attribute is set low enough that it doesn't filter these messages! + +SandboxAdapter +~~~~~~~~~~~~~~ + +.. code:: python + + from iota import Iota + from iota.adapter.sandbox import SandboxAdapter + + api =\ + Iota( + SandboxAdapter( + uri = 'https://sandbox.iotatoken.com/api/v1/', + auth_token = 'demo7982-be4a-4afa-830e-7859929d892c', + ), + ) + +The ``SandboxAdapter`` is a specialized ``HttpAdapter`` that sends +authenticated requests to sandbox nodes. + +.. note:: + + See `Sandbox `_ Documentation for more information about sandbox nodes. + +Sandbox nodes process certain commands asynchronously. When +``SandboxAdapter`` determines that a request is processed +asynchronously, it will block, then poll the node periodically until it +receives a response. + +The result is that ``SandboxAdapter`` abstracts away the sandbox node's +asynchronous functionality so that your API client behaves exactly the +same as if it were connecting to a non-sandbox node. + +To create a ``SandboxAdapter``, you must provide the URI of the sandbox +node and the auth token that you received from the node maintainer. Note +that ``SandboxAdapter`` only works with ``http://`` and ``https://`` +URIs. + +You may also specify the polling interval (defaults to 15 seconds) and +the number of polls before giving up on an asynchronous job (defaults to +8 times). + +.. note:: + + For parity with the other adapters, ``SandboxAdapter`` blocks until it receives a response from the node. + + If you do not want ``SandboxAdapter`` to block the main thread, it is recommended that you execute it in a separate thread or process. + + +MockAdapter +~~~~~~~~~~~ + +.. code:: python + + from iota import Iota + from iota.adapter import MockAdapter + + # Inject a mock adapter. + api = Iota('mock://') + api = Iota(MockAdapter()) + + # Seed responses from the node. + api.adapter.seed_response('getNodeInfo', {'message': 'Hello, world!'}) + api.adapter.seed_response('getNodeInfo', {'message': 'Hello, IOTA!'}) + + # Invoke API commands, using the adapter. + print(api.get_node_info()) # {'message': 'Hello, world!'} + print(api.get_node_info()) # {'message': 'Hello, IOTA!'} + print(api.get_node_info()) # raises BadApiResponse exception + +``MockAdapter`` is used to simulate the behavior of an adapter without +actually sending any requests to the node. + +This is particularly useful in unit and functional tests where you want +to verify that your code works correctly in specific scenarios, without +having to engineer your own subtangle. + +To configure an ``Iota`` instance to use ``MockAdapter``, specify +``mock://`` as the node URI, or provide a ``MockAdapter`` instance. + +To use ``MockAdapter``, you must first seed the responses that you want +it to return by calling its ``seed_response`` method. + +``seed_response`` takes two parameters: + +- ``command: Text``: The name of the command. Note that this is the + camelCase version of the command name (e.g., ``getNodeInfo``, not + ``get_node_info``). +- ``response: dict``: The response that the adapter will return. + +You can seed multiple responses for the same command; the +``MockAdapter`` maintains a queue for each command internally, and it +will pop a response off of the corresponding queue each time it +processes a request. + +Note that you have to call ``seed_response`` once for each request you +expect it to process. If ``MockAdapter`` does not have a seeded response +for a particular command, it will raise a ``BadApiResponse`` exception +(simulates a 404 response). + +Wrappers +-------- + +Wrappers act like decorators for adapters; they are used to enhance or +otherwise modify the behavior of adapters. + +RoutingWrapper +~~~~~~~~~~~~~~ + +.. code:: python + + from iota import Iota + from iota.adapter.wrappers import RoutingWrapper + + api =\ + Iota( + # Send PoW requests to local node. + # All other requests go to light wallet node. + RoutingWrapper('https://service.iotasupport.com:14265') + .add_route('attachToTangle', 'http://localhost:14265') + .add_route('interruptAttachingToTangle', 'http://localhost:14265') + ) + +``RoutingWrapper`` allows you to route API requests to different nodes +depending on the command name. + +For example, you could use this wrapper to direct all PoW requests to a +local node, while sending the other requests to a light wallet node. + +``RoutingWrapper`` must be initialized with a default URI/adapter. This +is the adapter that will be used for any command that doesn't have a +route associated with it. + +Once you've initialized the ``RoutingWrapper``, invoke its ``add_route`` +method to specify a different adapter to use for a particular command. + +``add_route`` requires two arguments: + +- ``command: Text``: The name of the command. Note that this is the + camelCase version of the command name (e.g., ``getNodeInfo``, not + ``get_node_info``). +- ``adapter: AdapterSpec``: The adapter or URI to send this request to. diff --git a/docs/index.rst b/docs/index.rst index 625e1ba..8eb81e6 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,6 +4,9 @@ getting_started types + adapters + addresses + api .. note:: **🚧 PyOTA documentation is still under construction. 🚧** From 0663a42875e8c130fe8fced14a36cb8630ca2b40 Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Wed, 29 Nov 2017 13:20:57 +0100 Subject: [PATCH 08/27] added raw **addresses** and **api** rst files --- docs/addresses.rst | 198 +++++++++++++++++++++++++++++ docs/api.rst | 311 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 509 insertions(+) create mode 100644 docs/addresses.rst create mode 100644 docs/api.rst diff --git a/docs/addresses.rst b/docs/addresses.rst new file mode 100644 index 0000000..e61958f --- /dev/null +++ b/docs/addresses.rst @@ -0,0 +1,198 @@ +Generating Addresses +==================== + +In IOTA, addresses are generated deterministically from seeds. This +ensures that your account can be accessed from any location, as long as +you have the seed. + +Note that this also means that anyone with access to your seed can spend +your IOTAs! Treat your seed(s) the same as you would the password for +any other financial service. + +.. raw:: html + + + +PyOTA provides two methods for generating addresses: + +Using the API +------------- + +.. code:: python + + from iota import Iota + + api = Iota('http://localhost:14265', b'SEED9GOES9HERE') + + # Generate 5 addresses, starting with index 0. + gna_result = api.get_new_addresses(count=5) + addresses = gna_result['addresses'] + + # Generate 1 address, starting with index 42: + gna_result = api.get_new_addresses(start=42) + addresses = gna_result['addresses'] + + # Find the first unused address, starting with index 86: + gna_result = api.get_new_addresses(start=86, count=None) + addresses = gna_result['addresses'] + +To generate addresses using the API, invoke its ``get_new_addresses`` +method, using the following parameters: + +- ``start: int``: The starting index (defaults to 0). This can be used + to skip over addresses that have already been generated. +- ``count: Optional[int]``: The number of addresses to generate + (defaults to 1). +- If ``None``, the API will generate addresses until it finds one that + has not been used (has no transactions associated with it on the + Tangle). It will then return the unused address and discard the rest. +- ``security_level: int``: Determines the security level of the + generated addresses. See `Security Levels <#security-levels>`__ + below. + +``get_new_addresses`` returns a dict with the following items: + +- ``addresses: List[Address]``: The generated address(es). Note that + this value is always a list, even if only one address was generated. + +Using AddressGenerator +---------------------- + +.. code:: python + + from iota.crypto.addresses import AddressGenerator + + generator = AddressGenerator(b'SEED9GOES9HERE') + + # Generate a list of addresses: + addresses = generator.get_addresses(start=0, count=5) + + # Generate a list of addresses in reverse order: + addresses = generator.get_addresses(start=42, count=10, step=-1) + + # Create an iterator, advancing 5 indices each iteration. + iterator = generator.create_iterator(start=86, step=5) + for address in iterator: + ... + +If you want more control over how addresses are generated, you can use +the ``AddressGenerator`` class. + +``AddressGenerator`` can create iterators, allowing your application to +generate addresses as needed, instead of having to generate lots of +addresses up front. + +You can also specify an optional ``step`` parameter, which allows you to +skip over multiple addresses between iterations... or even iterate over +addresses in reverse order! + +``AddressGenerator`` provides two methods: + +- ``get_addresses: (int, int, int) -> List[Address]``: Returns a list + of addresses. This is the same method that the ``get_new_addresses`` + API command uses internally. +- ``create_iterator: (int, int) -> Generator[Address]``: Returns an + iterator that will create addresses endlessly. Use this if you have a + feature that needs to generate addresses "on demand". + +Security Levels +=============== + +.. code:: python + + gna_result = api.get_new_addresses(security_level=3) + + generator =\ + AddressGenerator( + seed = b'SEED9GOES9HERE', + security_level = 3, + ) + +If desired, you may change the number of iterations that +``AddressGenerator`` uses internally when generating new addresses, by +specifying a different ``security_level`` when creating a new instance. + +``security_level`` should be between 1 and 3, inclusive. Values outside +this range are not supported by the IOTA protocol. + +Use the following guide when deciding which security level to use: + +- ``security_level=1``: Least secure, but generates addresses the + fastest. +- ``security_level=2``: Default; good compromise between speed and + security. +- ``security_level=3``: Most secure; results in longer signatures in + transactions. diff --git a/docs/api.rst b/docs/api.rst new file mode 100644 index 0000000..9073b16 --- /dev/null +++ b/docs/api.rst @@ -0,0 +1,311 @@ +Standard API +============ + +The Standard API includes all of the core API calls that are made +available by the current `IOTA Reference +Implementation `__. + +These methods are "low level" and generally do not need to be called +directly. + +For the full documentation of all the Standard API calls, please refer +to the `official documentation `__. + +Extended API +============ + +The Extended API includes a number of "high level" commands to perform +tasks such as sending and receiving transfers. + +``broadcast_and_store`` +----------------------- + +Broadcasts and stores a set of transaction trytes. + +Parameters +~~~~~~~~~~ + +- ``trytes: Iterable[TransactionTrytes]``: Transaction trytes. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``trytes: List[TransactionTrytes]``: Transaction trytes that were + broadcast/stored. Should be the same as the value of the ``trytes`` + parameter. + +``get_account_data`` +-------------------- + +More comprehensive version of ``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. + +Parameters +~~~~~~~~~~ + +- ``start: int``: Starting key index. + +- ``stop: Optional[int]``: Stop before this index. Note that this + parameter behaves like the ``stop`` attribute in a ``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. + +- ``inclusion_states: bool`` 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 +~~~~~~ + +This method returns a dict with the following items: + +- ``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[Bundles]``: List of bundles with transactions to/from + this account. + +``get_bundles`` +--------------- + +Given a ``TransactionHash``, returns the bundle(s) associated with it. + +Parameters +~~~~~~~~~~ + +- ``transaction: TransactionHash``: Hash of a tail transaction. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``bundles: List[Bundle]``: List of matching bundles. Note that this + value is always a list, even if only one bundle was found. + +``get_inputs`` +-------------- + +Gets all possible inputs of a seed and returns them with the total +balance. + +This is either done deterministically (by generating all addresses until +``find_transactions`` returns an empty result), or by providing a key +range to search. + +Parameters +~~~~~~~~~~ + +- ``start: int``: Starting key index. Defaults to 0. +- ``stop: Optional[int]``: Stop before this index. +- Note that this parameter behaves like the ``stop`` attribute in a + ``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. +- ``threshold: Optional[int]``: 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. +- 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. + +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. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``inputs: List[Address]``: Addresses with nonzero balances that can + be used as inputs. +- ``totalBalance: int``: Aggregate balance of all inputs found. + +``get_latest_inclusion`` +------------------------ + +Fetches the inclusion state for the specified transaction hashes, as of +the latest milestone that the node has processed. + +Parameters +~~~~~~~~~~ + +- ``hashes: Iterable[TransactionHash]``: Iterable of transaction + hashes. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``: bool``: Inclusion state for a single + transaction. + +There will be one item per transaction hash in the ``hashes`` parameter. + +``get_new_addresses`` +--------------------- + +Generates one or more new addresses from the seed. + +Parameters +~~~~~~~~~~ + +- ``index: int``: Specify the index of the new address (must be >= 1). +- ``count: Optional[int]``: Number of addresses to generate (must be >= + 1). +- If ``None``, this method will scan the Tangle to find the next + available unused address and return that. +- ``security_level: int``: Number of iterations to use when generating + new addresses. Lower values generate addresses faster, higher values + result in more secure signatures in transactions. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``addresses: List[Address]``: The generated address(es). Note that + this value is always a list, even if only one address was generated. + +``get_transfers`` +----------------- + +Returns all transfers associated with the seed. + +Parameters +~~~~~~~~~~ + +- ``start: int``: Starting key index. +- ``stop: Optional[int]``: Stop before this index. +- Note that this parameter behaves like the ``stop`` attribute in a + ``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. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``bundles: List[Bundle]``: Matching bundles, sorted by tail + transaction timestamp. + +``prepare_transfer`` +-------------------- + +Prepares transactions to be broadcast to the Tangle, by generating the +correct bundle, as well as choosing and signing the inputs (for value +transfers). + +Parameters +~~~~~~~~~~ + +- ``transfers: Iterable[ProposedTransaction]``: Transaction objects to + prepare. +- ``inputs: Optional[Iterable[Address]]``: 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. +- ``change_address: Optional[Address]``: If inputs are provided, any + unspent amount will be sent to this address. +- If not specified, a change address will be generated automatically. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``trytes: List[TransactionTrytes]``: Raw trytes for the transactions + in the bundle, ready to be provided to ``send_trytes``. + +``replay_bundle`` +----------------- + +Takes a tail transaction hash as input, gets the bundle associated with +the transaction and then replays the bundle by attaching it to the +Tangle. + +Parameters +~~~~~~~~~~ + +- ``transaction: TransactionHash``: Transaction hash. Must be a tail. +- ``depth: int``: Depth at which to attach the bundle. +- ``min_weight_magnitude: Optional[int]``: Min weight magnitude, used + by the node to calibrate Proof of Work. +- If not provided, a default value will be used. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``trytes: List[TransactionTrytes]``: Raw trytes that were published + to the Tangle. + +``send_transfer`` +----------------- + +Prepares a set of transfers and creates the bundle, then attaches the +bundle to the Tangle, and broadcasts and stores the transactions. + +Parameters +~~~~~~~~~~ + +- ``depth: int``: Depth at which to attach the bundle. +- ``transfers: Iterable[ProposedTransaction]``: Transaction objects to + prepare. +- ``inputs: Optional[Iterable[Address]]``: 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. +- ``change_address: Optional[Address]``: If inputs are provided, any + unspent amount will be sent to this address. +- If not specified, a change address will be generated automatically. +- ``min_weight_magnitude: Optional[int]``: Min weight magnitude, used + by the node to calibrate Proof of Work. +- If not provided, a default value will be used. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``bundle: Bundle``: The newly-published bundle. + +``send_trytes`` +--------------- + +Attaches transaction trytes to the Tangle, then broadcasts and stores +them. + +Parameters +~~~~~~~~~~ + +- ``trytes: Iterable[TransactionTrytes]``: Transaction trytes to + publish. +- ``depth: int``: Depth at which to attach the bundle. +- ``min_weight_magnitude: Optional[int]``: Min weight magnitude, used + by the node to calibrate Proof of Work. +- If not provided, a default value will be used. + +Return +~~~~~~ + +This method returns a ``dict`` with the following items: + +- ``trytes: List[TransactionTrytes]``: Raw trytes that were published + to the Tangle. From f94788f2832531e8b47c1e6fd6d5d852e557f64a Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Wed, 29 Nov 2017 13:43:48 +0100 Subject: [PATCH 09/27] Converted and fixed **addresses** documentation --- docs/addresses.rst | 82 +++++++--------------------------------------- 1 file changed, 12 insertions(+), 70 deletions(-) diff --git a/docs/addresses.rst b/docs/addresses.rst index e61958f..6571e7f 100644 --- a/docs/addresses.rst +++ b/docs/addresses.rst @@ -9,82 +9,24 @@ Note that this also means that anyone with access to your seed can spend your IOTAs! Treat your seed(s) the same as you would the password for any other financial service. -.. raw:: html +.. note:: - + If you are familiar with Python 2's C API, we'd love to hear from you! + Check the `GitHub issue `_ + for more information. PyOTA provides two methods for generating addresses: From b46b4417f438a448af91096fa4161491fb1743eb Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Fri, 1 Dec 2017 10:21:21 +0100 Subject: [PATCH 10/27] converted and adjusted **types.rst** --- docs/types.rst | 541 +++++++++++++++++++++++++++++-------------------- 1 file changed, 316 insertions(+), 225 deletions(-) diff --git a/docs/types.rst b/docs/types.rst index aa9b54e..f8f6cbd 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -1,264 +1,355 @@ -================ -PyOTA Data Types -================ -.. important:: +Basic Concepts +============== - Before diving into the API, it's important to understand the fundamental data - types of IOTA. +Before diving into the API, it's important to understand the fundamental +data types of IOTA. - For an introduction to the IOTA protocol and the Tangle, give the - `protocol documentation`_ a once-over. +:todo: Link to IOTA docs -PyOTA defines a few types that will make it easy for you to model objects like -Transactions and Bundles in your own code. +PyOTA Types +=========== + +PyOTA defines a few types that will make it easy for you to model +objects like Transactions and Bundles in your own code. TryteString ----------- -A :py:class:`TryteString` is an ASCII representation of a sequence of trytes. -In many respects, it is similar to a Python ``bytes`` object (which is an ASCII -representation of a sequence of bytes). - -In fact, the two objects behave very similarly; they support concatenation, -comparison, can be used as dict keys, etc. - -However, unlike ``bytes``, a :py:class:`TryteString` can only contain uppercase -letters and the number 9 (as a regular expression: ``^[A-Z9]*$``). - -.. admonition:: Why only these characters? - - You can find the answer on the - `IOTA Forum `__. - -As you go through the API documentation, you will see many references to -:py:class:`TryteString` and its subclasses: - -- :py:class:`Fragment` - - A signature or message fragment inside a transaction. - Fragments are always 2187 trytes long. -- :py:class:`Hash` +.. code:: python - An object identifier. Hashes are always 81 trytes long. + from iota import TryteString - There are many different types of hashes: + trytes_1 = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + trytes_2 = TryteString(b'LH9GYEMHCF9GWHZFEELHVFOEOHNEEEWHZFUD') - :py:class:`Address` - Identifies an address on the Tangle. - :py:class:`BundleHash` - Identifies a bundle on the Tangle. - :py:class:`TransactionHash` - Identifies a transaction on the Tangle. + if trytes_1 != trytes_2: + trytes_combined = trytes_1 + trytes_2 -- :py:class:`Seed` + index = { + trytes_1: 42, + trytes_2: 86, + } - A TryteString that is used for crypto functions such as generating addresses, - signing inputs, etc. +A ``TryteString`` is an ASCII representation of a sequence of trytes. In +many respects, it is similar to a Python ``bytes`` object (which is an +ASCII representation of a sequence of bytes). - .. important:: +In fact, the two objects behave very similarly; they support +concatenation, comparison, can be used as dict keys, etc. - Seeds can be any length, but 81 trytes offers the best security. - More information is available on the - `IOTA Forum `__. - -- :py:class:`Tag` - - A tag used to classify a transaction. Tags are always 27 trytes long. - -- :py:class:`TransactionTrytes` - - A TryteString representation of a transaction on the Tangle. - :py:class:`TransactionTrytes` are always 2673 trytes long. - -Creating TryteStrings -~~~~~~~~~~~~~~~~~~~~~ -To create a new :py:class:`TryteString` from a sequence of trytes, simply -wrap the trytes inside the :py:class:`TryteString` initializer: - -.. code-block:: python - - from iota import TryteString - - trytes = TryteString('RBTC9D9DCDQAEASBYBCCKBFA') +However, unlike ``bytes``, a ``TryteString`` can only contain uppercase +letters and the number 9 (as a regular expression: ``^[A-Z9]*$``). -To encode ASCII text into trytes, use the :py:meth:`TryteString.from_string` -method: +As you go through the API documentation, you will see many references to +``TryteString`` and its subclasses: -.. code-block:: python +- ``Fragment``: A signature or message fragment inside a transaction. + Fragments are always 2187 trytes long. +- ``Hash``: An object identifier. Hashes are always 81 trytes long. + There are many different types of hashes: +- ``Address``: Identifies an address on the Tangle. +- ``BundleHash``: Identifies a bundle on the Tangle. +- ``TransactionHash``: Identifies a transaction on the Tangle. +- ``Seed``: A TryteString that is used for crypto functions such as + generating addresses, signing inputs, etc. Seeds can be any length, + but 81 trytes offers the best security. +- ``Tag``: A tag used to classify a transaction. Tags are always 27 + trytes long. +- ``TransactionTrytes``: A TryteString representation of a transaction + on the Tangle. ``TransactionTrytes`` are always 2673 trytes long. - from iota import TryteString +Encoding +~~~~~~~~ - message_trytes = TryteString.from_string('Hello, IOTA!') +.. code:: python - print(message_trytes) # RBTC9D9DCDQAEASBYBCCKBFA + from iota import TryteString -To decode a sequence of trytes back into ASCII text, use -:py:meth:`TryteString.as_string`: + message_trytes = TryteString.from_string('Hello, IOTA!') -.. code-block:: python +To encode character data into trytes, use the +``TryteString.from_string`` method. - from iota import TryteString +You can also convert a tryte sequence into characters using +``TryteString.as_string``. Note that not every tryte sequence can be +converted; garbage in, garbage out! - message_trytes = TryteString('RBTC9D9DCDQAEASBYBCCKBFA') +.. code:: python - message_str = message_trytes.as_string() + from iota import TryteString - print(message_str) # Hello, IOTA! + trytes = TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA') + message = trytes.as_string() .. note:: - PyOTA also supports encoding non-ASCII characters, but this functionality is - **experimental** and has not yet been standardized. + PyOTA also supports encoding non-ASCII characters, but this functionality is + **experimental** and has not yet been evaluated by the IOTA + community! - If you encode non-ASCII characters, be aware that other IOTA libraries - (possibly including future versions of PyOTA!) might not be able to decode - them! + Until this feature has been standardized, it is recommended that you only + use ASCII characters when generating ``TryteString`` objects from + character strings. Transaction Types ----------------- -PyOTA defines two different types used to represent transactions: - -:py:class:`Transaction` - A transaction that has been loaded from the Tangle. -:py:class:`ProposedTransaction` - A transaction that was created locally and hasn't been broadcast to the - Tangle yet. +PyOTA defines two different types used to represent transactions: Transaction ~~~~~~~~~~~ -Generally, you will never need to create `Transaction` objects; the API will -build them for you, as the result of various API methods. - -.. tip:: - - If you have a TryteString representation of a transaction, and you'd like to - convert it into a :py:class:`Transaction` object, use the - :py:meth:`Transaction.from_tryte_string` method: - - .. code-block:: python - - from iota import Transaction - - txn_1 =\ - Transaction.from_tryte_string( - 'GYPRVHBEZOOFXSHQBLCYW9ICTCISLHDBNMMVYD9JJHQMPQCTIQ...', - ) - - This is equivalent to the `Paste Trytes`_ feature from the IOTA Wallet. - -Each :py:class:`Transaction` object has the following attributes: - -- ``address`` (:py:class:`Address`) - - The address associated with this transaction. Depending on the transaction's - ``value``, this address may be a sender or a recipient. - -- ``attachment_timestamp`` (:py:class:`int`) - Timestamp after completing the Proof of Work process. - - See the `timestamps white paper`_ for more information. - -- ``attachment_timestamp_lower_bound`` (:py:class:`int`) - Lower bound of the timestamp. - - See the `timestamps white paper`_ for more information. - -- ``attachment_timestamp_upper_bound`` (:py:class:`int`) - Upper bound of the timestamp. - See the `timestamps white paper`_ for more information. - -- ``branch_transaction_hash`` (:py:class:`TransactionHash`) - - An unrelated transaction that this transaction "approves". - Refer to the `protocol documentation`_ for more information. - -- ``bundle_hash`` (:py:class:`BundleHash`) - - The bundle hash, used to identify transactions that are part of the same - bundle. This value is generated by taking a hash of the metadata from all - transactions in the bundle. - -- ``current_index`` (:py:class:`int`) - - The transaction's position in the bundle. - - - If the ``current_index`` value is 0, then this is the "tail transaction". - - If it is equal to ``last_index``, then this is the "head transaction". - -- ``hash`` (:py:class:`TransactionHash`) - - The transaction hash, used to uniquely identify the transaction on the - Tangle. This value is generated by taking a hash of the raw transaction - trytes. - -- ``last_index`` (:py:class:`int`) - - The index of the final transaction in the bundle. This value is attached to - every transaction to make it easier to traverse and verify bundles. - -- ``nonce`` (:py:class:`Nonce`) - - This is the product of the PoW process. - - Refer to the `protocol documentation`_ for more information. - -- ``signature_message_fragment`` (:py:class:`Fragment`) - - Additional data attached to the transaction: - - - If ``value < 0``, this value contains a fragment of the cryptographic - signature authorizing the spending of the IOTAs. - - If ``value > 0``, this value is an (optional) string message attached to - the transaction. - - If ``value = 0``, this value could be either a signature or message - fragment, depending on the previous transaction. - - .. tip:: - - Read this as "Signature/Message Fragment". That is, it could be a - fragment of a signature **or** a message, depending on the transaction. - -- ``tag`` (:py:class:`Tag`) - - Used to classify the transaction. - - Every transaction has a tag, but many transactions have empty tags. - -- ``timestamp`` (:py:class:`int`) - - Unix timestamp when the transaction was created. - - Note that devices can specify any timestamp when creating transactions, so - this value is not safe to use by itself for security measures (such as - resolving double-spends). - - .. note:: - - The IOTA protocol does support verifiable timestamps. Refer to the - `timestamps white paper`_ for more information. - -- ``trunk_transaction_hash`` (:py:class:`TransactionHash`) - - The transaction hash of the next transaction in the bundle. - - If this transaction is the head transaction, its ``trunk_transaction_hash`` - will be pseudo-randomly selected, similarly to ``branch_transaction_hash``. - -- ``value`` (:py:class:`int`) - - The number of IOTAs being transferred in this transaction: - - - If this value is negative, then the ``address`` is spending IOTAs. - - If it is positive, then the ``address`` is receiving IOTAs. - - If it is zero, then this transaction is being used to carry metadata (such - as a signature fragment or a message) instead of transferring IOTAs. - - -:todo: ProposedTransaction +.. code:: python + + from iota import Address, ProposedTransaction, Tag, Transaction + + txn_1 =\ + Transaction.from_tryte_string( + b'GYPRVHBEZOOFXSHQBLCYW9ICTCISLHDBNMMVYD9JJHQMPQCTIQAQTJNNNJ9IDXLRCC' + b'OYOXYPCLR9PBEY9ORZIEPPDNTI9CQWYZUOTAVBXPSBOFEQAPFLWXSWUIUSJMSJIIIZ' + b'WIKIRH9GCOEVZFKNXEVCUCIIWZQCQEUVRZOCMEL9AMGXJNMLJCIA9UWGRPPHCEOPTS' + b'VPKPPPCMQXYBHMSODTWUOABPKWFFFQJHCBVYXLHEWPD9YUDFTGNCYAKQKVEZYRBQRB' + b'XIAUX9SVEDUKGMTWQIYXRGSWYRK9SRONVGTW9YGHSZRIXWGPCCUCDRMAXBPDFVHSRY' + b'WHGB9DQSQFQKSNICGPIPTRZINYRXQAFSWSEWIFRMSBMGTNYPRWFSOIIWWT9IDSELM9' + b'JUOOWFNCCSHUSMGNROBFJX9JQ9XT9PKEGQYQAWAFPRVRRVQPUQBHLSNTEFCDKBWRCD' + b'X9EYOBB9KPMTLNNQLADBDLZPRVBCKVCYQEOLARJYAGTBFR9QLPKZBOYWZQOVKCVYRG' + b'YI9ZEFIQRKYXLJBZJDBJDJVQZCGYQMROVHNDBLGNLQODPUXFNTADDVYNZJUVPGB9LV' + b'PJIYLAPBOEHPMRWUIAJXVQOEM9ROEYUOTNLXVVQEYRQWDTQGDLEYFIYNDPRAIXOZEB' + b'CS9P99AZTQQLKEILEVXMSHBIDHLXKUOMMNFKPYHONKEYDCHMUNTTNRYVMMEYHPGASP' + b'ZXASKRUPWQSHDMU9VPS99ZZ9SJJYFUJFFMFORBYDILBXCAVJDPDFHTTTIYOVGLRDYR' + b'TKHXJORJVYRPTDH9ZCPZ9ZADXZFRSFPIQKWLBRNTWJHXTOAUOL9FVGTUMMPYGYICJD' + b'XMOESEVDJWLMCVTJLPIEKBE9JTHDQWV9MRMEWFLPWGJFLUXI9BXPSVWCMUWLZSEWHB' + b'DZKXOLYNOZAPOYLQVZAQMOHGTTQEUAOVKVRRGAHNGPUEKHFVPVCOYSJAWHZU9DRROH' + b'BETBAFTATVAUGOEGCAYUXACLSSHHVYDHMDGJP9AUCLWLNTFEVGQGHQXSKEMVOVSKQE' + b'EWHWZUDTYOBGCURRZSJZLFVQQAAYQO9TRLFFN9HTDQXBSPPJYXMNGLLBHOMNVXNOWE' + b'IDMJVCLLDFHBDONQJCJVLBLCSMDOUQCKKCQJMGTSTHBXPXAMLMSXRIPUBMBAWBFNLH' + b'LUJTRJLDERLZFUBUSMF999XNHLEEXEENQJNOFFPNPQ9PQICHSATPLZVMVIWLRTKYPI' + b'XNFGYWOJSQDAXGFHKZPFLPXQEHCYEAGTIWIJEZTAVLNUMAFWGGLXMBNUQTOFCNLJTC' + b'DMWVVZGVBSEBCPFSM99FLOIDTCLUGPSEDLOKZUAEVBLWNMODGZBWOVQT9DPFOTSKRA' + b'BQAVOQ9RXWBMAKFYNDCZOJGTCIDMQSQQSODKDXTPFLNOKSIZEOY9HFUTLQRXQMEPGO' + b'XQGLLPNSXAUCYPGZMNWMQWSWCKAQYKXJTWINSGPPZG9HLDLEAWUWEVCTVRCBDFOXKU' + b'ROXH9HXXAXVPEJFRSLOGRVGYZASTEBAQNXJJROCYRTDPYFUIQJVDHAKEG9YACV9HCP' + b'JUEUKOYFNWDXCCJBIFQKYOXGRDHVTHEQUMHO999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999999999999999999999999999999999999' + b'999999999999RKWEEVD99A99999999A99999999NFDPEEZCWVYLKZGSLCQNOFUSENI' + b'XRHWWTZFBXMPSQHEDFWZULBZFEOMNLRNIDQKDNNIELAOXOVMYEI9PGTKORV9IKTJZQ' + b'UBQAWTKBKZ9NEZHBFIMCLV9TTNJNQZUIJDFPTTCTKBJRHAITVSKUCUEMD9M9SQJ999' + b'999TKORV9IKTJZQUBQAWTKBKZ9NEZHBFIMCLV9TTNJNQZUIJDFPTTCTKBJRHAITVSK' + b'UCUEMD9M9SQJ999999999999999999999999999999999999999999999999999999' + b'999999999999999999999999999999999' + ) + +``Transaction`` is a transaction that has been loaded from the Tangle. + +Generally, you will never need to create ``Transaction`` objects; the +API will build them for you, as the result of various API methods. + +Each ``Transaction`` has the following attributes: + +- ``address: Address``: The address associated with this transaction. + Depending on the transaction's ``value``, this address may be a + sender or a recipient. +- ``branch_transaction_hash: TransactionHash``: An unrelated + transaction that this transaction "approves". Refer to the Basic + Concepts section for more information. +- ``bundle_hash: BundleHash``: The bundle hash, used to identify + transactions that are part of the same bundle. This value is + generated by taking a hash of the metadata from all transactions in + the bundle. +- ``current_index: int``: The transaction's position in the bundle. +- If the ``current_index`` value is 0, then this is the "tail + transaction". +- If it is equal to ``last_index``, then this is the "head + transaction". +- ``hash: TransactionHash``: The transaction hash, used to uniquely + identify the transaction on the Tangle. This value is generated by + taking a hash of the raw transaction trits. +- ``is_confirmed: Optional[bool]``: Whether this transaction has been + "confirmed". Refer to the Basic Concepts section for more + information. +- ``last_index: int``: The index of the final transaction in the + bundle. This value is attached to every transaction to make it easier + to traverse and verify bundles. +- ``nonce: Hash``: This is the product of the PoW process. +- ``signature_message_fragment: Fragment``: Additional data attached to + the transaction: +- If ``value < 0``, this value contains a fragment of the cryptographic + signature authorizing the spending of the IOTAs. +- If ``value > 0``, this value is an (optional) string message attached + to the transaction. +- If ``value = 0``, this value could be either a signature or message + fragment, depending on the previous transaction. +- ``tag: Tag``: Used to classify the transaction. Many transactions + have empty tags (``Tag(b'999999999999999999999999999')``). +- ``timestamp: int``: Unix timestamp when the transaction was created. + Note that devices can specify any timestamp when creating + transactions, so this value is not safe to use for security measures + (such as resolving double-spends). +- ``trunk_transaction_hash: TransactionHash``: The transaction hash of + the next transaction in the bundle. If this transaction is the head + transaction, its ``trunk_transaction_hash`` will be pseudo-randomly + selected, similarly to ``branch_transaction_hash``. +- ``value: int``: The number of IOTAs being transferred in this + transaction: +- If this value is negative, then the ``address`` is spending IOTAs. +- If it is positive, then the ``address`` is receiving IOTAs. +- If it is zero, then this transaction is being used to carry metadata + (such as a signature fragment or a message) instead of transferring + IOTAs. + +ProposedTransaction +~~~~~~~~~~~~~~~~~~~ + +``ProposedTransaction`` is a transaction that was created locally and +hasn't been broadcast yet. + +.. code:: python + + txn_2 =\ + ProposedTransaction( + address = + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999XE9IVG' + b'EFNDOCQCMERGUATCIEGGOHPHGFIAQEZGNHQ9W99CH' + ), + + message = TryteString.from_string('thx fur cheezburgers'), + tag = Tag(b'KITTEHS'), + value = 42, + ) + +This type is useful when creating new transactions to broadcast to the +Tangle. Note that creating a ``ProposedTransaction`` requires only a +small subset of the attributes needed to create a ``Transaction`` +object. + +To create a ``ProposedTransaction``, specify the following values: + +- ``address: Address``: The address associated with the transaction. + Note that each transaction references exactly one address; in order + to transfer IOTAs from one address to another, you must create at + least two transactions: One to deduct the IOTAs from the sender's + balance, and one to add the IOTAs to the recipient's balance. +- ``message: Optional[TryteString]``: Optional trytes to attach to the + transaction. This could be any value (character strings, binary data, + or raw trytes), as long as it's converted to a ``TryteString`` first. +- ``tag: Optional[Tag]``: Optional tag to classify this transaction. + Each transaction may have exactly one tag, and the tag is limited to + 27 trytes. +- ``value: int``: The number of IOTAs being transferred in this + transaction. This value can be 0; for example, to send a message + without spending any IOTAs. + +Bundle Types +------------ + +As with transactions, PyOTA defines two bundle types. + +Bundle +~~~~~~ + +.. code:: python + + from iota import Bundle + + bundle = Bundle.from_tryte_strings([ + b'GYPRVHBEZOOFXSHQBLCYW9ICTCISLHDBNMMVYD9JJHQMPQCTIQAQTJNNNJ9IDXLRCC...', + b'OYOXYPCLR9PBEY9ORZIEPPDNTI9CQWYZUOTAVBXPSBOFEQAPFLWXSWUIUSJMSJIIIZ...', + # etc. + ]) + +``Bundle`` represents a bundle of transactions published on the Tangle. +It is intended to be a read-only object, allowing you to inspect the +transactions and bundle metadata. + +Each bundle has the following attributes: + +- ``hash: BundleHash``: The hash of this bundle. This value is + generated by taking a hash of the metadata from all transactions in + the bundle. +- ``is_confirmed: Optional[bool]``: Whether the transactions in this + bundle have been confirmed. Refer to the Basic Concepts section for + more information. +- ``tail_transaction: Optional[Transaction]``: The bundle's tail + transaction. +- ``transactions: List[Transaction]``: The transactions associated with + this bundle. + +ProposedBundle +~~~~~~~~~~~~~~ + +.. code:: python + + from iota import Address, ProposedBundle, ProposedTransaction + from iota.crypto.signing import KeyGenerator + + bundle = ProposedBundle() + + bundle.add_transaction(ProposedTransaction(...)) + bundle.add_transaction(ProposedTransaction(...)) + bundle.add_transaction(ProposedTransaction(...)) + + bundle.add_inputs([ + Address( + address = + b'TESTVALUE9DONTUSEINPRODUCTION99999HAA9UA' + b'MHCGKEUGYFUBIARAXBFASGLCHCBEVGTBDCSAEBTBM', + + balance = 86, + key_index = 0, + ), + ]) + + bundle.send_unspent_inputs_to( + Address( + b'TESTVALUE9DONTUSEINPRODUCTION99999D99HEA' + b'M9XADCPFJDFANCIHR9OBDHTAGGE9TGCI9EO9ZCRBN' + ), + ) + + bundle.finalize() + bundle.sign_inputs(KeyGenerator(b'SEED9GOES9HERE')) +.. note:: -.. _protocol documentation: https://iota.readme.io/docs/ -.. _paste trytes: https://forum.iota.org/t/3457/3 -.. _timestamps white paper: https://iota.org/timestamps.pdf + This section contains information about how PyOTA works "under the + hood". + + The ``prepare_transfer`` API method encapsulates this functionality + for you; it is not necessary to understand how ``ProposedBundle`` + works in order to use PyOTA. + + +``ProposedBundle`` provides a convenient interface for creating new +bundles, listed in the order that they should be invoked: + +- ``add_transaction: (ProposedTransaction) -> None``: Adds a + transaction to the bundle. If necessary, it may split the transaction + into multiple (for example, if the transaction's message is too long + to fit into 2187 trytes). +- ``add_inputs: (List[Address]) -> None``: Specifies inputs that can be + used to fund transactions that spend IOTAs. The ``ProposedBundle`` + will use these to create the necessary input transactions. +- You can use the ``get_inputs`` API command to find suitable inputs. +- ``send_unspent_inputs_to: (Address) -> None``: Specifies the address + that will receive unspent IOTAs. The ``ProposedBundle`` will use this + to create the necessary change transaction, if necessary. +- ``finalize: () -> None``: Prepares the bundle for PoW. Once this + method is invoked, no new transactions may be added to the bundle. +- ``sign_inputs: (KeyGenerator) -> None``: Generates the necessary + cryptographic signatures to authorize spending the inputs. You do not + need to invoke this method if the bundle does not contain any + transactions that spend IOTAs. + +Once the ``ProposedBundle`` has been finalized (and inputs signed, if +necessary), invoke its ``as_tryte_strings`` method to generate the raw +trytes that should be included in an ``attach_to_tangle`` API request. From b342e6fb429100c5287418c2a654234085acaf76 Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Wed, 6 Dec 2017 12:28:40 +0100 Subject: [PATCH 11/27] miltisig initial documentation --- docs/index.rst | 1 + docs/multisig.rst | 122 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 docs/multisig.rst diff --git a/docs/index.rst b/docs/index.rst index 8eb81e6..1ed2d68 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -7,6 +7,7 @@ adapters addresses api + multisig .. note:: **🚧 PyOTA documentation is still under construction. 🚧** diff --git a/docs/multisig.rst b/docs/multisig.rst new file mode 100644 index 0000000..00c336e --- /dev/null +++ b/docs/multisig.rst @@ -0,0 +1,122 @@ +Multisignature +============== + +Multisignature transactions are transactions which require multiple signatures before execution. In simplest example it means that, if there is token wallet which require 5 signatures from different parties, all 5 parties must sign spent transaction, before it will be processed. + +It is standard functionality in blockchain systems and it is also implemented in IOTA + +.. note:: + + You can read more about IOTA multisignature on the `wiki`_. + +Generating multisignature address +--------------------------------- + +In order to use multisignature functionality, a special multisignature address must be created. It is done by adding each key digest in agreed order into digests list. At the end, last participant is converting digests list (Curl state trits) into multisignature address. + +Here is the example where digest is created: + +.. code-block:: python + + # Create digest 3 of 3. + api_3 =\ + MultisigIota( + adapter = 'http://localhost:14265', + + seed = + Seed( + b'TESTVALUE9DONTUSEINPRODUCTION99999JYFRTI' + b'WMKVVBAIEIYZDWLUVOYTZBKPKLLUMPDF9PPFLO9KT', + ), + ) + + gd_result = api_3.get_digests(index=8, count=1, security_level=2) + + digest_3 = gd_result['digests'][0] # type: Digest + +And here is example where digests are converted into multisignature address: + +.. code-block:: python + + 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 + + + +Prepare transfer +------------------ + +.. note:: + + Since spending tokens from the same address more than once is insecure, remainder should be transferred to other address. So, this address should be created before as next to be used multisignature address. + +First signer for multisignature wallet is defining address where tokens should be transferred and next wallet address for reminder: + +.. code-block:: python + + 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'), + ), + ], + + # 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, + + # 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] + + +Sign the inputs +--------------- + +.. note:: + + Validate the signatures. + +Broadcast the bundle +-------------------- + +Remarks +------- + +Full code `example`_. + +.. note:: + + How M-of-N works + +.. _example: https://github.com/iotaledger/iota.lib.py/blob/develop/examples/multisig.py +.. _wiki: https://github.com/iotaledger/wiki/blob/master/multisigs.md From 2c63162130fd85b53cbdd43a9cee57eb03737b41 Mon Sep 17 00:00:00 2001 From: plenarius Date: Thu, 7 Dec 2017 20:47:59 -0500 Subject: [PATCH 12/27] Issue #89 - Initial work on adding checksum option to get_new_addresses --- examples/address_generator.py | 21 +++++++-- iota/api.py | 8 +++- iota/commands/extended/get_new_addresses.py | 27 ++++++++--- iota/crypto/addresses.py | 10 ++-- .../extended/get_new_addresses_test.py | 46 +++++++++++++++++++ 5 files changed, 98 insertions(+), 14 deletions(-) diff --git a/examples/address_generator.py b/examples/address_generator.py index f48328a..49d5141 100644 --- a/examples/address_generator.py +++ b/examples/address_generator.py @@ -16,8 +16,8 @@ from six import binary_type, moves as compat, text_type -def main(uri, index, count): - # type: (Text, int, Optional[int], bool) -> None +def main(uri, index, count, security, checksum): + # type: (Text, int, Optional[int], Optional[int], Optional[bool]) -> None seed = get_seed() # Create the API instance. @@ -34,7 +34,7 @@ def main(uri, index, count): print('') # Here's where all the magic happens! - api_response = api.get_new_addresses(index, count) + api_response = api.get_new_addresses(index, count, security, checksum) for addy in api_response['addresses']: print(binary_type(addy).decode('ascii')) @@ -111,4 +111,19 @@ def output_seed(seed): 'If not specified, the first unused address will be returned.' ) + parser.add_argument( + '--security', + type = int, + default = 2, + 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/iota/api.py b/iota/api.py index 251bfa4..674425c 100644 --- a/iota/api.py +++ b/iota/api.py @@ -611,8 +611,9 @@ def get_new_addresses( index = 0, count = 1, security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL, + checksum = False, ): - # type: (int, Optional[int], int) -> dict + # type: (int, Optional[int], int, Optional[bool]) -> dict """ Generates one or more new addresses from the seed. @@ -636,6 +637,10 @@ def get_new_addresses( 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:: @@ -651,6 +656,7 @@ def get_new_addresses( count = count, index = index, securityLevel = security_level, + checksum = checksum, seed = self.seed, ) diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index b14c9a8..f2aefd2 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -6,7 +6,7 @@ import filters as f -from iota import Address +from iota import Address, AddressChecksum from iota.commands import FilterCommand, RequestFilter from iota.commands.core.find_transactions import FindTransactionsCommand from iota.crypto.addresses import AddressGenerator @@ -36,25 +36,35 @@ def _execute(self, request): count = request['count'] # type: Optional[int] index = request['index'] # type: int security_level = request['securityLevel'] # type: int + checksum = request['checksum'] # type: Optional[bool] seed = request['seed'] # type: Seed return { - 'addresses': self._find_addresses(seed, index, count, security_level), + 'addresses': + self._find_addresses(seed, index, count, security_level, checksum), } - def _find_addresses(self, seed, index, count, security_level): - # type: (Seed, int, Optional[int], int) -> List[Address] + def _find_addresses(self, seed, index, count, security_level, checksum): + # type: (Seed, int, Optional[int], int, Optional[bool]) -> List[Address] """ Find addresses matching the command parameters. """ - # type: (Seed, int, Optional[int]) -> List[Address] - generator = AddressGenerator(seed, security_level) + # type: (Seed, int, Optional[bool]) -> List[Address] + 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): - response = FindTransactionsCommand(self.adapter)(addresses=[addy]) + # If we're generating addresses with checksums we need to check + # for transactions on the address without the checksum + if not checksum: + response = FindTransactionsCommand(self.adapter)(addresses=[addy]) + else: + response = FindTransactionsCommand(self.adapter)( + addresses=[addy][:-AddressChecksum.LEN] + ) + if not response.get('hashes'): return [addy] @@ -84,6 +94,8 @@ def __init__(self): | f.Max(self.MAX_SECURITY_LEVEL) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), + 'checksum': f.Type(bool) | f.Optional(default=False), + 'seed': f.Required | Trytes(result_type=Seed), }, @@ -91,5 +103,6 @@ def __init__(self): 'count', 'index', 'securityLevel', + 'checksum', }, ) diff --git a/iota/crypto/addresses.py b/iota/crypto/addresses.py index 2c1a570..a2a5ac6 100644 --- a/iota/crypto/addresses.py +++ b/iota/crypto/addresses.py @@ -40,11 +40,12 @@ class AddressGenerator(Iterable[Address]): - :py:class:`iota.transaction.BundleValidator` """ - def __init__(self, seed, security_level=DEFAULT_SECURITY_LEVEL): - # type: (TrytesCompatible, int) -> None + 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): @@ -175,7 +176,10 @@ def _generate_address(self, key_iterator): Used in the event of a cache miss. """ - return self.address_from_digest(self._get_digest(key_iterator)) + 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): diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index ea1a120..dadf953 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -35,6 +35,7 @@ def test_pass_happy_path(self): 'index': 1, 'count': 1, 'securityLevel': 2, + 'checksum': False, } filter_ = self._filter(request) @@ -59,6 +60,7 @@ def test_pass_optional_parameters_excluded(self): 'index': 0, 'count': None, 'securityLevel': AddressGenerator.DEFAULT_SECURITY_LEVEL, + 'checksum': False, }, ) @@ -75,6 +77,9 @@ def test_pass_compatible_types(self): 'index': 100, 'count': 8, 'securityLevel': 2, + + # ``checksum`` must be boolean. + 'checksum': False, }) self.assertFilterPasses(filter_) @@ -86,6 +91,7 @@ def test_pass_compatible_types(self): 'index': 100, 'count': 8, 'securityLevel': 2, + 'checksum': False, }, ) @@ -111,6 +117,7 @@ def test_fail_unexpected_parameters(self): 'index': None, 'count': 1, 'securityLevel': 2, + 'checksum': False, # Some men just want to watch the world burn. 'foo': 'bar', @@ -306,6 +313,21 @@ def test_fail_security_level_wrong_type(self): }, ) + def test_fail_checksum_wrong_type(self): + """ + ``checksum`` is not a boolean. + """ + self.assertFilterErrors( + { + 'checksum': '2', + 'seed': Seed(self.seed), + }, + + { + 'checksum': [f.Type.CODE_WRONG_TYPE], + }, + ) + class GetNewAddressesCommandTestCase(TestCase): # noinspection SpellCheckingInspection @@ -333,6 +355,13 @@ def setUp(self): b'IWYTLQUUHDWSOVXLIKVJTYZBFKLABWRBFYVSMD9NB', ) + self.addy_1_checksum =\ + Address( + b'NYMWLBUJEISSACZZBRENC9HEHYQXHCGQHSNHVCEA' + b'ZDCTEVNGSDUEKTSYBSQGMVJRIEDHWDYSEYCFAZAH' + b'9T9FPJROTW', + ) + def test_wireup(self): """ Verify that the command is wired up correctly. @@ -446,3 +475,20 @@ def test_get_addresses_online(self): }, ], ) + + def test_new_address_checksum(self): + """ + Generate address with a checksum. + """ + response =\ + self.command( + count = 1, + index = 0, + seed = self.seed, + checksum = True, + ) + + self.assertDictEqual( + response, + {'addresses': [self.addy_1_checksum]}, + ) From 9c67ba3f503c377f4f427ae661e3e2aa1843be55 Mon Sep 17 00:00:00 2001 From: plenarius Date: Fri, 8 Dec 2017 17:16:13 -0500 Subject: [PATCH 13/27] Change Optional[bool] to just bool, alphabetical string ordering, use AddressGenerator.DEFAULT_SECURITY_LEVEL instead of hardcoded 2 in example, use addy.address for checksum-less address --- examples/address_generator.py | 8 +++-- iota/api.py | 2 +- iota/commands/extended/get_new_addresses.py | 27 ++++++--------- .../extended/get_new_addresses_test.py | 2 +- test/crypto/addresses_test.py | 34 +++++++++++++++++++ 5 files changed, 52 insertions(+), 21 deletions(-) diff --git a/examples/address_generator.py b/examples/address_generator.py index 49d5141..01043e6 100644 --- a/examples/address_generator.py +++ b/examples/address_generator.py @@ -12,12 +12,13 @@ from typing import Optional, Text from iota import __version__, Iota +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], Optional[bool]) -> None + # type: (Text, int, Optional[int], Optional[int], bool) -> None seed = get_seed() # Create the API instance. @@ -114,8 +115,9 @@ def output_seed(seed): parser.add_argument( '--security', type = int, - default = 2, - help = 'Security level to be used for the private key / address. Can be 1, 2 or 3', + 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( diff --git a/iota/api.py b/iota/api.py index 674425c..3cf85b2 100644 --- a/iota/api.py +++ b/iota/api.py @@ -613,7 +613,7 @@ def get_new_addresses( security_level = AddressGenerator.DEFAULT_SECURITY_LEVEL, checksum = False, ): - # type: (int, Optional[int], int, Optional[bool]) -> dict + # type: (int, Optional[int], int, bool) -> dict """ Generates one or more new addresses from the seed. diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index f2aefd2..7acf1a8 100644 --- a/iota/commands/extended/get_new_addresses.py +++ b/iota/commands/extended/get_new_addresses.py @@ -33,10 +33,10 @@ 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 - checksum = request['checksum'] # type: Optional[bool] seed = request['seed'] # type: Seed return { @@ -45,27 +45,22 @@ def _execute(self, request): } def _find_addresses(self, seed, index, count, security_level, checksum): - # type: (Seed, int, Optional[int], int, Optional[bool]) -> List[Address] + # type: (Seed, int, Optional[int], int, bool) -> List[Address] """ Find addresses matching the command parameters. """ - # type: (Seed, int, Optional[bool]) -> List[Address] 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): - # If we're generating addresses with checksums we need to check - # for transactions on the address without the checksum - if not checksum: - response = FindTransactionsCommand(self.adapter)(addresses=[addy]) - else: - response = FindTransactionsCommand(self.adapter)( - addresses=[addy][:-AddressChecksum.LEN] + # 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] @@ -85,8 +80,10 @@ def __init__(self): super(GetNewAddressesRequestFilter, self).__init__( { # Everything except ``seed`` is optional. - 'count': f.Type(int) | f.Min(1), - 'index': f.Type(int) | f.Min(0) | f.Optional(default=0), + + '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': f.Type(int) @@ -94,15 +91,13 @@ def __init__(self): | f.Max(self.MAX_SECURITY_LEVEL) | f.Optional(default=AddressGenerator.DEFAULT_SECURITY_LEVEL), - 'checksum': f.Type(bool) | f.Optional(default=False), - 'seed': f.Required | Trytes(result_type=Seed), }, allow_missing_keys = { + 'checksum', 'count', 'index', 'securityLevel', - 'checksum', }, ) diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index dadf953..735aa90 100644 --- a/test/commands/extended/get_new_addresses_test.py +++ b/test/commands/extended/get_new_addresses_test.py @@ -482,10 +482,10 @@ def test_new_address_checksum(self): """ response =\ self.command( + checksum = True, count = 1, index = 0, seed = self.seed, - checksum = True, ) self.assertDictEqual( diff --git a/test/crypto/addresses_test.py b/test/crypto/addresses_test.py index 6c1773d..64487ca 100644 --- a/test/crypto/addresses_test.py +++ b/test/crypto/addresses_test.py @@ -279,3 +279,37 @@ def test_security_level_elevated(self): ), ], ) + + def test_generator_checksum(self): + """ + Creating a generator with checksums on the addresses. + """ + ag = AddressGenerator( + self.seed_2, + security_level=AddressGenerator.DEFAULT_SECURITY_LEVEL, + checksum=True + ) + + generator = ag.create_iterator() + + # noinspection SpellCheckingInspection + self.assertEqual( + next(generator), + + Address( + b'FNKCVJPUANHNWNBAHFBTCONMCUBC9KCZ9EKREBCJ' + b'AFMABCTEPLGGXDJXVGPXDCFOUCRBWFJFLEAVOEUPY' + b'ADHVCBXFD', + ), + ) + + # noinspection SpellCheckingInspection + self.assertEqual( + next(generator), + + Address( + b'MSYILYYZLSJ99TDMGQHDOBWGHTBARCBGJZE9PIMQ' + b'LTEXJXKTDREGVTPA9NDGGLQHTMGISGRAKSLYPGWMB' + b'WIKQRCIOD', + ), + ) From d66776d688c0bae4dbede346c1aba5e105e3ef46 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Sun, 10 Dec 2017 11:36:43 +1300 Subject: [PATCH 14/27] QoL enhancements for docs devs. - Added extra for pip install. - Now uses RTD theme when building locally. - Added instructions for building docs locally. - Removed construction.gif from `index.rst`. --- MANIFEST.in | 4 ++++ README.rst | 33 ++++++++++++++++++++++++++++----- docs/conf.py | 2 +- docs/index.rst | 5 ----- setup.py | 1 + 5 files changed, 34 insertions(+), 11 deletions(-) diff --git a/MANIFEST.in b/MANIFEST.in index 7e586bf..77d712f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,8 @@ +include docs/conf.py +include docs/make.bat +include docs/Makefile include LICENSE include README.rst +recursive-include docs *.rst recursive-include examples *.py recursive-include test *.py *.csv diff --git a/README.rst b/README.rst index 997fd9b..f199041 100644 --- a/README.rst +++ b/README.rst @@ -45,12 +45,14 @@ To install this extension, use the following command:: pip install pyota[ccurl] +.. _readme-installing-from-source: + Installing from Source ====================== -1. `Create virtualenv`_ (recommended, but not required). -2. ``git clone https://github.com/iotaledger/iota.lib.py.git`` -3. ``pip install -e .`` +#. `Create virtualenv`_ (recommended, but not required). +#. ``git clone https://github.com/iotaledger/iota.lib.py.git`` +#. ``pip install -e .`` Running Unit Tests ------------------ @@ -66,12 +68,33 @@ PyOTA is also compatible with `tox`_:: ============= Documentation ============= -For the full documentation of this library, please refer to the -`official API`_ +PyOTA's documentation is available on `ReadTheDocs`_. + +If you are :ref:`installing from source `, you +can also build the documentation locally: + +#. Install extra dependencies (you only have to do this once):: + + pip install '.[docs-builder]' + + .. tip:: + + To install the CCurl extension and the documentation builder tools + together, use the following command:: + + pip install '.[ccurl,docs-builder]' + +#. Switch to the ``docs`` directory:: + + cd docs + +#. Build the documentation:: + make html .. _Create virtualenv: https://realpython.com/blog/python/python-virtual-environments-a-primer/ .. _PyOTA Bug Tracker: https://github.com/iotaledger/iota.lib.py/issues +.. _ReadTheDocs: https://pyota.readthedocs.io/ .. _Slack: https://slack.iota.org/ .. _dedicated forum: https://forum.iota.org/ .. _official API: https://iota.readme.io/ diff --git a/docs/conf.py b/docs/conf.py index d719762..b524b3c 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -83,7 +83,7 @@ # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. # -# html_theme = 'alabaster' +html_theme = 'sphinx_rtd_theme' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the diff --git a/docs/index.rst b/docs/index.rst index 8eb81e6..9c45459 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -8,9 +8,4 @@ addresses api -.. note:: - **🚧 PyOTA documentation is still under construction. 🚧** - - Follow https://github.com/iotaledger/iota.lib.py/issues/78 for updates. - .. include:: ../README.rst diff --git a/setup.py b/setup.py index 22a18c4..20cfe4e 100644 --- a/setup.py +++ b/setup.py @@ -60,6 +60,7 @@ extras_require = { 'ccurl': ['pyota-ccurl'], + 'docs-builder': ['sphinx', 'sphinx_rtd_theme'], }, test_suite = 'test', From 105679303e7b122c108155abbf0777e4b3a00a1c Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Mon, 11 Dec 2017 12:03:29 +0100 Subject: [PATCH 15/27] extended Generating multisignature address chapter --- docs/multisig.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/multisig.rst b/docs/multisig.rst index 00c336e..9d3ef82 100644 --- a/docs/multisig.rst +++ b/docs/multisig.rst @@ -14,7 +14,13 @@ Generating multisignature address In order to use multisignature functionality, a special multisignature address must be created. It is done by adding each key digest in agreed order into digests list. At the end, last participant is converting digests list (Curl state trits) into multisignature address. -Here is the example where digest is created: +.. note:: + + Each multisignature addresses participant has to create its own digest locally. Then, when it is created it can be safely shared with other participants, in order to build list of digests which then will be converted into multisignature address. + + Created digests should be shared with each multisignature participant, so each one of them could regenerate address and ensure it is OK. + +Here is the examples where digest is created: .. code-block:: python @@ -47,6 +53,11 @@ And here is example where digests are converted into multisignature address: # has a single value. multisig_address = cma_result['address'] # type: MultisigAddress +.. note:: + + As you can see in above example, multisignature addresses is created from list of digests, and in this case **order** is important. The same order need to be used in **signing transfer**. + + Prepare transfer From fdb104ac2d87e9d6f2b44fd2ea9a3f0c185a5e51 Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Mon, 11 Dec 2017 14:03:27 +0100 Subject: [PATCH 16/27] more extended Generating multisignature address chapter --- docs/multisig.rst | 65 +++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 2 deletions(-) diff --git a/docs/multisig.rst b/docs/multisig.rst index 9d3ef82..8e13752 100644 --- a/docs/multisig.rst +++ b/docs/multisig.rst @@ -20,7 +20,7 @@ In order to use multisignature functionality, a special multisignature address m Created digests should be shared with each multisignature participant, so each one of them could regenerate address and ensure it is OK. -Here is the examples where digest is created: +Here is the example where digest is created: .. code-block:: python @@ -113,13 +113,57 @@ First signer for multisignature wallet is defining address where tokens should b Sign the inputs --------------- +When trytes are prepared, round of signing must be performed. Order of signing must be the same as in generate multisignature addresses procedure (as described above). + +.. note:: + + In example below, all signing is done on one local machine. In real case, each participant sign bundle locally and then passes it to next participant in previously defined order + + **index**, **count** and **security_lavel** parameters for each private key should be the same as used in **get_digests** function in previous steps. + +.. code-block:: python + + bundle = Bundle.from_tryte_strings(prepared_trytes) + + 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.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.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.sign_input_transactions(bundle, 7) + + signed_trytes = bundle.as_tryte_strings() + .. note:: - Validate the signatures. + After creation, bundle can be optionally validated: + + .. code-block:: python + + 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), + ), + ) + + Broadcast the bundle -------------------- +When bundle is created it can be broadcasted in standard way: + +.. code-block:: python + + api_1.send_trytes(trytes=signed_trytes, depth=3) + Remarks ------- @@ -129,5 +173,22 @@ Full code `example`_. How M-of-N works + One of the key differences between IOTA multi-signatures is that M-of-N (e.g. 3 of 5) works differently. What this means is that in order to successfully spend inputs, all of the co-signers have to sign the transaction. As such, in order to enable M-of-N we have to make use of a simple trick: sharing of private keys. + + This concept is best explained with a concrete example: + + Lets say that we have a multi-signature between 3 parties: Alice, Bob and Carol. Each has their own private key, and they generated a new multi-signature address in the aforementioned order. Currently, this is a 3 of 3 multisig. This means that all 3 participants (Alice, Bob and Carol) need to sign the inputs with their private keys in order to successfully spend them. + + In order to enable a 2 of 3 multisig, the cosigners need to share their private keys with the other parties in such a way that no single party can sign inputs alone, but that still enables an M-of-N multsig. In our example, the sharing of the private keys would look as follows: + + Alice -> Bob + + Bob -> Carol + + Carol -> Alice + + Now, each participant holds two private keys that he/she can use to collude with another party to successfully sign the inputs and make a transaction. But no single party holds enough keys (3 of 3) to be able to independently make the transaction. + + .. _example: https://github.com/iotaledger/iota.lib.py/blob/develop/examples/multisig.py .. _wiki: https://github.com/iotaledger/wiki/blob/master/multisigs.md From 753d7a60c1b24786a6bdcdfaa85bd41c7237e4c3 Mon Sep 17 00:00:00 2001 From: Damian Melniczuk Date: Wed, 13 Dec 2017 13:28:36 +0100 Subject: [PATCH 17/27] extented important info for multisig --- docs/multisig.rst | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/multisig.rst b/docs/multisig.rst index 8e13752..d245076 100644 --- a/docs/multisig.rst +++ b/docs/multisig.rst @@ -189,6 +189,25 @@ Full code `example`_. Now, each participant holds two private keys that he/she can use to collude with another party to successfully sign the inputs and make a transaction. But no single party holds enough keys (3 of 3) to be able to independently make the transaction. +Important +--------- + +There are some general rules (repeated once again for convenience) which should be followed while working with multisignature addresses (and in general with IOTA): + +Signing order is important +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +When creating a multi-signature address and when signing a transaction for that address, it is important to follow the exact order that was used during the initial creation. If we have a multi-signature address that was signed in the following order: Alice -> Bob -> Carol. You will not be able to spend these inputs if you provide the signatures in a different order (e.g. Bob -> Alice -> Carol). As such, keep the signing order in mind. + +Never re-use keys +~~~~~~~~~~~~~~~~~ + +Probably the most important rule to keep in mind: absolutely never re-use private keys. IOTA uses one-time Winternitz signatures, which means that if you re-use private keys you significantly decrease the security of your private keys, up to the point where signing of another transaction can be done on a conventional computer within few days. Therefore, when generating a new multi-signature with your co-signers, always increase the private key **index counter** and only use a single private key once. Don't use it for any other multi-signatures and don't use it for any personal transactions. + +Never share your private keys +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Under no circumstances - other than wanting to reduce the requirements for a multi-signature (see section **How M-of-N works**) - should you share your private keys. Sharing your private keys with others means that they can sign your part of the multi-signature successfully. .. _example: https://github.com/iotaledger/iota.lib.py/blob/develop/examples/multisig.py .. _wiki: https://github.com/iotaledger/wiki/blob/master/multisigs.md From a480d603d20e1714c2fdd7943ad8b02adca60bea Mon Sep 17 00:00:00 2001 From: plenarius Date: Mon, 18 Dec 2017 19:28:34 -0500 Subject: [PATCH 18/27] Initial work on #101 - Create a filter that just quietly removes checksums since the core api doesn't care for them, update get_balances and find_transactions with that filter --- iota/commands/core/find_transactions.py | 4 +- iota/commands/core/get_balances.py | 4 +- iota/filters.py | 71 +++++++++++++++++++++++-- test/filters_test.py | 47 +++++++++++++++- 4 files changed, 118 insertions(+), 8 deletions(-) diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index ab5ba5a..9addaf3 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -7,7 +7,7 @@ from iota import Address, Tag, TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter -from iota.filters import Trytes +from iota.filters import Trytes, AddressNoChecksum __all__ = [ 'FindTransactionsCommand', @@ -43,7 +43,7 @@ def __init__(self): f.Array | f.FilterRepeater( f.Required - | Trytes(result_type=Address) + | AddressNoChecksum(result_type=Address) | f.Unicode(encoding='ascii', normalize=False) ) ), diff --git a/iota/commands/core/get_balances.py b/iota/commands/core/get_balances.py index 54bf824..5c96f8f 100644 --- a/iota/commands/core/get_balances.py +++ b/iota/commands/core/get_balances.py @@ -6,7 +6,7 @@ from iota import Address from iota.commands import FilterCommand, RequestFilter, ResponseFilter -from iota.filters import Trytes +from iota.filters import Trytes, AddressNoChecksum __all__ = [ 'GetBalancesCommand', @@ -37,7 +37,7 @@ def __init__(self): | f.Array | f.FilterRepeater( f.Required - | Trytes(result_type=Address) + | AddressNoChecksum(result_type=Address) | f.Unicode(encoding='ascii', normalize=False) ) ), diff --git a/iota/filters.py b/iota/filters.py index e8a0df7..ac41d55 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -9,7 +9,6 @@ from iota import Address, TryteString, TrytesCompatible - class GeneratedAddress(f.BaseFilter): """ Validates an incoming value as a generated :py:class:`Address` (must @@ -69,10 +68,12 @@ class Trytes(f.BaseFilter): """ Validates a sequence as a sequence of trytes. """ - CODE_NOT_TRYTES = 'not_trytes' - CODE_WRONG_FORMAT = 'wrong_format' + ADDRESS_BAD_CHECKSUM = 'address_bad_checksum' + CODE_NOT_TRYTES = 'not_trytes' + CODE_WRONG_FORMAT = 'wrong_format' templates = { + ADDRESS_BAD_CHECKSUM: 'The checksum for this address is invalid.', CODE_NOT_TRYTES: 'This value is not a valid tryte sequence.', CODE_WRONG_FORMAT: 'This value is not a valid {result_type}.', } @@ -142,3 +143,67 @@ def _apply(self, value): 'result_type': self.result_type.__name__, }, ) + + +class AddressNoChecksum(Trytes): + """ + Validates a sequence as a sequence of trytes. + """ + + 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 is an Address then we just make sure it has no checksum + if isinstance(value, Address): + if value.checksum and not value.is_checksum_valid(): + return self._invalid_value( + value = value, + reason = self.ADDRESS_BAD_CHECKSUM, + exc_info = True, + + template_vars = { + 'result_type': self.ADDRESS_BAD_CHECKSUM, + }, + ) + return Address(value.address) + + # 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) + + # Now coerce to the expected type and verify that there are no + # type-specific errors. + try: + addy = Address(value) + if addy.checksum and not addy.is_checksum_valid(): + return self._invalid_value( + value = addy, + reason = self.ADDRESS_BAD_CHECKSUM, + exc_info = True, + + template_vars = { + 'result_type': self.ADDRESS_BAD_CHECKSUM, + }, + ) + return Address(addy.address) + except ValueError: + return self._invalid_value( + value = value, + reason = self.CODE_WRONG_FORMAT, + exc_info = True, + + template_vars = { + 'result_type': self.result_type.__name__, + }, + ) \ No newline at end of file diff --git a/test/filters_test.py b/test/filters_test.py index 41dc817..b0d1fd7 100644 --- a/test/filters_test.py +++ b/test/filters_test.py @@ -6,7 +6,7 @@ from filters.test import BaseFilterTestCase from iota import Address, TryteString, TransactionHash -from iota.filters import GeneratedAddress, NodeUri, Trytes +from iota.filters import GeneratedAddress, NodeUri, Trytes, AddressNoChecksum class GeneratedAddressTestCase(BaseFilterTestCase): @@ -201,3 +201,48 @@ def test_fail_wrong_type(self): [TryteString(b'RBTC9D9DCDQAEASBYBCCKBFA')], [f.Type.CODE_WRONG_TYPE], ) + + +# noinspection SpellCheckingInspection +class AddressNoChecksumTestCase(BaseFilterTestCase): + filter_type = AddressNoChecksum + + # noinspection SpellCheckingInspection + def setUp(self): + super(AddressNoChecksumTestCase, self).setUp() + + # Define some addresses that we can reuse between tests + """ + Incoming value is not an :py:class:`Address` instance. + """ + self.tryte1 = ( + b'TESTVALUE9DONTUSEINPRODUCTION99999FBFFTG' + b'QFWEHEL9KCAFXBJBXGE9HID9XCOHFIDABHDG9AHDR' + ) + self.checksum = b'ENXYJOBP9' + self.address = Address(self.tryte1) + self.address_with_checksum = Address(self.tryte1 + self.checksum) + self.address_with_bad_checksum = Address(self.tryte1 + b'DEADBEEF9') + + def test_pass_checksumless_addy(self): + """ + Incoming value is tryte in address form or Address object + """ + self.assertFilterPasses(self.tryte1) + self.assertFilterPasses(self.address) + + def test_pass_withchecksum_addy(self): + """ + After passing through the filter an address with a checksum should + return the address without + """ + self.assertFilterPasses(self.address_with_checksum, self.address) + + def test_fail_badchecksum_addy(self): + """ + If they've got a bad checksum in their address we should probably tell + them so they don't wonder why something works in one place and not another + """ + self.assertFilterErrors( + self.address_with_bad_checksum, + [AddressNoChecksum.ADDRESS_BAD_CHECKSUM]) From ad270069978849cde658117adbd13476ff960706 Mon Sep 17 00:00:00 2001 From: plenarius Date: Mon, 18 Dec 2017 19:33:37 -0500 Subject: [PATCH 19/27] List imports alphabetically --- iota/commands/core/find_transactions.py | 2 +- iota/commands/core/get_balances.py | 2 +- test/filters_test.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index 9addaf3..7bc6d91 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -7,7 +7,7 @@ from iota import Address, Tag, TransactionHash from iota.commands import FilterCommand, RequestFilter, ResponseFilter -from iota.filters import Trytes, AddressNoChecksum +from iota.filters import AddressNoChecksum, Trytes __all__ = [ 'FindTransactionsCommand', diff --git a/iota/commands/core/get_balances.py b/iota/commands/core/get_balances.py index 5c96f8f..36a0812 100644 --- a/iota/commands/core/get_balances.py +++ b/iota/commands/core/get_balances.py @@ -6,7 +6,7 @@ from iota import Address from iota.commands import FilterCommand, RequestFilter, ResponseFilter -from iota.filters import Trytes, AddressNoChecksum +from iota.filters import AddressNoChecksum, Trytes __all__ = [ 'GetBalancesCommand', diff --git a/test/filters_test.py b/test/filters_test.py index b0d1fd7..8b24af6 100644 --- a/test/filters_test.py +++ b/test/filters_test.py @@ -6,7 +6,7 @@ from filters.test import BaseFilterTestCase from iota import Address, TryteString, TransactionHash -from iota.filters import GeneratedAddress, NodeUri, Trytes, AddressNoChecksum +from iota.filters import AddressNoChecksum, GeneratedAddress, NodeUri, Trytes class GeneratedAddressTestCase(BaseFilterTestCase): From f45923f3cc4cacaa545739456ae1acd8e0c726bf Mon Sep 17 00:00:00 2001 From: plenarius Date: Tue, 19 Dec 2017 15:31:54 -0500 Subject: [PATCH 20/27] Fix some of the comments to be specific to this special subclass, not Tryte --- iota/filters.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/iota/filters.py b/iota/filters.py index ac41d55..ed579c9 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -147,7 +147,7 @@ def _apply(self, value): class AddressNoChecksum(Trytes): """ - Validates a sequence as a sequence of trytes. + Validates a sequence as an Address then chops off the checksum if it exists """ def _apply(self, value): @@ -163,6 +163,7 @@ def _apply(self, value): # If the incoming value already is an Address then we just make sure it has no checksum if isinstance(value, Address): + # Make sure it has a valid checksum if one exists if value.checksum and not value.is_checksum_valid(): return self._invalid_value( value = value, @@ -182,10 +183,11 @@ def _apply(self, value): except ValueError: return self._invalid_value(value, self.CODE_NOT_TRYTES, exc_info=True) - # Now coerce to the expected type and verify that there are no + # Now coerce to an Address and verify that there are no # type-specific errors. try: addy = Address(value) + # Make sure it has a valid checksum if one exists if addy.checksum and not addy.is_checksum_valid(): return self._invalid_value( value = addy, @@ -206,4 +208,4 @@ def _apply(self, value): template_vars = { 'result_type': self.result_type.__name__, }, - ) \ No newline at end of file + ) From 06770cd17667a51c13a16a89dc0a7e73d8a217ab Mon Sep 17 00:00:00 2001 From: plenarius Date: Tue, 19 Dec 2017 19:50:01 -0500 Subject: [PATCH 21/27] Nicer test function names for easier reading. --- test/filters_test.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/filters_test.py b/test/filters_test.py index 8b24af6..fe1a625 100644 --- a/test/filters_test.py +++ b/test/filters_test.py @@ -224,21 +224,21 @@ def setUp(self): self.address_with_checksum = Address(self.tryte1 + self.checksum) self.address_with_bad_checksum = Address(self.tryte1 + b'DEADBEEF9') - def test_pass_checksumless_addy(self): + def test_pass_no_checksum_addy(self): """ Incoming value is tryte in address form or Address object """ self.assertFilterPasses(self.tryte1) self.assertFilterPasses(self.address) - def test_pass_withchecksum_addy(self): + def test_pass_with_checksum_addy(self): """ After passing through the filter an address with a checksum should return the address without """ self.assertFilterPasses(self.address_with_checksum, self.address) - def test_fail_badchecksum_addy(self): + def test_fail_with_bad_checksum_addy(self): """ If they've got a bad checksum in their address we should probably tell them so they don't wonder why something works in one place and not another From 6acf0a9d8e22263f421fed508fc134585c4e44f4 Mon Sep 17 00:00:00 2001 From: plenarius Date: Tue, 19 Dec 2017 20:02:12 -0500 Subject: [PATCH 22/27] * Address need not be specified just hardcode it * Run through Trytes _apply first to do most of the work * Use context for more details in exception --- iota/commands/core/find_transactions.py | 2 +- iota/commands/core/get_balances.py | 2 +- iota/filters.py | 83 +++++++++---------------- 3 files changed, 32 insertions(+), 55 deletions(-) diff --git a/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index 7bc6d91..0071586 100644 --- a/iota/commands/core/find_transactions.py +++ b/iota/commands/core/find_transactions.py @@ -43,7 +43,7 @@ def __init__(self): f.Array | f.FilterRepeater( f.Required - | AddressNoChecksum(result_type=Address) + | AddressNoChecksum() | f.Unicode(encoding='ascii', normalize=False) ) ), diff --git a/iota/commands/core/get_balances.py b/iota/commands/core/get_balances.py index 36a0812..a109b58 100644 --- a/iota/commands/core/get_balances.py +++ b/iota/commands/core/get_balances.py @@ -37,7 +37,7 @@ def __init__(self): | f.Array | f.FilterRepeater( f.Required - | AddressNoChecksum(result_type=Address) + | AddressNoChecksum() | f.Unicode(encoding='ascii', normalize=False) ) ), diff --git a/iota/filters.py b/iota/filters.py index ed579c9..3b02b45 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -68,12 +68,10 @@ class Trytes(f.BaseFilter): """ Validates a sequence as a sequence of trytes. """ - ADDRESS_BAD_CHECKSUM = 'address_bad_checksum' CODE_NOT_TRYTES = 'not_trytes' CODE_WRONG_FORMAT = 'wrong_format' templates = { - ADDRESS_BAD_CHECKSUM: 'The checksum for this address is invalid.', CODE_NOT_TRYTES: 'This value is not a valid tryte sequence.', CODE_WRONG_FORMAT: 'This value is not a valid {result_type}.', } @@ -149,63 +147,42 @@ class AddressNoChecksum(Trytes): """ Validates a sequence as an Address then chops off the checksum if it exists """ + ADDRESS_BAD_CHECKSUM = 'address_bad_checksum' - 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 is an Address then we just make sure it has no checksum - if isinstance(value, Address): - # Make sure it has a valid checksum if one exists - if value.checksum and not value.is_checksum_valid(): - return self._invalid_value( - value = value, - reason = self.ADDRESS_BAD_CHECKSUM, - exc_info = True, - - template_vars = { - 'result_type': self.ADDRESS_BAD_CHECKSUM, - }, - ) - return Address(value.address) - - # 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) + templates = { + ADDRESS_BAD_CHECKSUM: 'Checksum is {supplied_checksum}, should be {expected_checksum}?', + } - # Now coerce to an Address and verify that there are no - # type-specific errors. - try: - addy = Address(value) - # Make sure it has a valid checksum if one exists - if addy.checksum and not addy.is_checksum_valid(): - return self._invalid_value( - value = addy, - reason = self.ADDRESS_BAD_CHECKSUM, - exc_info = True, + def __init__(self): + # type: (type) -> None + super(AddressNoChecksum, self).__init__(result_type=Address) - template_vars = { - 'result_type': self.ADDRESS_BAD_CHECKSUM, - }, - ) - return Address(addy.address) - except ValueError: + def _apply(self, value): + super(AddressNoChecksum, self)._apply(value) + + if not isinstance(value, Address): + try: + value = Address(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__, + }, + ) + + if value.checksum and not value.is_checksum_valid(): return self._invalid_value( value = value, - reason = self.CODE_WRONG_FORMAT, + reason = self.ADDRESS_BAD_CHECKSUM, exc_info = True, - template_vars = { - 'result_type': self.result_type.__name__, + context = { + 'supplied_checksum': value.checksum, + 'expected_checksum': value.with_valid_checksum().checksum, }, ) + return Address(value.address) From 7cc797ba6856a1112772ecb4e5e2137abc35714c Mon Sep 17 00:00:00 2001 From: plenarius Date: Tue, 19 Dec 2017 20:06:29 -0500 Subject: [PATCH 23/27] Fix some accidental whitespace additions from testing. --- iota/filters.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/iota/filters.py b/iota/filters.py index 3b02b45..0ef4ec8 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -9,6 +9,7 @@ from iota import Address, TryteString, TrytesCompatible + class GeneratedAddress(f.BaseFilter): """ Validates an incoming value as a generated :py:class:`Address` (must @@ -68,8 +69,8 @@ class Trytes(f.BaseFilter): """ Validates a sequence as a sequence of trytes. """ - CODE_NOT_TRYTES = 'not_trytes' - CODE_WRONG_FORMAT = 'wrong_format' + CODE_NOT_TRYTES = 'not_trytes' + CODE_WRONG_FORMAT = 'wrong_format' templates = { CODE_NOT_TRYTES: 'This value is not a valid tryte sequence.', From b5263dfab1a1da606154879f3cb4cd5ded7b1d8c Mon Sep 17 00:00:00 2001 From: plenarius Date: Tue, 19 Dec 2017 23:11:26 -0500 Subject: [PATCH 24/27] Thanks to the test cases I understand a bit more how the super and _apply work. All tests now pass. --- iota/filters.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/iota/filters.py b/iota/filters.py index 0ef4ec8..00d7375 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -161,20 +161,14 @@ def __init__(self): 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): - try: - value = Address(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__, - }, - ) + 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, From 8e0a18a7463b3028f00b2be8c8f638848d3531c5 Mon Sep 17 00:00:00 2001 From: scottbelden Date: Thu, 21 Dec 2017 16:16:43 -0500 Subject: [PATCH 25/27] add check_consistency api --- iota/api.py | 28 +++ iota/commands/core/__init__.py | 1 + iota/commands/core/check_consistency.py | 38 +++ test/commands/core/check_consistency_test.py | 247 +++++++++++++++++++ 4 files changed, 314 insertions(+) create mode 100644 iota/commands/core/check_consistency.py create mode 100644 test/commands/core/check_consistency_test.py diff --git a/iota/api.py b/iota/api.py index 251bfa4..3389400 100644 --- a/iota/api.py +++ b/iota/api.py @@ -195,6 +195,34 @@ def broadcast_transactions(self, trytes): """ 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, diff --git a/iota/commands/core/__init__.py b/iota/commands/core/__init__.py index 501d520..73e27b5 100644 --- a/iota/commands/core/__init__.py +++ b/iota/commands/core/__init__.py @@ -13,6 +13,7 @@ from .add_neighbors import * from .attach_to_tangle import * from .broadcast_transactions import * +from .check_consistency import * from .find_transactions import * from .get_balances import * from .get_inclusion_states import * diff --git a/iota/commands/core/check_consistency.py b/iota/commands/core/check_consistency.py new file mode 100644 index 0000000..09c3f41 --- /dev/null +++ b/iota/commands/core/check_consistency.py @@ -0,0 +1,38 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +import filters as f +from iota import Transaction, TransactionHash +from iota.commands import FilterCommand, RequestFilter +from iota.filters import Trytes + +__all__ = [ + 'CheckConsistencyCommand', +] + + +class CheckConsistencyCommand(FilterCommand): + """ + Executes ``checkConsistency`` extended API command. + + See :py:meth:`iota.api.Iota.check_consistency` for more info. + """ + command = 'checkConsistency' + + def get_request_filter(self): + return CheckConsistencyRequestFilter() + + 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)) + ), + }) diff --git a/test/commands/core/check_consistency_test.py b/test/commands/core/check_consistency_test.py new file mode 100644 index 0000000..3b684b5 --- /dev/null +++ b/test/commands/core/check_consistency_test.py @@ -0,0 +1,247 @@ +# coding=utf-8 +from __future__ import absolute_import, division, print_function, \ + unicode_literals + +from unittest import TestCase + +import filters as f +from filters.test import BaseFilterTestCase + +from iota import Iota, TransactionHash, TryteString +from iota.adapter import MockAdapter +from iota.commands.core.check_consistency import CheckConsistencyCommand +from iota.filters import Trytes + + +class CheckConsistencyRequestFilterTestCase(BaseFilterTestCase): + filter_type = CheckConsistencyCommand(MockAdapter()).get_request_filter + skip_value_check = True + + # noinspection SpellCheckingInspection + def setUp(self): + super(CheckConsistencyRequestFilterTestCase, self).setUp() + + self.hash1 = ( + 'TESTVALUE9DONTUSEINPRODUCTION99999DXSCAD' + 'YBVDCTTBLHFYQATFZPYPCBG9FOUKIGMYIGLHM9NEZ' + ) + + self.hash2 = ( + 'TESTVALUE9DONTUSEINPRODUCTION99999EMFYSM' + 'HWODIAPUTTFDLQRLYIDAUIPJXXEXZZSBVKZEBWGAN' + ) + + def test_pass_happy_path(self): + """ + Request is valid. + """ + request = { + # Raw trytes are extracted to match the IRI's JSON protocol. + 'tails': [self.hash1, self.hash2], + } + + filter_ = self._filter(request) + + self.assertFilterPasses(filter_) + self.assertDictEqual(filter_.cleaned_data, request) + + def test_pass_compatible_types(self): + """ + Request contains values that can be converted to the expected + types. + """ + filter_ = self._filter({ + 'tails': [ + # Any TrytesCompatible value can be used here. + TransactionHash(self.hash1), + bytearray(self.hash2.encode('ascii')), + ], + }) + + self.assertFilterPasses(filter_) + self.assertDictEqual( + filter_.cleaned_data, + + { + # Raw trytes are extracted to match the IRI's JSON protocol. + 'tails': [self.hash1, self.hash2], + }, + ) + + def test_fail_empty(self): + """ + Request is empty. + """ + self.assertFilterErrors( + {}, + + { + 'tails': [f.FilterMapper.CODE_MISSING_KEY], + }, + ) + + def test_fail_unexpected_parameters(self): + """ + Request contains unexpected parameters. + """ + self.assertFilterErrors( + { + 'tails': [TransactionHash(self.hash1)], + 'foo': 'bar', + }, + + { + 'foo': [f.FilterMapper.CODE_EXTRA_KEY], + }, + ) + + def test_fail_tails_null(self): + """ + ``tails`` is null. + """ + self.assertFilterErrors( + { + 'tails': None, + }, + + { + 'tails': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_tails_wrong_type(self): + """ + ``tails`` is not an array. + """ + self.assertFilterErrors( + { + # It's gotta be an array, even if there's only one hash. + 'tails': TransactionHash(self.hash1), + }, + + { + 'tails': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_tails_empty(self): + """ + ``tails`` is an array, but it is empty. + """ + self.assertFilterErrors( + { + 'tails': [], + }, + + { + 'tails': [f.Required.CODE_EMPTY], + }, + ) + + def test_fail_tails_contents_invalid(self): + """ + ``tails`` is a non-empty array, but it contains invalid values. + """ + self.assertFilterErrors( + { + 'tails': [ + b'', + True, + None, + b'not valid trytes', + + # This is actually valid; I just added it to make sure the + # filter isn't cheating! + TryteString(self.hash1), + + 2130706433, + b'9' * 82, + ], + }, + + { + 'tails.0': [f.Required.CODE_EMPTY], + 'tails.1': [f.Type.CODE_WRONG_TYPE], + 'tails.2': [f.Required.CODE_EMPTY], + 'tails.3': [Trytes.CODE_NOT_TRYTES], + 'tails.5': [f.Type.CODE_WRONG_TYPE], + 'tails.6': [Trytes.CODE_WRONG_FORMAT], + }, + ) + + +class CheckConsistencyCommandTestCase(TestCase): + # noinspection SpellCheckingInspection + def setUp(self): + super(CheckConsistencyCommandTestCase, self).setUp() + + self.adapter = MockAdapter() + self.command = CheckConsistencyCommand(self.adapter) + + # Define some tryte sequences that we can re-use across tests. + self.milestone =\ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999W9KDIH' + b'BALAYAFCADIDU9HCXDKIXEYDNFRAKHN9IEIDZFWGJ' + ) + + self.hash1 =\ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999TBPDM9' + b'ADFAWCKCSFUALFGETFIFG9UHIEFE9AYESEHDUBDDF' + ) + + self.hash2 =\ + TransactionHash( + b'TESTVALUE9DONTUSEINPRODUCTION99999CIGCCF' + b'KIUFZF9EP9YEYGQAIEXDTEAAUGAEWBBASHYCWBHDX' + ) + + def test_wireup(self): + """ + Verify that the command is wired up correctly. + """ + self.assertIsInstance( + Iota(self.adapter).checkConsistency, + CheckConsistencyCommand, + ) + + def test_happy_path(self): + """ + Successfully checking consistency. + """ + + self.adapter.seed_response('checkConsistency', { + 'state': True, + }) + + response = self.command(tails=[self.hash1, self.hash2]) + + self.assertDictEqual( + response, + + { + 'state': True, + } + ) + + def test_info_with_false_state(self): + """ + `info` field exists when `state` is False. + """ + + self.adapter.seed_response('checkConsistency', { + 'state': False, + 'info': 'Additional information', + }) + + response = self.command(tails=[self.hash1, self.hash2]) + + self.assertDictEqual( + response, + + { + 'state': False, + 'info': 'Additional information', + } + ) From c9990d8b8f20bcd68652d256a091d00fae43155f Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 27 Dec 2017 07:56:09 +1300 Subject: [PATCH 26/27] Bumping version number. --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 20cfe4e..998db43 100644 --- a/setup.py +++ b/setup.py @@ -9,6 +9,7 @@ import setuptools from setuptools import find_packages, setup + from distutils.version import LooseVersion if LooseVersion(setuptools.__version__) < LooseVersion('20.5'): import sys @@ -27,7 +28,7 @@ name = 'PyOTA', description = 'IOTA API library for Python', url = 'https://github.com/iotaledger/iota.lib.py', - version = '2.0.2', + version = '2.0.3', long_description = long_description, From f4356bcc5e85105187a497cab9a7a044d9d17537 Mon Sep 17 00:00:00 2001 From: Phoenix Zerin Date: Wed, 27 Dec 2017 07:56:27 +1300 Subject: [PATCH 27/27] Added some documentation. --- setup.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/setup.py b/setup.py index 998db43..fffecca 100644 --- a/setup.py +++ b/setup.py @@ -10,11 +10,16 @@ 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') + ## # Load long description for PyPi. with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader