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/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/addresses.rst b/docs/addresses.rst new file mode 100644 index 0000000..6571e7f --- /dev/null +++ b/docs/addresses.rst @@ -0,0 +1,140 @@ +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. + +.. note:: + + PyOTA's crytpo functionality is currently very slow; on average it takes + 8-10 seconds to generate each address. + + These performance issues will be fixed in a future version of the library; + please bear with us! + + In the meantime, if you are using Python 3, you can install a C extension + that boosts PyOTA's performance significantly (speedups of 60x are common!). + + To install the extension, run ``pip install pyota[ccurl]``. + + **Important:** The extension is not yet compatible with Python 2. + + 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: + +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. 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 625e1ba..ba7b24b 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -4,10 +4,9 @@ getting_started types - -.. note:: - **🚧 PyOTA documentation is still under construction. 🚧** - - Follow https://github.com/iotaledger/iota.lib.py/issues/78 for updates. + adapters + addresses + api + multisig .. include:: ../README.rst diff --git a/docs/multisig.rst b/docs/multisig.rst new file mode 100644 index 0000000..d245076 --- /dev/null +++ b/docs/multisig.rst @@ -0,0 +1,213 @@ +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. + +.. 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 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 + +.. 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 +------------------ + +.. 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 +--------------- + +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:: + + 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 +------- + +Full code `example`_. + +.. note:: + + 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. + +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 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. diff --git a/examples/address_generator.py b/examples/address_generator.py index f48328a..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): - # type: (Text, int, Optional[int], bool) -> None +def main(uri, index, count, security, checksum): + # type: (Text, int, Optional[int], Optional[int], bool) -> None seed = get_seed() # Create the API instance. @@ -34,7 +35,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 +112,20 @@ def output_seed(seed): 'If not specified, the first unused address will be returned.' ) + parser.add_argument( + '--security', + type = int, + default = AddressGenerator.DEFAULT_SECURITY_LEVEL, + help = 'Security level to be used for the private key / address. ' + 'Can be 1, 2 or 3', + ) + + parser.add_argument( + '--with-checksum', + action = 'store_true', + default = False, + dest = 'checksum', + help = 'List the address with the checksum.', + ) + main(**vars(parser.parse_args(argv[1:]))) diff --git a/iota/api.py b/iota/api.py index 251bfa4..71c4414 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, @@ -433,7 +461,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. @@ -470,7 +498,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. } """ @@ -611,8 +639,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, bool) -> dict """ Generates one or more new addresses from the seed. @@ -636,6 +665,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 +684,7 @@ def get_new_addresses( count = count, index = index, securityLevel = security_level, + checksum = checksum, seed = self.seed, ) diff --git a/iota/bin/repl.py b/iota/bin/repl.py index 72cbfae..ac73da8 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: 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/iota/commands/core/find_transactions.py b/iota/commands/core/find_transactions.py index ab5ba5a..0071586 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 AddressNoChecksum, Trytes __all__ = [ 'FindTransactionsCommand', @@ -43,7 +43,7 @@ def __init__(self): f.Array | f.FilterRepeater( f.Required - | Trytes(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 54bf824..a109b58 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 AddressNoChecksum, Trytes __all__ = [ 'GetBalancesCommand', @@ -37,7 +37,7 @@ def __init__(self): | f.Array | f.FilterRepeater( f.Required - | Trytes(result_type=Address) + | AddressNoChecksum() | f.Unicode(encoding='ascii', normalize=False) ) ), diff --git a/iota/commands/extended/get_new_addresses.py b/iota/commands/extended/get_new_addresses.py index b14c9a8..7acf1a8 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 @@ -33,28 +33,33 @@ def get_response_filter(self): pass def _execute(self, request): + checksum = request['checksum'] # type: bool count = request['count'] # type: Optional[int] index = request['index'] # type: int security_level = request['securityLevel'] # type: int seed = request['seed'] # type: Seed return { - 'addresses': self._find_addresses(seed, index, count, security_level), + '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, bool) -> List[Address] """ Find addresses matching the command parameters. """ - # type: (Seed, int, Optional[int]) -> List[Address] - generator = AddressGenerator(seed, security_level) + 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]) + # 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] @@ -75,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) @@ -88,6 +95,7 @@ def __init__(self): }, allow_missing_keys = { + 'checksum', 'count', 'index', 'securityLevel', 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/iota/filters.py b/iota/filters.py index e8a0df7..00d7375 100644 --- a/iota/filters.py +++ b/iota/filters.py @@ -142,3 +142,42 @@ def _apply(self, value): 'result_type': self.result_type.__name__, }, ) + + +class AddressNoChecksum(Trytes): + """ + Validates a sequence as an Address then chops off the checksum if it exists + """ + ADDRESS_BAD_CHECKSUM = 'address_bad_checksum' + + templates = { + ADDRESS_BAD_CHECKSUM: 'Checksum is {supplied_checksum}, should be {expected_checksum}?', + } + + def __init__(self): + # type: (type) -> None + super(AddressNoChecksum, self).__init__(result_type=Address) + + def _apply(self, value): + super(AddressNoChecksum, self)._apply(value) + + if self._has_errors: + return None + + # Possible it's still just a TryteString + if not isinstance(value, Address): + value = Address(value) + + # Bail out if we have a bad checksum + if value.checksum and not value.is_checksum_valid(): + return self._invalid_value( + value = value, + reason = self.ADDRESS_BAD_CHECKSUM, + exc_info = True, + + context = { + 'supplied_checksum': value.checksum, + 'expected_checksum': value.with_valid_checksum().checksum, + }, + ) + return Address(value.address) diff --git a/iota/types.py b/iota/types.py index 9671f3f..2510a98 100644 --- a/iota/types.py +++ b/iota/types.py @@ -77,9 +77,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. @@ -458,9 +458,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 diff --git a/setup.py b/setup.py index 22a18c4..fffecca 100644 --- a/setup.py +++ b/setup.py @@ -9,11 +9,17 @@ import setuptools from setuptools import find_packages, setup + +## +# Because of the way PyOTA declares its dependencies, it requires a +# more recent version of setuptools. +# https://www.python.org/dev/peps/pep-0508/#environment-markers from distutils.version import LooseVersion if LooseVersion(setuptools.__version__) < LooseVersion('20.5'): import sys sys.exit('Installation failed: Upgrade setuptools to version 20.5 or later') + ## # Load long description for PyPi. with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader @@ -27,7 +33,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, @@ -60,6 +66,7 @@ extras_require = { 'ccurl': ['pyota-ccurl'], + 'docs-builder': ['sphinx', 'sphinx_rtd_theme'], }, test_suite = 'test', 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', + } + ) diff --git a/test/commands/extended/get_new_addresses_test.py b/test/commands/extended/get_new_addresses_test.py index ea1a120..735aa90 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( + checksum = True, + count = 1, + index = 0, + seed = self.seed, + ) + + self.assertDictEqual( + response, + {'addresses': [self.addy_1_checksum]}, + ) 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', + ), + ) diff --git a/test/filters_test.py b/test/filters_test.py index 41dc817..fe1a625 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 AddressNoChecksum, GeneratedAddress, NodeUri, Trytes 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_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_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_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 + """ + self.assertFilterErrors( + self.address_with_bad_checksum, + [AddressNoChecksum.ADDRESS_BAD_CHECKSUM])