diff --git a/README.md b/README.md new file mode 100644 index 0000000..6d19663 --- /dev/null +++ b/README.md @@ -0,0 +1,190 @@ +
+
+
+
+ About ◈ + Prerequisites ◈ + Installation ◈ + Getting started ◈ + Examples ◈ + Supporting the project ◈ + Joining the discussion +
+ +--- + +## About + +This is the **official** Python client library, which allows you to do the following: +* Create transactions +* Read transactions +* Sign transactions +* Generate addresses + +This is beta software, so there may be performance and stability issues. +Please report any issues in our [issue tracker](https://github.com/iotaledger/iota.py/issues/new). + +## Prerequisites + +To install the IOTA Python client library and its dependencies, you need Python version 3.7, 3.6, 3.5, or 2.7 installed on your device. + +## Installation + +To download the IOTA Python client library and its dependencies, do the following: + +```bash +pip install pyota +``` + +### Installing the optional C extension + +PyOTA has an optional C extension that improves the performance of its +cryptography features by an average of **60 times**. + +To install this extension, do the following: + +```bash +pip install pyota[ccurl] +``` + +### Installing the optional module for local proof of work + +To do proof of work on your local device without relying on a node, +you can install the [PyOTA-PoW](https://pypi.org/project/PyOTA-PoW/) extension module. + +To install this extension, use the following command:: + +```bash +pip install pyota[pow] +``` + +When you've installed this module, you can use it by passing the `local_pow=True` argument to your API instance. Doing so will redirect all `attach_to_tangle` +API calls to an interface function in the `pow` package. + +### Installing from source + +To install the library from the source code on GitHub, do the following: + +```bash +# Recommended, but not required +Create virtualenv +git clone https://github.com/iotaledger/iota.py.git +pip install -e . +``` + +## Getting started + +After you've [installing the library](#installation), you can connect to an IRI node to send transactions to it and interact with the ledger. +An extended guide can be found on our [documentation portal](https://docs.iota.org/docs/client-libraries/0.1/getting-started/python-quickstart), we strongly recommend you to go here for starting off. A quick starting tutorial is shown below. + +To connect to a local IRI node, you can do the following: + +```py +from iota import Iota + +# Create a new instance of the IOTA API object +# Specify which node to connect to +api = Iota(adapter = 'https://nodes.devnet.iota.org:443') + +# Call the `get_node_info()` method for information about the node and the Tangle +response = api.get_node_info() + +print(response) +``` + +## Examples + +We have a list of test cases in the [`examples` directory](https://github.com/iotaledger/iota.py/tree/master/examples) that you can use as a reference when developing apps with IOTA. + +Here's how you could send a zero-value transaction, using the library. For the guide, see the [documentation portal](https://docs.iota.org/docs/client-libraries/0.1/how-to-guides/python/send-your-first-bundle). + +```python +# You don't need a seed to send zero-value transactions +api = Iota('https://nodes.devnet.iota.org:443', testnet=True) + +# Define a message to send. +# This message must include only ASCII characters. +message = TryteString.from_unicode('Hello world') + +# Define an address. +# This does not need to belong to anyone or have IOTA tokens. +# It must only contain a maximum of 81 trytes +# or 90 trytes with a valid checksum +address = 'ZLGVEQ9JUZZWCZXLWVNTHBDX9G9KZTJP9VEERIIFHY9SIQKYBVAHIMLHXPQVE9IXFDDXNHQINXJDRPFDXNYVAPLZAW' + +# Define a zero-value transaction object +# that sends the message to the address +tx = ProposedTransaction( + address = Address(address), + message = message, + value = 0 +) + +# Create a bundle from the `ProposedTransaction` object +# and send the transaction to the node +result = api.send_transfer(transfers=[tx]) + +print('Bundle: ') + +print(result['bundle'].hash) +``` + +## Supporting the project + +If the IOTA Python client library has been useful to you and you feel like contributing, consider posting a [bug report](https://github.com/iotaledger/iota.py/issues/new-issue), feature request or a [pull request](https://github.com/iotaledger/iota.py/pulls/). +We have some [basic contribution guidelines](CONTRIBUTING.rst) to keep our code base stable and consistent. + +### Running test cases + +To run test, do the following: + +```bash +python setup.py test +``` + +PyOTA is also compatible with [tox](https://tox.readthedocs.io/), which will run the unit tests in different virtual environments (one for each supported version of Python). + +To run the unit tests, it is recommended that you use the `-p` argument. +This speeds up the tests by running them in parallel. + +Install PyOTA with the `test-runner` extra to set up the necessary +dependencies, and then you can run the tests with the `tox` command:: + +```bash +pip install -e .[test-runner] +tox -v -p all +``` + +### Building the auto-generated documentation + +The auto-generated documentation can be generated on your local device by doing the following: + +```bash +# Install extra dependencies (you only have to do this once) +pip install .[docs-builder] +cd docs +# Build the documentation:: +make html +``` + +## Joining the discussion + +If you want to get involved in the community, need help with getting setup, have any issues related with the library or just want to discuss blockchain, distributed ledgers, and IoT with other people, feel free to join our [Discord](https://discord.iota.org/). diff --git a/README.rst b/docs/README.rst similarity index 100% rename from README.rst rename to docs/README.rst diff --git a/docs/index.rst b/docs/index.rst index 5c654c0..2a6a0da 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -12,5 +12,6 @@ addresses multisig commands + tutorials -.. include:: ../README.rst +.. include:: README.rst diff --git a/docs/tutorials.rst b/docs/tutorials.rst new file mode 100644 index 0000000..b46f00e --- /dev/null +++ b/docs/tutorials.rst @@ -0,0 +1,726 @@ +Tutorials +========= +Are you new to IOTA in Python? Don't worry, we got you covered! With the +walkthrough examples of this section, you will be a master of PyOTA. + +In each section below, a code snippet will be shown and discussed in detail +to help you understand how to carry out specific tasks with PyOTA. + +The example scripts displayed here can also be found under ``examples/tutorials/`` +directory in the repository. Run them in a Python environment that has PyOTA +installed. See :ref:`Install PyOTA` for more info. + +If you feel that something is missing or not clear, please post your questions +and suggestions in the `PyOTA Bug Tracker`_. + +Let's get to it then! + +.. py:currentmodule:: iota + +1. Hello Node +------------- +In this example, you will learn how to: + +- **Import the** ``iota`` **package into your application.** +- **Instantiate an API object for communication with the IOTA network.** +- **Request information about the IOTA node you are connected to.** + + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/01_hello_node.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/01_hello_node.py + :lines: 1-3 + :lineno-start: 1 + +First things first, we need to import in our application the modules we intend +to use. PyOTA provide the ``iota`` package, therefore, whenever you need +something from the library, you need to import it from there. + +Notice, how we import the :py:class:`Iota` object, that defines a +so-called extended API object. We will use this to send and receive data from +the network. Read more about API objects at :ref:`PyOTA API Classes`. + +We also import the ``pprint`` method that prettifies the output before printing +it to the console. + +.. literalinclude:: ../examples/tutorials/01_hello_node.py + :lines: 5-6 + :lineno-start: 5 + +Next, we declare an API object. Since this object handles the communication, +we need to specify an IOTA node to connect to in the form of an URI. Note, that +the library will parse this string and will throw an exception if it is not +a valid one. + +.. literalinclude:: ../examples/tutorials/01_hello_node.py + :lines: 8-9 + :lineno-start: 8 + +Then we can call the :py:meth:`Iota.get_node_info` method of the API +object to get some basic info about the node. + +.. code-block:: + :lineno-start: 11 + + # Using pprint instead of print for a nicer looking result in the console + pprint(response) + +Finally, we print out the response. It is important to note, that all API +methods return a python dictionary. Refer to the method's documentation to +determine what exactly is in the response ``dict``. Here for example, +we could list the ``features`` of the node:: + + pprint(response['features']) + +2. Send Data +------------ +In this example, you will learn how to: + +- **Encode data to be stored on the Tangle.** +- **Generate a random IOTA address that doesn't belong to anyone.** +- **Create a zero-value transaction with custom payload.** +- **Send a transaction to the network.** + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/02_send_data.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/02_send_data.py + :lines: 1-5 + :lineno-start: 1 + +We have seen this part before. Note, that now we import more objects which we +will use to construct our transaction. + +Notice ``testnet=True`` in the argument list of the API instantiation. We +tell the API directly that we will use the devnet/testnet. By default, the API +is configured for the mainnet. + +.. literalinclude:: ../examples/tutorials/02_send_data.py + :lines: 7-8 + :lineno-start: 7 + +If you read :ref:`Basic Concepts` and :ref:`PyOTA Types`, it shouldn't be a +surprise to you that most things in IOTA are represented as trytes, that are +:py:class:`TryteString` in PyOTA. + +Here, we encode our message with :py:meth:`TryteString.from_unicode` into +trytes. + +.. literalinclude:: ../examples/tutorials/02_send_data.py + :lines: 10-11 + :lineno-start: 10 + +To put anything (transactions) on the Tangle, it needs to be associated with +an address. **Since we will be posting a zero-value transaction, nobody has to +own this address**; therefore we can use the :py:meth:`TryteString.random` (an +:py:class:`Address` is just a :py:class:`TryteString` with some additional +attributes and fixed length) method to generate one. + +.. literalinclude:: ../examples/tutorials/02_send_data.py + :lines: 13-14 + :lineno-start: 13 + +To tag our transaction, we might define a custom :py:class:`Tag` object. +Notice, that the ``b`` means we are passing a `bytestring`_ value instead of a +unicode string. This is so that PyOTA interprets our input as literal trytes, +rather than a unicode string that needs to be encoded into trytes. + +When passing a bytestring to a PyOTA class, each byte is interpreted as a tryte; +therefore we are restricted to the `tryte alphabet`_. + +.. literalinclude:: ../examples/tutorials/02_send_data.py + :lines: 16-22 + :lineno-start: 16 + +It's time to construct the transaction. According to :ref:`Transaction Types`, +PyOTA uses :py:class:`ProposedTransaction` to build transactions that are not +yet broadcast to the network. Oberve, that the ``value=0`` means this is +a zero-value transaction. + +.. literalinclude:: ../examples/tutorials/02_send_data.py + :lines: 24-25 + :lineno-start: 24 + +Next, we send the transfer to the node for tip selection, +proof-of-work calculation, broadcasting and storing. The API takes care of +all these tasks, and returns the resulting ``Bundle`` object. + +.. note:: + + :py:meth:`~Iota.send_transfer` takes a list of :py:class:`ProposedTransaction` + objects as its ``transfers`` argument. An IOTA transfer (bundle) usually + consists of multiple transactions linked together, however, in this simple + example, there is only one transaction in the bundle. Regardless, you need + to pass this sole transaction as a list of one transaction. + +.. literalinclude:: ../examples/tutorials/02_send_data.py + :lines: 27-28 + :lineno-start: 27 + +Finally, we print out the transaction's link on the Tangle Explorer. +Observe how we extract the transaction hash from the response ``dict``. We take +the first element of the bundle, as it is just a sequence of transactions, and +access its ``hash`` attribute. + +3. Fetch Data +------------- +In this example, you will learn how to: + +- **Fetch transaction objects from the Tangle based on a criteria.** +- **Decode messages from transactions.** + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/03_fetch_data.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/03_fetch_data.py + :lines: 1-5 + :lineno-start: 1 + +The usual part again, but we also import ``TrytesDecodeError`` from +``iota.codec``. We will use this to detect if the fetched trytes contain +encoded text. + +.. literalinclude:: ../examples/tutorials/03_fetch_data.py + :lines: 7-10 + :lineno-start: 7 + +We declare an IOTA address on the Tangle to fetch data from. You can replace +this address with your own from the previous example `2. Send Data`_, or just +run it as it is. + +.. literalinclude:: ../examples/tutorials/03_fetch_data.py + :lines: 12-14 + :lineno-start: 12 + +We use :py:meth:`~Iota.find_transaction_objects` extended API method to gather +the transactions that belong to our address. This method is also capable of +returning :py:class:`Transaction` objects for bundle hashes, tags or approving +transactions. Note that you can supply multiple of these, the method always +returns a ``dict`` with a list of transactions. + +.. note:: + + Remember, that the parameters need to be supplied as lists, even if + there is only one value. + +.. literalinclude:: ../examples/tutorials/03_fetch_data.py + :lines: 16-25 + :lineno-start: 16 + +Finally, we extract the data we are looking for from the transaction objects. +A :py:class:`Transaction` has several attributes, one of which is the +``signature_message_fragment``. This contains the payload message for zero-value +transactions, and the digital signature that authorizes spending for value +transactions. + +Since we are interested in data now, we decode its content (raw trytes) into +text. Notice, that we pass the ``errors='ignore'`` argument to the ``decode()`` +method to drop values we can't decode using ``utf-8``, or if the raw trytes +can't be decoded into legit bytes. A possible reason for the latter can be if +the attribute contains a signature rather than a message. + +4.a Generate Address +-------------------- + +In this example, you will learn how to: + +- **Generate a random seed.** +- **Generate an IOTA address that belongs to your seed.** +- **Acquire free devnet IOTA tokens that you can use to play around with.** + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/04a_gen_address.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/04a_gen_address.py + :lines: 1-7 + :lineno-start: 1 + +We start off by generating a random seed with the help of the library. You are +also free to use your own seed, just uncomment line 6 and put it there. + +If you choose to generate one, your seed is written to the console so that you +can save it for later. Be prepared to do so, because you will have to use it +in the following tutorials. + +.. literalinclude:: ../examples/tutorials/04a_gen_address.py + :lines: 9-14 + :lineno-start: 9 + +Notice, how we pass the ``seed`` argument to the API class's init method. +Whenever the API needs to work with addresses or private keys, it will derive +them from this seed. + +.. important:: + + Your seed never leaves the library and your computer. Treat your (mainnet) + seed like any other password for a financial service: safe. If your seed is + compromised, attackers can steal your funds. + +.. literalinclude:: ../examples/tutorials/04a_gen_address.py + :lines: 16-20 + :lineno-start: 16 + +To generate a new address, we call :py:meth:`~Iota.get_new_addresses` +extended API method. Without arguments, this will return a ``dict`` with the +first unused address starting from ``index`` 0. An unused address is address +that has no transactions referencing it on the Tangle and was never spent from. + +If we were to generate more addresses starting from a desired index, +we could specify the ``start`` and ``count`` parameters. Read more about how to +generate addresses in PyOTA at :ref:`Generating Addresses`. + +On line 20 we access the first element of the list of addresses in the response +dictionary. + +.. literalinclude:: ../examples/tutorials/04a_gen_address.py + :lines: 22-23 + :lineno-start: 22 + +Lastly, the address is printed to the console, so that you can copy it. +Visit https://faucet.devnet.iota.org/ and enter the address to receive free +devnet tokens of 1000i. + +You might need to wait 1-2 minutes until the sum arrives to you address. To +check your balance, go to `4.b Check Balance`_ or `4.c Get Account Data`_. + +4.b Check Balance +----------------- + +In this example, you will learn how to: + +- **Check the balance of a specific IOTA address.** + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/04b_check_balance.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/04b_check_balance.py + :lines: 1-8 + :lineno-start: 1 + +The first step to check the balance of an address is to actually have an +address. Exchange the sample address on line 5 with your generated address from +`4.a Generate Address`_. + +Since we don't need to generate an address, there is no need for a seed to be +employed in the API object. Note the ``time`` import, we need it for later. + +.. literalinclude:: ../examples/tutorials/04b_check_balance.py + :lines: 10-25 + :lineno-start: 10 + +Our script will poll the network for the address balance as long as the returned +balance is zero. Therefore, the address you declared as ``my_address`` should +have some balance. If you see the ``Zero balance found...`` message a couple of +times, head over to https://faucet.devnet.iota.org/ and load up your address. + +:py:meth:`~Iota.get_balances` returns the confirmed balance of the address. +You could supply multiple addresses at the same time and get their respective +balances in a single call. Don't forget, that the method returns a ``dict``. +More details about it can be found at :py:meth:`~Iota.get_balances`. + +4.c Get Account Data +-------------------- + +In this example, you will learn how to: + +- **Gather addresses, balance and bundles associated with your seed on the Tangle.** + +.. warning:: + + **Account** in the context of this example is not to be confused with the + `Account Module`_, that is a feature yet to be implemented in PyOTA. + + **Account** here simply means the addresses and funds that belong to your + seed. + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/04c_get_acc_data.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/04c_get_acc_data.py + :lines: 1-3 + :lineno-start: 1 + +We will need ``pprint`` for a prettified output of the response ``dict`` and +``time`` for polling until we find non-zero balance. + +.. literalinclude:: ../examples/tutorials/04c_get_acc_data.py + :lines: 5-13 + :lineno-start: 5 + +Copy your seed from `4.a Generate Address`_ onto line 6. The API will use your +seed to generate addresses and look for corresponding transactions on the +Tangle. + +.. literalinclude:: ../examples/tutorials/04c_get_acc_data.py + :lines: 15-30 + :lineno-start: 15 + +Just like in the previous example, we will poll for information until we find +a non-zero balance. :py:meth:`~Iota.get_account_data` without arguments +generates addresses from ``index`` 0 until it finds the first unused. Then, it +queries the node about bundles of those addresses and sums up their balance. + +.. note:: + + If you read :py:meth:`~Iota.get_account_data` documentation carefully, you + notice that you can gain control over which addresses are checked during + the call by specifying the ``start`` and ``stop`` index parameters. + + This can be useful when your addresses with funds do not follow each other + in the address namespace, or a snapshot removed transactions from the + Tangle. It is recommended that you keep a local database of your already + used address indices. + + Once implemented in PyOTA, `Account Module`_ will address the aforementioned + problems. + +The response ``dict`` contains the addresses, bundles and total balance of +your seed. + +5. Send Tokens +-------------- + +In this example, you will learn how to: + +- **Construct a value transfer with PyOTA.** +- **Send a value transfer to an arbitrary IOTA address.** +- **Analyze a bundle of transactions on the Tangle.** + +.. note:: + + As a prerequisite to this tutorial, you need to have completed + `4.a Generate Address`_, and have a seed that owns devnet tokens. + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/05_send_tokens.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/05_send_tokens.py + :lines: 1-11 + :lineno-start: 1 + +We are going to send a value transaction, that requires us to prove that we +own the address containg the funds to spend. Therefore, we need our seed from +which the address was generated. + +Put your seed from `4.a Generate Address`_ onto line 4. We pass this seed to +the API object, that will utilize it for signing the transfer. + +.. literalinclude:: ../examples/tutorials/05_send_tokens.py + :lines: 13-16 + :lineno-start: 13 + +In IOTA, funds move accross addresses, therefore we need to define a **receiver +address**. For testing value transfers, you should send the funds only to +addresses that you control; if you use a randomly-generated receiver address, +you won't be able to recover the funds afterward! +Re-run `4.a Generate Address`_ for a new seed and a new address, or just paste +a valid IOTA address that you own onto line 16. + +.. literalinclude:: ../examples/tutorials/05_send_tokens.py + :lines: 18-25 + :lineno-start: 18 + +We declare a :py:class:`ProposedTransaction` object like we did before, but +this time, with ``value=1`` parameter. The smallest value you can send is 1 +iota ("1i"), there is no way to break it into smaller chunks. It is a really small +value anyway. You can also attach a message to the transaction, for example a +little note to the beneficiary of the payment. + +.. literalinclude:: ../examples/tutorials/05_send_tokens.py + :lines: 27-29 + :lineno-start: 27 + +To actually send the transfer, all you need to do is call +:py:meth:`~Iota.send_transfer` extended API method. This method will take care +of: + +- Gathering ``inputs`` (addresses you own and have funds) to fund the 1i transfer. +- Generating a new ``change_address``, and automatically sending the remaining + funds (``balance of chosen inputs`` - 1i) from ``inputs`` to ``change_address``. + + .. warning:: + + This step is extremely important, as it prevents you from `spending twice + from the same address`_. + + When an address is used as an input, all tokens will be withdrawn. Part + of the tokens will be used to fund your transaction, the rest will be + transferred to ``change_address``. + +- Constructing the transfer bundle with necessary input and output transactions. +- Finalizing the bundle and signing the spending transactions. +- Doing proof-of-work for each transaction in the bundle and sending it to the + network. + +.. literalinclude:: ../examples/tutorials/05_send_tokens.py + :lines: 31-32 + :lineno-start: 31 + +Open the link and observe the bundle you have just sent to the Tangle. Probably +it will take a couple of seconds for the network to confirm it. + +What you see is a bundle with 4 transactions in total, 1 input and 3 outputs. +But why are there so many transactions? + +- There is one transaction that withdraws iotas, this has negative value. + To authorize this spending, a valid signature is included in the transaction's + ``signature_message_fragment`` field. The signature however is too long to + fit into one transaction, therefore the library appends a new, zero-value + transaction to the bundle that holds the second part of the signature. This + you see on the output side of the bundle. +- A 1i transaction to the receiver address spends part of the withdrawn amount. +- The rest is transfered to ``change_address`` in a new output transaction. + +Once the bundle is confirmed, try rerunning the script from +`4.c Get Account Data`_ with the same seed as in this tutorial. Your balance +should be decremented by 1i, and you should see a new address, which was +actually the ``change_address``. + +6. Store Encrypted Data +----------------------- + +In this example, you will learn how to: + +- **Convert Python data structures to JSON format.** +- **Encrypt data and include it in a zero-value transaction.** +- **Store the zero-value transaction with encrypted data on the Tangle.** + +.. warning:: + + We will use the ``simple-crypt`` external library for encryption/decryption. + Before proceeding to the tutorial, make sure you install it by running:: + + pip install simple-crypt + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/06_store_encrypted.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/06_store_encrypted.py + :lines: 1-18 + :lineno-start: 1 + +We will use the ``encrypt`` method to encipher the data, and ``b64encode`` for +representing it as ASCII characters. ``getpass`` will prompt the user for a +password, and the ``json`` library is used for JSON formatting. + +We will need an address to upload the data, therefore we need to supply the +seed to the ``Iota`` API instance. The address will be generated from this +seed. + +.. literalinclude:: ../examples/tutorials/06_store_encrypted.py + :lines: 20-26 + :lineno-start: 20 + +The data to be stored is considered confidential information, therefore we +can't just put it on the Tangle as plaintext so everyone can read it. Think of +what would happen if the world's most famous secret agent's identity was leaked +on the Tangle... + +.. literalinclude:: ../examples/tutorials/06_store_encrypted.py + :lines: 28-29 + :lineno-start: 28 + +Notice, that ``data`` is a Python ``dict`` object. As a common way of exchanging +data on the web, we would like to convert it to JSON format. The ``json.dumps()`` +method does exactly that, and the result is a JSON formatted plaintext. + +.. literalinclude:: ../examples/tutorials/06_store_encrypted.py + :lines: 31-40 + :lineno-start: 31 + +Next, we will encrypt this data with a secret password we obtain from the user. + +.. note:: + + When you run this example, please remember the password at least until the + next tutorial! + +The output of the ``encrypt`` method is a ``bytes`` object in Python3 and +contains many special characters. This is a problem, since we can only convert +ASCII characters from ``bytes`` directly into :py:class:`TryteString`. + +Therefore, we first encode our binary data into ASCII characters with `Base64`_ +encoding. + +.. literalinclude:: ../examples/tutorials/06_store_encrypted.py + :lines: 42-58 + :lineno-start: 42 + +Now, we are ready to construct the transfer. We convert the encrypted `Base64`_ +encoded data to trytes and assign it to the :py:class:`ProposedTransaction` +object's ``message`` argument. + +An address is also needed, so we generate one with the help of +:py:meth:`~Iota.get_new_addresses` extended API method. Feel free to choose the +index of the generated address, and don't forget, that the method returns a +``dict`` with a list of addresses, even if it contains only one. +For more detailed explanation on how addresses are generated in PyOTA, +refer to the :ref:`Generating Addresses` page. + +We also attach a custom :py:class:`Tag` to our :py:class:`ProposedTransaction`. +Note, that if our ``trytes_encrypted_data`` was longer than the maximum payload +of a transaction, the library would split it accross more transactions that +together form the transfer bundle. + +.. literalinclude:: ../examples/tutorials/06_store_encrypted.py + :lines: 60-66 + :lineno-start: 60 + +Finally, we use :py:meth:`Iota.send_transfer` to prepare the transfer and +send it to the network. + +Click on the link to check your transaction on the Tangle Explorer. + +The tail transaction (a tail transaction is the one with index 0 in the bundle) +hash is printed on the console, because you will need it in the next tutorial, +and anyway, it is a good practice to keep a reference to your transfers. + +In the next example, we will try to decode the confidential information from +the Tangle. + +7. Fetch Encrypted Data +----------------------- + +In this example, you will learn how to: + +- **Fetch bundles from the Tangle based on their tail transaction hashes.** +- **Extract messages from a bundle.** +- **Decrypt encrypted messages from a bundle.** + +.. warning:: + + We will use the ``simple-crypt`` external library for encryption/decryption. + Before proceeding to the tutorial, make sure you install it by running:: + + pip install simple-crypt + +Code +~~~~ +.. literalinclude:: ../examples/tutorials/07_fetch_encrypted.py + :linenos: + +Discussion +~~~~~~~~~~ +.. literalinclude:: ../examples/tutorials/07_fetch_encrypted.py + :lines: 1-14 + :lineno-start: 1 + +In contrast to `6. Store Encrypted Data`_ where we intended to encrypt data, in +this tutorial we will do the reverse, and decrypt data from the Tangle. +Therefore, we need the ``decrypt`` method from ``simplecrypt`` library and the +``b64decode`` method from ``base64`` library. + +Furthermore, ``getpass`` is needed to prompt the user for a decryption +password, and ``json`` for deserializing JSON formatted string into Python +object. + +.. literalinclude:: ../examples/tutorials/07_fetch_encrypted.py + :lines: 16-17 + :lineno-start: 16 + +To fetch transactions or bundles from the Tangle, a reference is required to +retreive them from the network. Transactions are identified by their +transaction hash, while a group of transaction (a bundle) by bundle hash. +Hashes ensure the integrity of the Tangle, since they contain verifiable +information about the content of the transfer objects. + +``input()`` asks the user to give the tail transaction hash of the bundle +that holds the encrypted messages. The tail transaction is the first in the +bundle with index 0. Copy and paste the tail transaction hash from the console +output of `6. Store Encrypted Data`_ when prompted. + +.. literalinclude:: ../examples/tutorials/07_fetch_encrypted.py + :lines: 19-21 + :lineno-start: 19 + +Next, we fetch the bundle from the Tangle with the help of the +:py:meth:`~Iota.get_bundles` extended API command. It takes a list of tail +transaction hashes and returns the bundles for each of them. The response +``dict`` contains a ``bundles`` key with the value being a list of bundles +in the same order as the input argument hashes. Also note, that the bundles +in the response are actual PyOTA :py:class:`Bundle` objects. + +To simplify the code, several operations are happening on line 21: + +- Calling :py:meth:`~Iota.get_bundles` that returns a ``dict``, +- accessing the ``'bundles'`` key in the ``dict``, +- and taking the first element of the the list of bundles in the value + associated with the key. + +.. literalinclude:: ../examples/tutorials/07_fetch_encrypted.py + :lines: 23-39 + :lineno-start: 23 + +The next step is to extract the content of the message fields of the +transactions in the bundle. We call :py:meth:`Bundle.get_messages` to carry +out this operation. The method returns a list of unicode strings, essentially +the ``signature_message_fragment`` fields of the transactions, decoded from +trytes into unicode characters. + +We then combine these message chunks into one stream of characters by using +``string.join()``. + +We know that at this stage that we can't make sense of our message, because it +is encrypted and encoded into `Base64`_. Let's peel that onion layer by layer: + +- On line 28, we decode the message into bytes with ``b64decode``. +- On line 31, we ask the user for thr decryption password (from the previous + tutorial). +- On line 36, we decrypt the bytes cipher with the password and decode the + result into a unicode string. +- Since we used JSON formatting in the previous tutorial, there is one + additional step to arrive at our original data. On line 39, we deserialize + the JSON string into a Python object, namely a ``dict``. + +.. literalinclude:: ../examples/tutorials/07_fetch_encrypted.py + :lines: 41-42 + :lineno-start: 41 + +If everything went according to plan and the user supplied the right password, +we should see our original data printed out to the console. + +Now you know how to use the Tangle for data storage while keeping privacy. +When you need more granular access control on how and when one could read +data from the Tangle, consider using `Masked Authenticated Messaging`_ (MAM). + +.. _PyOTA Bug Tracker: https://github.com/iotaledger/iota.py/issues +.. _bytestring: https://docs.python.org/3/library/stdtypes.html#bytes +.. _tryte alphabet: https://docs.iota.org/docs/getting-started/0.1/introduction/ternary#tryte-encoding +.. _Tangle Explorer: https://utils.iota.org +.. _Account Module: https://docs.iota.org/docs/client-libraries/0.1/account-module/introduction/overview +.. _spending twice from the same address: https://docs.iota.org/docs/getting-started/0.1/clients/addresses#spent-addresses +.. _Base64: https://en.wikipedia.org/wiki/Base64 +.. _Masked Authenticated Messaging: https://docs.iota.org/docs/client-libraries/0.1/mam/introduction/overview?q=masked%20auth&highlights=author;authent \ No newline at end of file diff --git a/docs/types.rst b/docs/types.rst index b1780b1..0b24971 100644 --- a/docs/types.rst +++ b/docs/types.rst @@ -129,6 +129,13 @@ converted; garbage in, garbage out! ^^^^^^^^^^^^ .. automethod:: TryteString.as_trits +Generation +~~~~~~~~~~ + +**random** +^^^^^^^^^^ +.. automethod:: TryteString.random + Seed ---- .. autoclass:: Seed diff --git a/examples/tutorials/01_hello_node.py b/examples/tutorials/01_hello_node.py new file mode 100644 index 0000000..456b088 --- /dev/null +++ b/examples/tutorials/01_hello_node.py @@ -0,0 +1,12 @@ +# Import neccessary modules +from iota import Iota +from pprint import pprint + +# Declare an API object +api = Iota('https://nodes.devnet.iota.org:443') + +# Request information about the node +response = api.get_node_info() + +# Using pprint instead of print for a nicer looking result in the console +pprint(response) \ No newline at end of file diff --git a/examples/tutorials/02_send_data.py b/examples/tutorials/02_send_data.py new file mode 100644 index 0000000..5732a5a --- /dev/null +++ b/examples/tutorials/02_send_data.py @@ -0,0 +1,28 @@ +from iota import Iota, TryteString, Address, Tag, ProposedTransaction +from pprint import pprint + +# Declare an API object +api = Iota('https://nodes.devnet.iota.org:443', testnet=True) + +# Prepare custom data +my_data = TryteString.from_unicode('Hello from the Tangle!') + +# Generate a random address that doesn't have to belong to anyone +my_address = Address.random() + +# Tag is optional here +my_tag = Tag(b'MY9FIRST9TAG') + +# Prepare a transaction object +tx = ProposedTransaction( + address=my_address, + value=0, + tag=my_tag, + message=my_data +) + +# Send the transaction to the network +response = api.send_transfer([tx]) + +pprint('Check your transaction on the Tangle!') +pprint('https://utils.iota.org/transaction/%s/devnet' % response['bundle'][0].hash) \ No newline at end of file diff --git a/examples/tutorials/03_fetch_data.py b/examples/tutorials/03_fetch_data.py new file mode 100644 index 0000000..66ab768 --- /dev/null +++ b/examples/tutorials/03_fetch_data.py @@ -0,0 +1,25 @@ +from iota import Iota, Address +from iota.codecs import TrytesDecodeError + +# Declare an API object +api = Iota('https://nodes.devnet.iota.org:443', testnet=True) + +# Address to fetch data from +# Replace with your random generated address from Tutorial 2. to fetch the data +# that you uploaded. +addy = Address(b'WWO9DRAUDDSDSTTUPKJRNPSYLWAVQBBXISLKLTNDPVKOPMUERDUELLUPHNT9L9YWBDKOLYVWRAFRKIBLP') + +print('Fetching data from the Tangle...') +# Fetch the transaction objects of the address from the Tangle +response = api.find_transaction_objects(addresses=[addy]) + +if not response['transactions']: + print('Couldn\'t find data for the given address.') +else: + print('Found:') + # Iterate over the fetched transaction objects + for tx in response['transactions']: + # data is in the signature_message_fragment attribute as trytes, we need + # to decode it into a unicode string + data = tx.signature_message_fragment.decode(errors='ignore') + print(data) \ No newline at end of file diff --git a/examples/tutorials/04a_gen_address.py b/examples/tutorials/04a_gen_address.py new file mode 100644 index 0000000..0559c0c --- /dev/null +++ b/examples/tutorials/04a_gen_address.py @@ -0,0 +1,23 @@ +from iota import Iota, Seed + +# Generate a random seed, or use one you already have (for the devnet) +print('Generating a random seed...') +my_seed = Seed.random() +# my_seed = Seed(b'MYCUSTOMSEED') +print('Your seed is: ' + str(my_seed)) + +# Declare an API object +api = Iota( + adapter='https://nodes.devnet.iota.org:443', + seed=my_seed, + testnet=True, +) + +print('Generating the first unused address...') +# Generate the first unused address from the seed +response = api.get_new_addresses() + +addy = response['addresses'][0] + +print('Your new address is: ' + str(addy)) +print('Go to https://faucet.devnet.iota.org/ and enter you address to receive free devnet tokens.') \ No newline at end of file diff --git a/examples/tutorials/04b_check_balance.py b/examples/tutorials/04b_check_balance.py new file mode 100644 index 0000000..9f7353e --- /dev/null +++ b/examples/tutorials/04b_check_balance.py @@ -0,0 +1,25 @@ +from iota import Iota, Address +import time + +# Put your address from Tutorial 4.a here +my_address = Address(b'YOURADDRESSFROMTHEPREVIOUSTUTORIAL') + +# Declare an API object +api = Iota(adapter='https://nodes.devnet.iota.org:443', testnet=True) + +# Script actually runs until you load up your address +success = False + +while not success: + print('Checking balance on the Tangle for a specific address...') + # API method to check balance + response = api.get_balances(addresses=[my_address]) + + # response['balances'] is a list! + if response['balances'][0]: + print('Found the following information for address ' + str(my_address) + ':') + print('Balance: ' + str(response['balances'][0]) + 'i') + success = True + else: + print('Zero balance found, retrying in 30 seconds...') + time.sleep(30) \ No newline at end of file diff --git a/examples/tutorials/04c_get_acc_data.py b/examples/tutorials/04c_get_acc_data.py new file mode 100644 index 0000000..4061d2e --- /dev/null +++ b/examples/tutorials/04c_get_acc_data.py @@ -0,0 +1,30 @@ +from iota import Iota, Seed +from pprint import pprint +import time + +# Put your seed from Tutorial 4.a here +my_seed = Seed(b'YOURSEEDFROMTHEPREVIOUSTUTORIAL99999999999999999999999999999999999999999999999999') + +# Declare an API object +api = Iota( + adapter='https://nodes.devnet.iota.org:443', + seed=my_seed, + testnet=True +) + +# Script actually runs until it finds balance +success = False + +while not success: + print('Checking account information on the Tangle...') + # Gather addresses, balance and bundles + response = api.get_account_data() + + # response['balance'] is an integer! + if response['balance']: + print('Found the following information based on your seed:') + pprint(response) + success = True + else: + print('Zero balance found, retrying in 30 seconds...') + time.sleep(30) \ No newline at end of file diff --git a/examples/tutorials/05_send_tokens.py b/examples/tutorials/05_send_tokens.py new file mode 100644 index 0000000..bec20d8 --- /dev/null +++ b/examples/tutorials/05_send_tokens.py @@ -0,0 +1,32 @@ +from iota import Iota, Seed, Address, TryteString, ProposedTransaction, Tag + +# Put your seed here from Tutorial 4.a, or a seed that owns tokens (devnet) +my_seed = Seed(b'YOURSEEDFROMTHEPREVIOUSTUTORIAL') + +# Declare an API object +api = Iota( + adapter='https://nodes.devnet.iota.org:443', + seed=my_seed, + testnet=True, +) + +# Addres to receive 1i +# Feel free to replace it. For example, run the code from Tutorial 4.a +# and use that newly generated address with a 'fresh' seed. +receiver = Address(b'WWUTQBO99YDCBVBPAPVCANW9ATSNUPPLCPGDQXGQEVLUBSFHCEWOA9DIYYOXJONDIRHYPXQXOYXDPHREZ') + +print('Constructing transfer of 1i...') +# Create the transfer object +tx = ProposedTransaction( + address=receiver, + value=1, + message=TryteString.from_unicode('I just sent you 1i, use it wisely!'), + tag=Tag('VALUETX'), +) + +print('Preparing bundle and sending it to the network...') +# Prepare the transfer and send it to the network +response = api.send_transfer(transfers=[tx]) + +print('Check your transaction on the Tangle!') +print('https://utils.iota.org/bundle/%s/devnet' % response['bundle'].hash) \ No newline at end of file diff --git a/examples/tutorials/06_store_encrypted.py b/examples/tutorials/06_store_encrypted.py new file mode 100644 index 0000000..f4e5d61 --- /dev/null +++ b/examples/tutorials/06_store_encrypted.py @@ -0,0 +1,66 @@ +""" +Encrypt data and store it on the Tangle. + +simplecrypt library is needed for this example (`pip install simple-crypt`)! +""" +from iota import Iota, TryteString, Tag, ProposedTransaction +from simplecrypt import encrypt +from base64 import b64encode +from getpass import getpass + +import json + +# Declare an API object +api = Iota( + adapter='https://nodes.devnet.iota.org:443', + seed=b'YOURSEEDFROMTHEPREVIOUSTUTORIAL', + testnet=True, +) + +# Some confidential information +data = { + 'name' : 'James Bond', + 'age' : '32', + 'job' : 'agent', + 'address' : 'London', +} + +# Convert to JSON format +json_data = json.dumps(data) + +# Ask user for a password to use for encryption +password = getpass('Please supply a password for encryption:') + +print('Encrypting data...') +# Encrypt data +# Note, that in Python 3, encrypt returns 'bytes' +cipher = encrypt(password, json_data) + +# Encode to base64, output contains only ASCII chars +b64_cipher = b64encode(cipher) + +print('Constructing transaction locally...') +# Convert to trytes +trytes_encrypted_data = TryteString.from_bytes(b64_cipher) + +# Generate an address from your seed to post the transfer to +my_address = api.get_new_addresses(index=42)['addresses'][0] + +# Tag is optional here +my_tag = Tag(b'CONFIDENTIALINFORMATION') + +# Prepare a transaction object +tx = ProposedTransaction( + address=my_address, + value=0, + tag=my_tag, + message=trytes_encrypted_data, +) + +print('Sending transfer...') +# Send the transaction to the network +response = api.send_transfer([tx]) + +print('Check your transaction on the Tangle!') +print('https://utils.iota.org/transaction/%s/devnet' % response['bundle'][0].hash) +print('Tail transaction hash of the bundle is: %s' % response['bundle'].tail_transaction.hash) \ No newline at end of file diff --git a/examples/tutorials/07_fetch_encrypted.py b/examples/tutorials/07_fetch_encrypted.py new file mode 100644 index 0000000..9bdad45 --- /dev/null +++ b/examples/tutorials/07_fetch_encrypted.py @@ -0,0 +1,42 @@ +""" +Decrypt data fetched from the Tangle. + +simplecrypt library is needed for this example (`pip install simple-crypt`)! +""" +from iota import Iota +from simplecrypt import decrypt +from base64 import b64decode +from getpass import getpass + +import json + +# Declare an API object +api = Iota('https://nodes.devnet.iota.org:443', testnet=True) + +# Prompt user for tail tx hash of the bundle +tail_hash = input('Tail transaction hash of the bundle: ') + +print('Looking for bundle on the Tangle...') +# Fetch bundle +bundle = api.get_bundles(tail_hash)['bundles'][0] + +print('Extracting data from bundle...') +# Get all messages from the bundle and concatenate them +b64_encrypted_data = "".join(bundle.get_messages()) + +# Decode from base64 +encrypted_data = b64decode(b64_encrypted_data) + +# Prompt for passwword +password = getpass('Password to be used for decryption:') + +print('Decrypting data...') +# Decrypt data +# decrypt returns 'bytes' in Python 3, decode it into string +json_data = decrypt(password, encrypted_data).decode('utf-8') + +# Convert JSON string to python dict object +data = json.loads(json_data) + +print('Succesfully decrypted the following data:') +print(data) \ No newline at end of file diff --git a/iota-python.png b/iota-python.png new file mode 100644 index 0000000..f0ac2db Binary files /dev/null and b/iota-python.png differ diff --git a/iota/api.py b/iota/api.py index d0d40b3..19481c5 100644 --- a/iota/api.py +++ b/iota/api.py @@ -926,7 +926,20 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False, security_ included in the result. If ``None`` (default), then this method will check every - address until it finds one without any transfers. + address until it finds one that is unused. + + .. note:: + An unused address is an address that **has not been spent from** + and **has no transactions** referencing it on the Tangle. + + A snapshot removes transactions from the Tangle. As a + consequence, after a snapshot, it may happen that this API does + not return the correct account data with ``stop`` being ``None``. + + As a workaround, you can save your used addresses and their + ``key_index`` attribute in a local database. Use the + ``start`` and ``stop`` parameters to tell the API from where to + start checking and where to stop. :param bool inclusion_states: Whether to also fetch the inclusion states of the transfers. @@ -970,14 +983,14 @@ def get_account_data(self, start=0, stop=None, inclusion_states=False, security_ security_level=security_level ) - def get_bundles(self, transaction): - # type: (TransactionHash) -> dict + def get_bundles(self, transactions): + # type: (Iterable[TransactionHash]) -> dict """ Returns the bundle(s) associated with the specified transaction - hash. + hashes. - :param TransactionHash transaction: - Transaction hash. Must be a tail transaction. + :param Iterable[TransactionHash] transactions: + Transaction hashes. Must be a tail transaction. :return: ``dict`` with the following structure:: @@ -988,15 +1001,15 @@ def get_bundles(self, transaction): always a list, even if only one bundle was found. } - :raise: - - :py:class:`iota.adapter.BadApiResponse` if any of the - bundles fails validation. + :raise :py:class:`iota.adapter.BadApiResponse`: + - if any of the bundles fails validation. + - if any of the bundles is not visible on the Tangle. References: - https://github.com/iotaledger/wiki/blob/master/api-proposal.md#getbundle """ - return extended.GetBundlesCommand(self.adapter)(transaction=transaction) + return extended.GetBundlesCommand(self.adapter)(transactions=transactions) def get_inputs( self, @@ -1028,6 +1041,19 @@ def get_inputs( If ``None`` (default), then this method will not stop until it finds an unused address. + .. note:: + An unused address is an address that **has not been spent from** + and **has no transactions** referencing it on the Tangle. + + A snapshot removes transactions from the Tangle. As a + consequence, after a snapshot, it may happen that this API does + not return the correct inputs with ``stop`` being ``None``. + + As a workaround, you can save your used addresses and their + ``key_index`` attribute in a local database. Use the + ``start`` and ``stop`` parameters to tell the API from where to + start checking for inputs and where to stop. + :param Optional[int] threshold: If set, determines the minimum threshold for a successful result: @@ -1236,7 +1262,20 @@ def get_transfers(self, start=0, stop=None, inclusion_states=False): included in the result. If ``None`` (default), then this method will check every - address until it finds one without any transfers. + address until it finds one that is unused. + + .. note:: + An unused address is an address that **has not been spent from** + and **has no transactions** referencing it on the Tangle. + + A snapshot removes transactions from the Tangle. As a + consequence, after a snapshot, it may happen that this API does + not return the expected transfers with ``stop`` being ``None``. + + As a workaround, you can save your used addresses and their + ``key_index`` attribute in a local database. Use the + ``start`` and ``stop`` parameters to tell the API from where to + start checking for transfers and where to stop. :param bool inclusion_states: Whether to also fetch the inclusion states of the transfers. diff --git a/iota/commands/extended/broadcast_bundle.py b/iota/commands/extended/broadcast_bundle.py index a4cb893..b7d84b3 100644 --- a/iota/commands/extended/broadcast_bundle.py +++ b/iota/commands/extended/broadcast_bundle.py @@ -36,7 +36,7 @@ def _execute(self, request): # and validates it. # Returns List[List[TransactionTrytes]] # (outer list has one item in current implementation) - bundle = GetBundlesCommand(self.adapter)(transaction=request['tail_hash']) + bundle = GetBundlesCommand(self.adapter)(transactions=[request['tail_hash']]) BroadcastTransactionsCommand(self.adapter)(trytes=bundle[0]) return { 'trytes': bundle[0], diff --git a/iota/commands/extended/get_bundles.py b/iota/commands/extended/get_bundles.py index ac2575a..cb1acb3 100644 --- a/iota/commands/extended/get_bundles.py +++ b/iota/commands/extended/get_bundles.py @@ -8,8 +8,8 @@ from iota.commands import FilterCommand, RequestFilter from iota.commands.extended.traverse_bundle import TraverseBundleCommand from iota.exceptions import with_context -from iota.filters import Trytes from iota.transaction.validator import BundleValidator +from iota.filters import Trytes __all__ = [ 'GetBundlesCommand', @@ -31,35 +31,41 @@ def get_response_filter(self): pass def _execute(self, request): - transaction_hash = request['transaction'] # type: TransactionHash + transaction_hashes = request['transactions'] # type: Iterable[TransactionHash] + + bundles = [] + + # Fetch bundles one-by-one + for tx_hash in transaction_hashes: + bundle = TraverseBundleCommand(self.adapter)( + transaction=tx_hash + )['bundles'][0] # Currently 1 bundle only - bundle = TraverseBundleCommand(self.adapter)( - transaction=transaction_hash - )['bundles'][0] # Currently 1 bundle only + validator = BundleValidator(bundle) - validator = BundleValidator(bundle) + if not validator.is_valid(): + raise with_context( + exc=BadApiResponse( + 'Bundle failed validation (``exc.context`` has more info).', + ), - if not validator.is_valid(): - raise with_context( - exc=BadApiResponse( - 'Bundle failed validation (``exc.context`` has more info).', - ), + context={ + 'bundle': bundle, + 'errors': validator.errors, + }, + ) - context={ - 'bundle': bundle, - 'errors': validator.errors, - }, - ) + bundles.append(bundle) return { - # Always return a list, so that we have the necessary - # structure to return multiple bundles in a future - # iteration. - 'bundles': [bundle], + 'bundles': bundles, } class GetBundlesRequestFilter(RequestFilter): def __init__(self): super(GetBundlesRequestFilter, self).__init__({ - 'transaction': f.Required | Trytes(TransactionHash), + 'transactions': + f.Required | f.Array | f.FilterRepeater( + f.Required | Trytes(TransactionHash) + ) }) diff --git a/iota/commands/extended/replay_bundle.py b/iota/commands/extended/replay_bundle.py index aff8e1e..b2c2e68 100644 --- a/iota/commands/extended/replay_bundle.py +++ b/iota/commands/extended/replay_bundle.py @@ -34,7 +34,7 @@ def _execute(self, request): min_weight_magnitude = request['minWeightMagnitude'] # type: int transaction = request['transaction'] # type: TransactionHash - gb_response = GetBundlesCommand(self.adapter)(transaction=transaction) + gb_response = GetBundlesCommand(self.adapter)(transactions=[transaction]) # Note that we only replay the first bundle returned by # ``getBundles``. diff --git a/iota/commands/extended/traverse_bundle.py b/iota/commands/extended/traverse_bundle.py index d81196d..d1176db 100644 --- a/iota/commands/extended/traverse_bundle.py +++ b/iota/commands/extended/traverse_bundle.py @@ -7,7 +7,7 @@ import filters as f from iota import BadApiResponse, BundleHash, Transaction, \ - TransactionHash, TryteString, Bundle + TransactionHash, TryteString, Bundle, TransactionTrytes from iota.commands import FilterCommand, RequestFilter from iota.commands.core.get_trytes import GetTrytesCommand from iota.exceptions import with_context @@ -55,10 +55,13 @@ def _traverse_bundle(self, txn_hash, target_bundle_hash): GetTrytesCommand(self.adapter)(hashes=[txn_hash])['trytes'] ) # type: List[TryteString] - if not trytes: + # If no tx was found by the node for txn_hash, it returns 9s, + # so we check here if it returned all 9s trytes. + if not trytes or trytes == [TransactionTrytes('')]: raise with_context( exc=BadApiResponse( - 'Bundle transactions not visible ' + 'Could not get trytes of bundle transaction from the Tangle. ' + 'Bundle transactions not visible.' '(``exc.context`` has more info).', ), diff --git a/iota/commands/extended/utils.py b/iota/commands/extended/utils.py index 2ab0afd..3292a16 100644 --- a/iota/commands/extended/utils.py +++ b/iota/commands/extended/utils.py @@ -118,7 +118,7 @@ def get_bundles_from_transaction_hashes( # Find the bundles for each transaction. for txn in tail_transactions: - gb_response = GetBundlesCommand(adapter)(transaction=txn.hash) + gb_response = GetBundlesCommand(adapter)(transactions=[txn.hash]) txn_bundles = gb_response['bundles'] # type: List[Bundle] if inclusion_states: diff --git a/iota/crypto/types.py b/iota/crypto/types.py index e0597db..c868940 100644 --- a/iota/crypto/types.py +++ b/iota/crypto/types.py @@ -112,6 +112,11 @@ class Seed(TryteString): - https://iota.stackexchange.com/q/249 """ + LEN = 81 + """ + Length of a Seed. + """ + def __init__(self, trytes=None): # type: (Optional[TrytesCompatible]) -> None if trytes and len(trytes) > Hash.LEN: diff --git a/iota/types.py b/iota/types.py index 5f5567c..abc7089 100644 --- a/iota/types.py +++ b/iota/types.py @@ -68,19 +68,32 @@ class TryteString(JsonSerializable): """ @classmethod - def random(cls, length): - # type: (int) -> TryteString + def random(cls, length=None): + # type: (Optional[int]) -> TryteString """ Generates a random sequence of trytes. - :param int length: + :param Optional[int] length: Number of trytes to generate. :return: :py:class:`TryteString` object. + + :raises TypeError: + - if ``length`` is negative, + - if ``length`` is not defined, and the class doesn't have ``LEN`` attribute. """ alphabet = list(itervalues(AsciiTrytesCodec.alphabet)) generator = SystemRandom() + try: + if length is None: + length = cls.LEN + + if length <= 0: + raise TypeError("length parameter needs to be greater than zero") + except AttributeError: # class has no LEN attribute + if length is None: + raise TypeError("{class_name} does not define a length property".format(class_name=cls.__name__)) # :py:meth:`SystemRandom.choices` wasn't added until Python 3.6; # for compatibility, we will continue to use ``choice`` in a diff --git a/setup.py b/setup.py index 0943f48..fca22f6 100644 --- a/setup.py +++ b/setup.py @@ -20,7 +20,7 @@ ## # Load long description for PyPI. -with open('README.rst', 'r', 'utf-8') as f: # type: StreamReader +with open('docs/README.rst', 'r', 'utf-8') as f: # type: StreamReader long_description = f.read() ## @@ -39,7 +39,7 @@ name='PyOTA', description='IOTA API library for Python', url='https://github.com/iotaledger/iota.py', - version='2.2.0b1', + version='2.3.0b1', long_description=long_description, diff --git a/test/commands/extended/get_bundles_test.py b/test/commands/extended/get_bundles_test.py index 4d596fb..4c91e85 100644 --- a/test/commands/extended/get_bundles_test.py +++ b/test/commands/extended/get_bundles_test.py @@ -23,10 +23,16 @@ def setUp(self): super(GetBundlesRequestFilterTestCase, self).setUp() # noinspection SpellCheckingInspection - self.transaction = ( - 'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR' - 'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ' - ) + self.transactions = [ + ( + 'TESTVALUE9DONTUSEINPRODUCTION99999KPZOTR' + 'VDB9GZDJGZSSDCBIX9QOK9PAV9RMDBGDXLDTIZTWQ' + ), + ( + 'TESTVALUE9DONTUSEINPRODUCTION99999TAXQBF' + 'ZMUQLZ9RXRRXQOUSAMGAPEKTZNERIKSDYGHQA9999' + ), + ] def test_pass_happy_path(self): """ @@ -34,7 +40,7 @@ def test_pass_happy_path(self): """ # Raw trytes are extracted to match the IRI's JSON protocol. request = { - 'transaction': self.transaction, + 'transactions': self.transactions, } filter_ = self._filter(request) @@ -47,9 +53,14 @@ def test_pass_compatible_types(self): Request contains values that can be converted to the expected types. """ + # Convert first to TranscationHash + tx_hashes = [] + for tx in self.transactions: + tx_hashes.append(TransactionHash(tx)) + filter_ = self._filter({ # Any TrytesCompatible value will work here. - 'transaction': TransactionHash(self.transaction), + 'transactions': tx_hashes, }) self.assertFilterPasses(filter_) @@ -57,7 +68,7 @@ def test_pass_compatible_types(self): filter_.cleaned_data, { - 'transaction': self.transaction, + 'transactions': self.transactions, }, ) @@ -69,7 +80,7 @@ def test_fail_empty(self): {}, { - 'transaction': [f.FilterMapper.CODE_MISSING_KEY], + 'transactions': [f.FilterMapper.CODE_MISSING_KEY], }, ) @@ -79,7 +90,7 @@ def test_fail_unexpected_parameters(self): """ self.assertFilterErrors( { - 'transaction': TransactionHash(self.transaction), + 'transactions': self.transactions, # SAY "WHAT" AGAIN! 'what': 'augh!', @@ -92,29 +103,73 @@ def test_fail_unexpected_parameters(self): def test_fail_transaction_wrong_type(self): """ - ``transaction`` is not a TrytesCompatible value. + ``transactions`` contains no TrytesCompatible value. """ self.assertFilterErrors( { - 'transaction': 42, + 'transactions': [42], }, { - 'transaction': [f.Type.CODE_WRONG_TYPE], + 'transactions.0': [f.Type.CODE_WRONG_TYPE], }, ) def test_fail_transaction_not_trytes(self): """ - ``transaction`` contains invalid characters. + ``transactions`` contains invalid characters. + """ + self.assertFilterErrors( + { + 'transactions': [b'not valid; must contain only uppercase and "9"'], + }, + + { + 'transactions.0': [Trytes.CODE_NOT_TRYTES], + }, + ) + + def test_fail_no_list(self): + """ + ``transactions`` has one hash rather than a list of hashes. + """ + self.assertFilterErrors( + { + 'transactions': self.transactions[0], + }, + + { + 'transactions': [f.Type.CODE_WRONG_TYPE], + }, + ) + + def test_fail_transactions_contents_invalid(self): + """ + ``transactions`` is a non-empty array, but it contains invlaid values. """ self.assertFilterErrors( { - 'transaction': b'not valid; must contain only uppercase and "9"', + 'transactions': [ + b'', + True, + None, + b'not valid transaction hash', + + # A valid tx hash, this should not produce error + TransactionHash(self.transactions[0]), + + 65498731, + b'9' * (TransactionHash.LEN +1), + ], }, { - 'transaction': [Trytes.CODE_NOT_TRYTES], + 'transactions.0': [f.Required.CODE_EMPTY], + 'transactions.1': [f.Type.CODE_WRONG_TYPE], + 'transactions.2': [f.Required.CODE_EMPTY], + 'transactions.3': [Trytes.CODE_NOT_TRYTES], + 'transactions.5': [f.Type.CODE_WRONG_TYPE], + 'transactions.6': [Trytes.CODE_WRONG_FORMAT], }, ) @@ -286,7 +341,7 @@ def test_wireup(self): api = Iota(self.adapter) # Don't need to call with proper args here. - response = api.get_bundles('transaction') + response = api.get_bundles('transactions') self.assertTrue(mocked_command.called) @@ -308,15 +363,45 @@ def test_happy_path(self): 'trytes': [self.spam_trytes], }) - response = self.command(transaction = self.tx_hash) + response = self.command(transactions = [self.tx_hash]) + + self.maxDiff = None + original_bundle = Bundle.from_tryte_strings(self.bundle_trytes) + self.assertListEqual( + response['bundles'][0].as_json_compatible(), + original_bundle.as_json_compatible(), + ) + + def test_happy_path_multiple_bundles(self): + """ + Get two bundles with multiple transactions. + """ + # We will fetch the same two bundle + for _ in range(2): + for txn_trytes in self.bundle_trytes: + self.adapter.seed_response('getTrytes', { + 'trytes': [txn_trytes], + }) + + self.adapter.seed_response('getTrytes', { + 'trytes': [self.spam_trytes], + }) + + response = self.command(transactions = [self.tx_hash, self.tx_hash]) self.maxDiff = None original_bundle = Bundle.from_tryte_strings(self.bundle_trytes) + self.assertListEqual( response['bundles'][0].as_json_compatible(), original_bundle.as_json_compatible(), ) + self.assertListEqual( + response['bundles'][1].as_json_compatible(), + original_bundle.as_json_compatible(), + ) + def test_validator_error(self): """ TraverseBundleCommand returns bundle but it is invalid. @@ -335,4 +420,4 @@ def test_validator_error(self): }) with self.assertRaises(BadApiResponse): - response = self.command(transaction = self.tx_hash) \ No newline at end of file + response = self.command(transactions = [self.tx_hash]) \ No newline at end of file diff --git a/test/commands/extended/traverse_bundle_test.py b/test/commands/extended/traverse_bundle_test.py index f28066f..b869b36 100644 --- a/test/commands/extended/traverse_bundle_test.py +++ b/test/commands/extended/traverse_bundle_test.py @@ -451,3 +451,20 @@ def test_missing_transaction(self): b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' ), ) + + def test_missing_transaction_zero_trytes(self): + """ + Unable to find the requested transaction. + getTrytes returned only zeros, no tx was found. + """ + zero_trytes = TransactionTrytes('') + self.adapter.seed_response('getTrytes', {'trytes': [zero_trytes]}) + + with self.assertRaises(BadApiResponse): + self.command( + transaction = + TransactionHash( + b'FSEWUNJOEGNUI9QOCRFMYSIFAZLJHKZBPQZZYFG9' + b'ORYCRDX9TOMJPFCRB9R9KPUUGFPVOWYXFIWEW9999' + ), + ) \ No newline at end of file diff --git a/test/crypto/types_test.py b/test/crypto/types_test.py index 39164c3..55682f4 100644 --- a/test/crypto/types_test.py +++ b/test/crypto/types_test.py @@ -79,6 +79,12 @@ def test_init_error_too_short(self): with self.assertRaises(ValueError): Digest(b'9' * (2 * Hash.LEN - 1)) + def test_random(self): + """ + Generating a random Digest should fail. + """ + with self.assertRaises(TypeError): + random_digest = Digest.random() # noinspection SpellCheckingInspection class PrivateKeyTestCase(TestCase): @@ -152,3 +158,10 @@ def test_get_digest_multiple_fragments(self): # # Each fragment is processed independently, which is critical for # multisig to work correctly. + + def test_random(self): + """ + Generating a random PrivateKey should fail. + """ + with self.assertRaises(TypeError): + random_digest = PrivateKey.random() diff --git a/test/transaction/types_test.py b/test/transaction/types_test.py index 6cbf1bf..c39570b 100644 --- a/test/transaction/types_test.py +++ b/test/transaction/types_test.py @@ -6,7 +6,8 @@ from six import binary_type -from iota import TransactionHash +from iota import TransactionHash, BundleHash, Fragment, TransactionTrytes, \ + Nonce class TransactionHashTestCase(TestCase): @@ -39,3 +40,42 @@ def test_init_error_too_long(self): b'JVMTDGDPDFYHMZPMWEKKANBQSLSDTIIHAYQUMZOK' b'HXXXGJHJDQPOMDOMNRDKYCZRUFZROZDADTHZC99999' ) + + def test_random(self): + """ + Creating a random TransactionHash object. + """ + random_tx_hash = TransactionHash.random() + self.assertEqual(len(random_tx_hash), TransactionHash.LEN) + +class BundleHashTestCase(TestCase): + def test_random(self): + """ + Creating a random BundleHash object. + """ + random_bundle_hash = BundleHash.random() + self.assertEqual(len(random_bundle_hash), BundleHash.LEN) + +class FragmentTestCase(TestCase): + def test_random(self): + """ + Creating a random Fragment object. + """ + random_fragment = Fragment.random() + self.assertEqual(len(random_fragment), Fragment.LEN) + +class TransactionTrytesTestCase(TestCase): + def test_random(self): + """ + Creating a random TransactionTrytes object. + """ + random_tx_trytes = TransactionTrytes.random() + self.assertEqual(len(random_tx_trytes), TransactionTrytes.LEN) + +class NonceTestCase(TestCase): + def test_random(self): + """ + Creating a random Nonce object. + """ + random_nonce = Nonce.random() + self.assertEqual(len(random_nonce), Nonce.LEN) \ No newline at end of file diff --git a/test/types_test.py b/test/types_test.py index bdf4a10..2127d6c 100644 --- a/test/types_test.py +++ b/test/types_test.py @@ -754,6 +754,20 @@ def test_random(self): # generated. self.assertEqual(len(trytes), Hash.LEN) + def test_random_no_length(self): + """ + Trying to create a random TryteString without specifying length. + """ + with self.assertRaises(TypeError): + trytes = TryteString.random() + + def test_random_wrong_length(self): + """ + Generating random Trytestring with negative length. + """ + with self.assertRaises(TypeError): + trytes = TryteString.random(length=-5) + def test_from_bytes(self): """ Converting a sequence of bytes into a TryteString. @@ -883,6 +897,14 @@ def test_from_trits_wrong_length_padded(self): b'RBTC', ) +class HashTestCase(TestCase): + def test_random(self): + """ + Generating a random Hash. + """ + rand = Hash.random() + self.assertEqual(len(rand), Hash.LEN) + # noinspection SpellCheckingInspection class AddressTestCase(TestCase): @@ -1124,6 +1146,12 @@ def test_remove_checksum_second_time(self): self.assertFalse(addy.is_checksum_valid()) self.assertTrue(len(addy) == Address.LEN) + def test_random(self): + """ + Creating a random Address object. + """ + addy = Address.random() + self.assertEqual(len(addy), Address.LEN) # noinspection SpellCheckingInspection class AddressChecksumTestCase(TestCase): @@ -1149,6 +1177,13 @@ def test_init_error_too_long(self): # If it's an address checksum, it must be 9 trytes exactly. AddressChecksum(b'FOXM9MUBX9') + def test_random(self): + """ + Creating a random AddressChecksum object. + """ + checksum = AddressChecksum.random() + self.assertEqual(len(checksum), AddressChecksum.LEN) + # noinspection SpellCheckingInspection class TagTestCase(TestCase): @@ -1167,3 +1202,10 @@ def test_init_error_too_long(self): with self.assertRaises(ValueError): # 28 chars = no va. Tag(b'COLOREDCOINS9999999999999999') + + def test_random(self): + """ + Creating a random Tag object. + """ + tag = Tag.random() + self.assertEqual(len(tag), Tag.LEN) \ No newline at end of file