Skip to content
This repository was archived by the owner on Jan 13, 2023. It is now read-only.

Commit 29cf906

Browse files
authored
Merge pull request #312 from lzpap/async_tutorial
Tutorial 8: Async Send and Monitor
2 parents e72ec3d + 955d94b commit 29cf906

File tree

2 files changed

+302
-1
lines changed

2 files changed

+302
-1
lines changed

docs/tutorials.rst

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,11 +716,197 @@ Now you know how to use the Tangle for data storage while keeping privacy.
716716
When you need more granular access control on how and when one could read
717717
data from the Tangle, consider using `Masked Authenticated Messaging`_ (MAM).
718718

719+
8. Send and Monitor Concurrently
720+
--------------------------------
721+
722+
In this example, you will learn how to:
723+
724+
- **Use the asynchronous PyOTA API.**
725+
- **Send transactions concurrently.**
726+
- **Monitor confirmation of transactions concurrently.**
727+
- **Execute arbitrary code concurrently while doing the former two.**
728+
729+
.. warning::
730+
731+
If you are new to `coroutines`_ and asynchronous programming in Python, it
732+
is strongly recommended that you check out this `article`_ and the official
733+
`asyncio`_ documentation before proceeding.
734+
735+
Code
736+
~~~~
737+
.. literalinclude:: ../examples/tutorials/08_async_send_monitor.py
738+
:linenos:
739+
740+
Discussion
741+
~~~~~~~~~~
742+
This example is divided into 4 logical parts:
743+
744+
1. Imports and constant declarations
745+
2. `Coroutine`_ to send and monitor a list of transactions as a bundle.
746+
3. `Coroutine`_ to execute arbitrary code concurrently.
747+
4. A main `coroutine`_ to schedule the execution of our application.
748+
749+
Let's start with the most simple one: **Imports and Constants**.
750+
751+
.. literalinclude:: ../examples/tutorials/08_async_send_monitor.py
752+
:lines: 1-17
753+
:lineno-start: 1
754+
755+
Notice, that we import the :py:class:`AsyncIota` api class, because we
756+
would like to use the asynchronous and concurrent features of PyOTA.
757+
:py:class:`List` from the :py:class:`typing` library is needed for correct
758+
type annotations, and we also import the `asyncio`_ library. This will come
759+
in handy when we want to schedule and run the coroutines.
760+
761+
On line 6, we instantiate an asynchronous IOTA api. Functionally, it does the
762+
same operations as :py:class:`Iota`, but the api calls are defined as
763+
coroutines. For this tutorial, we connect to a devnet node, and explicitly tell
764+
this as well to the api on line 8.
765+
766+
On line 12, we declare an IOTA address. We will send our zero value transactions
767+
to this address. Feel free to change it to your own address.
768+
769+
Once we have sent the transactions, we start monitoring their confirmation by the
770+
network. Confirmation time depends on current network activity, the referenced
771+
tips, etc., therefore we set a ``timeout`` of 120 seconds on line 15. You might
772+
have to modify this value later to see the confirmation of your transactions.
773+
774+
You can also fine-tune the example code by tinkering with ``polling_interval``.
775+
This is the interval between two subsequent confirmation checks.
776+
777+
Let's move on to the next block, namely the **send and monitor coroutine**.
778+
779+
.. literalinclude:: ../examples/tutorials/08_async_send_monitor.py
780+
:lines: 20-62
781+
:lineno-start: 20
782+
783+
Notice, that coroutines are defined in python by the ``async def`` keywords.
784+
This makes them `awaitable`_.
785+
786+
From the type annotations, we see that :py:meth:`send_and_monitor` accepts a
787+
list of :py:class:`ProposedTransaction` objects and return a ``bool``.
788+
789+
On line 28, we send the transfers with the help of
790+
:py:meth:`AsyncIota.send_transfer`. Since this is not a regular method, but a
791+
coroutine, we have to ``await`` its result. :py:meth:`AsyncIota.send_transfer`
792+
takes care of building the bundle, doing proof-of-work and sending the
793+
transactions within the bundle to the network.
794+
795+
Once we sent the transfer, we collect individual transaction hashes from the
796+
bundle, which we will use for confirmation checking.
797+
798+
On line 39, the so-called confirmation checking starts. With the help of
799+
:py:meth:`AsyncIota.get_inclusion_states`, we determine if our transactions
800+
have been confirmed by the network.
801+
802+
.. note::
803+
804+
You might wonder how your transactions get accepted by the network, that is,
805+
how they become confirmed.
806+
807+
- Pre-`Coordicide`_ (current state), transactions are confirmed by
808+
directly or indirectly being referenced by a `milestone`_.
809+
A milestone is a special transaction issued by the `Coordinator`_.
810+
- Post-`Coordicide`_ , confirmation is the result of nodes reaching
811+
consensus by a `voting mechanism`_.
812+
813+
The ``None`` value for the ``tips``
814+
parameter in the argument list basically means that we check against the latest
815+
milestone.
816+
817+
On line 43, we iterate over our original ``sent_tx_hashes`` list of sent
818+
transaction hashes and ``gis_response['states']``, which is a list of ``bool``
819+
values, at the same time using the built-in `zip`_ method. We also employ
820+
`enumerate`_, because we need the index of the elements in each iteration.
821+
822+
If a transaction is confirmed, we delete the corresponding elements from the
823+
lists. When all transactions are confirmed, ``sent_tx_hashes`` becomes empty,
824+
and the loop condition becomes ``False``.
825+
826+
If however, not all transactions have been confirmed, we should continue
827+
checking for confirmation. Observe line 58, where we suspend the coroutine
828+
with :py:meth:`asyncio.sleep` for ``polling_interval`` seconds. Awaiting the
829+
result of :py:meth:`asyncio.sleep` will cause our coroutine to continue
830+
execution in ``polling_interval`` time. While our coroutine is sleeping,
831+
other coroutines can run concurrently, hence it is a non-blocking call.
832+
833+
To do something in the meantime, we can **execute another coroutine concurrently**:
834+
835+
.. literalinclude:: ../examples/tutorials/08_async_send_monitor.py
836+
:lines: 65-71
837+
:lineno-start: 65
838+
839+
This is really just a dummy coroutine that prints something to the terminal and
840+
then goes to sleep periodically, but in a real application, you could do
841+
meaningful tasks here.
842+
843+
Now let's look at how to **schedule the execution of our application with the
844+
main coroutine**:
845+
846+
.. literalinclude:: ../examples/tutorials/08_async_send_monitor.py
847+
:lines: 74-115
848+
:lineno-start: 74
849+
850+
First, we declare a list of :py:meth:`ProposedTransaction` objects, that will
851+
be the input for our :py:meth:`send_and_monitor` coroutine.
852+
853+
The important stuff begins on line 101. We use :py:meth:`asyncio.gather` to
854+
submit our coroutines for execution, wait for their results and then return
855+
them in a list. `gather`_ takes our coroutines, transforms them into runnable
856+
`tasks`_, and runs them concurrently.
857+
858+
Notice, that we listed :py:meth:`send_and_monitor` twice in
859+
:py:meth:`asyncio.gather` with the same list of :py:meth:`ProposedTransaction`
860+
objects. This is to showcase how you can send and monitor multiple transfers
861+
concurrently. In this example, two different bundles will be created from the
862+
same :py:meth:`ProposedTransaction` objects. The two bundles post zero value
863+
transactions to the same address, contain the same messages respectively,
864+
but are not dependent on each other in any way. That is why we can send them
865+
concurrently.
866+
867+
As discussed previously, ``result`` will be a list of results of the coroutines
868+
submitted to :py:meth:`asyncio.gather`, preserving their order.
869+
``result[0]`` is the result from the first :py:meth:`send_and_monitor`, and
870+
``result[1]`` is the result from the second :py:meth:`send_and_monitor` from the
871+
argument list. If any of these are ``False``, confirmation did not happen
872+
before ``timeout``.
873+
874+
When you see the message from line 109 in your terminal, try increasing
875+
``timeout``, or check the status of the network, maybe there is a temporary
876+
downtime on the devnet due to maintenance.
877+
878+
Lastly, observe lines 113-115. If the current file (python module) is run
879+
from the terminal, we use :py:meth:`ayncio.run` to execute the main coroutine
880+
inside an `event loop`_.
881+
882+
To run this example, navigate to ``examples/tutorial`` inside the cloned
883+
PyOTA repository, or download the source file of `Tutorial 8 from GitHub`_
884+
and run the following in a terminal:
885+
886+
.. code-block:: sh
887+
888+
$ python 08_async_send_monitor.py
889+
719890
.. _PyOTA Bug Tracker: https://github.com/iotaledger/iota.py/issues
720891
.. _bytestring: https://docs.python.org/3/library/stdtypes.html#bytes
721892
.. _tryte alphabet: https://docs.iota.org/docs/getting-started/0.1/introduction/ternary#tryte-encoding
722893
.. _Tangle Explorer: https://utils.iota.org
723894
.. _Account Module: https://docs.iota.org/docs/client-libraries/0.1/account-module/introduction/overview
724895
.. _spending twice from the same address: https://docs.iota.org/docs/getting-started/0.1/clients/addresses#spent-addresses
725896
.. _Base64: https://en.wikipedia.org/wiki/Base64
726-
.. _Masked Authenticated Messaging: https://docs.iota.org/docs/client-libraries/0.1/mam/introduction/overview?q=masked%20auth&highlights=author;authent
897+
.. _Masked Authenticated Messaging: https://docs.iota.org/docs/client-libraries/0.1/mam/introduction/overview?q=masked%20auth&highlights=author;authent
898+
.. _coroutine: https://docs.python.org/3/glossary.html#term-coroutine
899+
.. _coroutines: https://docs.python.org/3/glossary.html#term-coroutine
900+
.. _asyncio: https://docs.python.org/3/library/asyncio.html
901+
.. _article: https://realpython.com/async-io-python/
902+
.. _awaitable: https://docs.python.org/3/library/asyncio-task.html#awaitables
903+
.. _Coordicide: https://coordicide.iota.org/
904+
.. _milestone: https://docs.iota.org/docs/getting-started/0.1/network/the-coordinator#milestones
905+
.. _coordinator: https://docs.iota.org/docs/getting-started/0.1/network/the-coordinator
906+
.. _voting mechanism: https://coordicide.iota.org/module4.1
907+
.. _zip: https://docs.python.org/3.3/library/functions.html#zip
908+
.. _enumerate: https://docs.python.org/3.3/library/functions.html#enumerate
909+
.. _gather: https://docs.python.org/3/library/asyncio-task.html#running-tasks-concurrently
910+
.. _tasks: https://docs.python.org/3/library/asyncio-task.html#asyncio.Task
911+
.. _event loop: https://docs.python.org/3/library/asyncio-eventloop.html
912+
.. _Tutorial 8 from GitHub: https://github.com/iotaledger/iota.py/blob/master/examples/tutorials/08_async_send_monitor.py
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
from iota import AsyncIota, ProposedTransaction, Address, TryteString
2+
from typing import List
3+
import asyncio
4+
5+
# Asynchronous API instance.
6+
api = AsyncIota(
7+
adapter='https://nodes.devnet.iota.org:443',
8+
devnet=True,
9+
)
10+
11+
# An arbitrary address to send zero-value transactions to.
12+
addy = Address('PZITJTHCIIANKQWEBWXUPHWPWVNBKW9GMNALMGGSIAUOYCKNWDLUUIGAVMJYCHZXHUBRIVPLFZHUVDLME')
13+
14+
# Timeout after which confirmation monitoring stops (seconds).
15+
timeout = 120
16+
# How often should we poll for confirmation? (seconds)
17+
polling_interval = 5
18+
19+
20+
async def send_and_monitor(
21+
transactions: List[ProposedTransaction]
22+
) -> bool:
23+
"""
24+
Send a list of transactions as a bundle and monitor their confirmation
25+
by the network.
26+
"""
27+
print('Sending bundle...')
28+
st_response = await api.send_transfer(transactions)
29+
30+
sent_tx_hashes = [tx.hash for tx in st_response['bundle']]
31+
32+
print('Sent bundle with transactions: ')
33+
print(*sent_tx_hashes, sep='\n')
34+
35+
# Measure elapsed time
36+
elapsed = 0
37+
38+
print('Checking confirmation...')
39+
while len(sent_tx_hashes) > 0:
40+
# Determine if transactions are confirmed
41+
gis_response = await api.get_inclusion_states(sent_tx_hashes, None)
42+
43+
for i, (tx, is_confirmed) in enumerate(zip(sent_tx_hashes, gis_response['states'])):
44+
if is_confirmed:
45+
print('Transaction %s is confirmed.' % tx)
46+
# No need to check for this any more
47+
del sent_tx_hashes[i]
48+
del gis_response['states'][i]
49+
50+
if len(sent_tx_hashes) > 0:
51+
if timeout <= elapsed:
52+
# timeout reached, terminate checking
53+
return False
54+
# Show some progress on the screen
55+
print('.')
56+
# Put on hold for polling_interval. Non-blocking, so you can
57+
# do other stuff in the meantime.
58+
await asyncio.sleep(polling_interval)
59+
elapsed = elapsed + polling_interval
60+
61+
# All transactions in the bundle are confirmed
62+
return True
63+
64+
65+
async def do_something() -> None:
66+
"""
67+
While waiting for confirmation, you can execute arbitrary code here.
68+
"""
69+
for _ in range(5):
70+
print('Doing something in the meantime...')
71+
await asyncio.sleep(2)
72+
73+
74+
async def main() -> None:
75+
"""
76+
A simple application that sends zero-value transactions to the Tangle and
77+
monitors the confirmation by the network. While waiting for the
78+
confirmation, we schedule a task (`do_something()`) to be executed concurrently.
79+
"""
80+
# Transactions to be sent.
81+
transactions = [
82+
ProposedTransaction(
83+
address=addy,
84+
value=0,
85+
message=TryteString.from_unicode('First'),
86+
),
87+
ProposedTransaction(
88+
address=addy,
89+
value=0,
90+
message=TryteString.from_unicode('Second'),
91+
),
92+
ProposedTransaction(
93+
address=addy,
94+
value=0,
95+
message=TryteString.from_unicode('Third'),
96+
),
97+
]
98+
99+
# Schedule coroutines as tasks, wait for them to finish and gather their
100+
# results.
101+
result = await asyncio.gather(
102+
send_and_monitor(transactions),
103+
# Send the same content. Bundle will be different!
104+
send_and_monitor(transactions),
105+
do_something(),
106+
)
107+
108+
if not (result[0] and result[1]):
109+
print('Transactions did not confirm after %s seconds!' % timeout)
110+
else:
111+
print('All transactions are confirmed!')
112+
113+
if __name__ == '__main__':
114+
# Execute main() inside an event loop if the file is ran
115+
asyncio.run(main())

0 commit comments

Comments
 (0)