Recently, I started to notice an uptick in the number of community members interested in building on ICON. I’ve been developing my own ICON APIs and bots for a little over a year now, and I recall it was pretty difficult to get started. I was a total beginner when it came to programming, and there weren’t any project-based tutorials that walked through the entire development process from setting up a local environment to deploying the finished product.
Thus, I’ve decided to dedicate some time into writing, and hopefully recording, a set of tutorials for beginner and intermediate developers looking to build on ICON. When it comes to building on ICON, there are two major categories of development – infrastructure and SCORE development. “Infrastructure” isn’t an official term or anything like that. It’s just what I call anything that’s not SCORE – ICON terminology for “smart contract”.
In this tutorial series, we’ll be focusing on building infrastructure-related stuff like APIs to query for some kind of data, apps that can timestamp data into transactions, Discord bots for relaying smart contract events in real-time, and more. For this tutorial series, I’ll be using ICON’s Python SDK, as that is the programming language that I’m most familiar with. However, most of the concepts can be applied to the other SDKs as well – JavaScript, Java, etc.
In this first tutorial, you’ll learn how to set up a local Python development environment for building apps and bots that leverage the ICON blockchain. We’ll cover topics like how to install Python and the ICON SDK, how to set up a Python virtual environment, and how to query for the latest block to test whether the environment is set up correctly.
What is the ICON SDK?
Before we dive into how to install Python and the ICON SDK, let’s first talk about what the ICON SDK is exactly. An SDK, short for “software development kit”, is a collection of software tools that make developing for something easier. In the context of the ICON blockchain, the ICON SDK makes interacting with nodes more streamlined and less verbose.
To communicate with ICON nodes (e.g. sending a query to get information about the latest block), you need to send a POST request in the correct format. At this time, ICON nodes use the JSON-RPC 2.0 specification. Don’t worry, you don’t need to be a JSON-RPC protocol expert to build on ICON – just know that nodes communicate via JSON-RPC.
Here’s an example of a JSON-RPC request lifecycle (request, response, and error) from the ICON developer portal:
// Request
{
"jsonrpc": "2.0",
"method": "$STRING1",
"id": $INT,
"params": {
"$KEY1": "$VALUE1",
"$KEY2": {
"method": "$STRING2",
"params": {
"$KEY3": "$VALUE3"
}
}
}
}
// Response - success
{
"jsonrpc": "2.0",
"id": $INT,
"result": "$STRING"
// or
"result": {
"$KEY1": "$VALUE1",
"$KEY2": "$VALUE2"
}
}
// Response - error
{
"jsonrpc": "2.0",
"id": $INT1,
"error": {
"code": $INT2,
"message": "$STRING"
}
}
The JSON-RPC syntax might look intimidating at first, but it’s actually pretty simple once you realize it’s just JSON that follows a specific syntax.
Alright, so how do ICON nodes make use of JSON-RPC exactly?
Instead of trying to describe it, let me just show you a snippet of JSON:
{
"jsonrpc": "2.0",
"method": "icx_getLastBlock",
"id": 1234
}
The message above is what you’d want to send to an ICON node if you wanted to get the latest block height. Similarly, here’s what you’d send to get the result of an ICX transaction:
{
"jsonrpc": "2.0",
"method": "icx_getTransactionResult",
"id": 1234,
"params": {
"txHash": "0xb903239f8543d04b5dc1ba6579132b143087c68db1b2168786408fcbce568238"
}
}
Now, imagine if you were developing an API or a bot that needed to make a bunch of these JSON-RPC POST requests to query data from the blockchain.
You’d end up with a bunch of code like this for each request:
import requests
api_endpoint = "https://ctz.solidwallet.io/api/v3"
payload = {
"jsonrpc": "2.0",
"method": "icx_getBlockByHeight",
"id": 1234,
"params": {
"height": "0x3"
}
}
r = requests.post(api_endpoint, json=payload)
print(r.json())
Making JSON-RPC requests directly is fine if you’re just doing a few calls, but having to specify the correct syntax for many calls can get overwhelming very quickly. That’s where the ICON SDK comes in. By using the SDK, you don’t have to worry about ensuring your JSON payloads are formatted correctly, and that’s just the start. Simply put, the ICON SDK implements all the JSON-RPC syntax for you, and abstracts away a lot of the repetitive and verbose stuff, so you can focus on building out the actual functionality of your app.
With the ICON SDK, the code above can be reduced to:
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider
icon_service = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))
block = icon_service.get_block(3)
print(block)
As you can see, it’s much less verbose than the JSON-RPC example! Alright, now let’s get into how to install Python and the ICON SDK.
How to Install Python and the ICON SDK
For this tutorial series, I’ll be using macOS. If you’re using Windows or Linux, check out the official Python website for instructions on how to install Python 3. Most macOS installations ship with a default Python installation. However, in most cases, the default macOS Python version is Python 2.7. To work with the ICON SDK, you’ll need to install Python 3.
How to Install Python 3 on macOS With Homebrew
To install Python 3 on macOS, I recommend using Homebrew – a handy package manager that makes it easy to install and update a wide variety of software. If you don’t have brew installed on your system, you can install it by running the command below in Terminal:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
After Homebrew has been installed, edit your ~/.zprofile
file and add the line below to the bottom of the file:
export PATH="/usr/local/opt/python/libexec/bin:$PATH"
If you’re unsure about how to edit the ~/.zprofile
file, use the Terminal command below:
nano ~/.zprofile
This will open the file in the nano text editor. After you’ve added the file, press Ctrl+O and Enter to save the file. To exit nano, press Ctrl+X.
Next, use Homebrew to install Python 3 with the command below:
brew update
brew install python
After the installation has finished, ensure Python 3 has been installed using the command below:
python3 --version
If the installation was successful, you should see Python 3.9.5
, or whatever the latest version of Python was when you ran the command. The important thing here is that we now have an installation of Python 3 to work with.
How to Install pyenv
After installing Python 3, we’ll need to install pyenv – a tool for managing multiple Python versions on your system.
To install pyenv, use the command below:
brew update
brew install pyenv
After install pyenv, let’s add a few lines to our ~/.zprofile
and ~/.zshrc
files. Depending on your operating system and shell, you may need to edit a different file. These commands will let us use the pyenv
command without having to configure it every time.
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.zprofile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.zprofile
echo 'eval "$(pyenv init --path)"' >> ~/.zprofile
echo 'eval "$(pyenv init -)"' >> ~/.zshrc
exec $SHELL
Alright, now that pyenv has been installed, let’s try installing another Python version. ICON recommends using Python 3.7 or above for ICON SDK, so let’s install the latest stable version of Python 3.7 (3.7.10).
Use the command below to install Python 3.7.10 with pyenv:
pyenv install 3.7.10
After installing Python 3.7.10, run pyenv versions
and you should see 3.7.10 in the list of installed versions.
How to Install Poetry
We’re almost done with environment configuration, so hang in there! The last housekeeping item we need to install and configure is Poetry. Poetry is an easy-to-use Python dependency manager that also lets you package your Python scripts for distribution online.
What’s a dependency manager, and why do we need one?
When you’re developing a Python app, or any app for that matter, you’ll probably end up installing third-party packages. For example, if your Python app requires you to make HTTP requests, you’ll probably want to install the tried and tested Requests package instead of implementing the HTTP specification with built-in Python libraries. To keep it simple, a dependency manager helps you keep track of what third-party libraries, along with which specific versions of said libraries, are required by your app. This makes it easier for both you and others to deploy and use your app.
Poetry also lets you create virtual Python environments to run your code. This feature is very useful for isolating dependencies on a per-project basis. Don’t worry if that doesn’t make sense right now. We’ll dig deeper into virtual environments later on in the tutorial.
To install Poetry, run the command below:
brew install poetry
Now that Poetry has been installed, let’s create our first project!
How to Create a Project With Poetry
To create a project, navigate to your Desktop (or whatever folder you want to work in) with cd ~/Desktop
. Next run, the command below to create a new project:
poetry new icon-sdk-project
Next, run cd icon-sdk-project
followed by ls
, and you should see the directory structure below.
- pyproject.toml - Configuration file for Poetry.
- README.rst - README file where you can write instructions for your app.
- icon_sdk_project/ - Project folder where application code will go.
- tests/ - Tests folder for application test code.
Edit the pyproject.toml File
Now let’s update the pyproject.toml
file, and change a few settings. Open up pyproject.toml
in Terminal or your text editor, and you should see something like this.
[tool.poetry]
name = "icon-sdk-project"
version = "0.1.0"
description = ""
authors = ["Brian Li <brian@brianli.com>"]
[tool.poetry.dependencies]
python = "^3.9"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
For this project, we want to use Python 3.7.10, so go ahead and change python = "^3.9"
to python = "3.7.10"
, and save the file. Setting python
to 3.7.10
in the pyproject.toml
lets people know that our code runs well on Python 3.7.10. The other settings can be left as-is for the time being.
At this point, your pyproject.toml
should look something like this:
[tool.poetry]
name = "icon-sdk-project"
version = "0.1.0"
description = ""
authors = ["Brian Li <brian@brianli.com>"]
[tool.poetry.dependencies]
python = "3.7.10"
[tool.poetry.dev-dependencies]
pytest = "^5.2"
[build-system]
requires = ["poetry-core>=1.0.0"]
build-backend = "poetry.core.masonry.api"
How to Create a Virtual Python Environment
Now let’s configure Poetry to use Python 3.7.10 when we run our code. To do this, run the command below in the icon-sdk-project
folder.
pyenv shell 3.7.10
poetry env use 3.7.10
poetry shell
After running poetry shell
, you should see a slight change in your Terminal. The command line input should now include the name of the virtual environment. In the screenshot below, you can see the input line now reads (icon-sdk-project-tHQQLkM1-py3.7) brianli@Brians-MacBook-Pro icon-sdk-project
. This means all commands are now being executed within the icon-sdk-project-tHQQLkM1-py3.7
virtual environment.

Now let’s install our project dependencies! From this point forward, make sure you’re running commands in the Python 3.7.10 virtual environment.
For the purposes of this tutorial, we’ll develop a basic Python app to fetch information about the latest block. For a simple script, we don’t need anything else besides the ICON SDK package, so let’s go ahead and install that into our virtual environment with the command below:
poetry add iconsdk
How to Query the ICON Blockchain
After the ICON SDK has been installed, open up your favorite text editor (I recommend Visual Studio Code or Sublime Text). Next, create a new file and save it in the ~/Desktop/icon-sdk-project/icon_sdk_project folder as main.py
. In other words, the file path to main.py
should be ~/Desktop/icon-sdk-project/icon_sdk_project/main.py
How to Import ICON SDK
In order to use the ICON SDK, we need to import it into our project. To do this, let’s add the code below to the top of main.py
:
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider
Initialize IconService Object
Next, we need a way of specifying the domain of a node on the ICON network, so our code knows where to query for data. The ICON SDK offers a handy IconService
class for this purpose.
To initialize an IconService
object, add the code below to main.py
:
icon_service = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))
As you can see, IconService
takes an HTTPProvider
object, which we also imported. The HTTPProvider
class then takes two arguments – ICON node domain (https://ctz.solidwallet.io) and API version (3). The node domain can be any ICON P-Rep or citizen node. If you’ve set up a personal citizen node, you can specify the IP address or domain name for your node instead of https://ctz.solidwallet.io
.
If you’re running your own node, you may need to append :9000
to your IP address or domain because ICON nodes communicate over Port 9000.
For example:
- http://35.123.123.123:9000
- http://my-cool-node.com:9000
The https://ctz.solidwallet.io
endpoint doesn’t require :9000
because its been specifically configured to forward external requests to Port 9000 via an internal reverse proxy (e.g. Nginx, HAProxy, etc.). So, unless the endpoint you’re using has configured a reverse proxy, you’ll need to specify the port.
Now that we’ve initialized the IconService
object, let’s use it to query for data about the latest block. To do this, just add the code below to main.py
.
Query the Latest Block
To query the latest block, we’ll use the built-in get_block
method, and supply "latest"
as the argument like so:
block = icon_service.get_block("latest")
print(block)
Your whole main.py
file should look like this now:
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider
icon_service = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))
block = icon_service.get_block("latest")
print(block)
That’s it for now! Save the main.py
file, and switch over to your Terminal. Finally, let’s run the Python script and see what happens. To do this, navigate to the icon-sdk-project
folder and run the command below (make sure you’re still in the Python 3.7.10 virtual environment):
python3 icon_sdk_project/main.py
If you did everything correctly, you should see something like this in your Terminal window:
{'version': '0.5', 'height': 35589816, 'signature': 'W+uY+ODvFIvPy2NDOvhRhEHzToeiM7hDL9bh7WLC7mpy6rXkdZob4giYCccjOUMuajEEpxVAjbNbO1qCUWGN1QA=', 'prev_block_hash': '38a65def3b0ec13f2ccb17d10433ca28d8b8f39bc9f810600e4c437379cd9981', 'merkle_tree_root_hash': '4262dec7a91f3f24380017fcecf7d510889286d4712b041b4d81248a41217991', 'time_stamp': 1623474467785654, 'confirmed_transaction_list': [{'version': 3, 'timestamp': 1623474467785654, 'dataType': 'base', 'data': {'prep': {'irep': '0x21e19e0c9bab2400000', 'rrep': '0x16e', 'totalDelegation': '0x131ba8e5c29b59673253e13', 'value': '0x2abee18c4149729e'}, 'result': {'coveredByFee': '0x565a2d9fe92000', 'coveredByOverIssuedICX': '0x0', 'issue': '0x2a68875ea160529e'}}, 'txHash': '0xb5af3b74ccbc96872eaa27b02a8f68a019f0803fb3d754af91ead66b78d52363'}, {'version': 3, 'from': 'hx5b802623eb53c6a90df9f29e5808596f3c2bf63e', 'to': 'cx9df59cf2c7dc7ae2dbdec4a10b295212595f2378', 'stepLimit': 100000000, 'timestamp': 1623474465294806, 'nid': 1, 'value': 0, 'nonce': 100, 'dataType': 'call', 'data': {'method': 'circle_arb'}, 'signature': 'bdg2sR5aY1yFKq1VI2vnwpADY8WgyZLDVGbOAbXLi1M4X34IcavcDJlfHV9Kq5rsmr+WuyDntqtxF6EF+XyIFQA=', 'txHash': '0x3fb6f6d024ded001da54f0c5166708f0fbd4c80f96901e9bacf5bb43125264e1'}, {'data': {'method': 'vp', 'params': {'_hash': '258d46b8942884f2928bfd365fc14be62c42ad5cb2f32624e80aefe752808919'}}, 'stepLimit': 999999999, 'signature': '7T8AxL1IfcgSnRtc9lL7NU3vCpK4GnOkhgI+S4DfoBUfI2gGaSA40wxmmFWm6Ww8vCMUILr3HV1ZAzW+qZOUzgA=', 'dataType': 'call', 'nid': 1, 'from': 'hx037c73025819e490e9a01a7e954f9b46d89b0245', 'to': 'cx9e3cadcc1a4be3323ea23371b84575abb32703ae', 'version': 3, 'timestamp': 1623474466677000, 'txHash': '0xd6498a5c8e17ecfa2f3bc0ddeb07fa9939319442cd438e407788ca20000876e5'}], 'block_hash': 'cbf2191b14e4ee37e80f927709fea2b1de308bb1349bccf63e50cf934352e83b', 'peer_id': 'hx262afdeda4eba10fe41fa5ef21796ac2bdcc6629', 'next_leader': 'hx262afdeda4eba10fe41fa5ef21796ac2bdcc6629'}
This output contains information about the latest block on the ICON blockchain in Python dictionary format.
Here’s what the output looks like in properly-formatted JSON:
{
"version": "0.5",
"height": 35589816,
"signature": "W+uY+ODvFIvPy2NDOvhRhEHzToeiM7hDL9bh7WLC7mpy6rXkdZob4giYCccjOUMuajEEpxVAjbNbO1qCUWGN1QA=",
"prev_block_hash": "38a65def3b0ec13f2ccb17d10433ca28d8b8f39bc9f810600e4c437379cd9981",
"merkle_tree_root_hash": "4262dec7a91f3f24380017fcecf7d510889286d4712b041b4d81248a41217991",
"time_stamp": 1623474467785654,
"confirmed_transaction_list": [
{
"version": 3,
"timestamp": 1623474467785654,
"dataType": "base",
"data": {
"prep": {
"irep": "0x21e19e0c9bab2400000",
"rrep": "0x16e",
"totalDelegation": "0x131ba8e5c29b59673253e13",
"value": "0x2abee18c4149729e"
},
"result": {
"coveredByFee": "0x565a2d9fe92000",
"coveredByOverIssuedICX": "0x0",
"issue": "0x2a68875ea160529e"
}
},
"txHash": "0xb5af3b74ccbc96872eaa27b02a8f68a019f0803fb3d754af91ead66b78d52363"
},
{
"version": 3,
"from": "hx5b802623eb53c6a90df9f29e5808596f3c2bf63e",
"to": "cx9df59cf2c7dc7ae2dbdec4a10b295212595f2378",
"stepLimit": 100000000,
"timestamp": 1623474465294806,
"nid": 1,
"value": 0,
"nonce": 100,
"dataType": "call",
"data": {
"method": "circle_arb"
},
"signature": "bdg2sR5aY1yFKq1VI2vnwpADY8WgyZLDVGbOAbXLi1M4X34IcavcDJlfHV9Kq5rsmr+WuyDntqtxF6EF+XyIFQA=",
"txHash": "0x3fb6f6d024ded001da54f0c5166708f0fbd4c80f96901e9bacf5bb43125264e1"
},
{
"data": {
"method": "vp",
"params": {
"_hash": "258d46b8942884f2928bfd365fc14be62c42ad5cb2f32624e80aefe752808919"
}
},
"stepLimit": 999999999,
"signature": "7T8AxL1IfcgSnRtc9lL7NU3vCpK4GnOkhgI+S4DfoBUfI2gGaSA40wxmmFWm6Ww8vCMUILr3HV1ZAzW+qZOUzgA=",
"dataType": "call",
"nid": 1,
"from": "hx037c73025819e490e9a01a7e954f9b46d89b0245",
"to": "cx9e3cadcc1a4be3323ea23371b84575abb32703ae",
"version": 3,
"timestamp": 1623474466677000,
"txHash": "0xd6498a5c8e17ecfa2f3bc0ddeb07fa9939319442cd438e407788ca20000876e5"
}
],
"block_hash": "cbf2191b14e4ee37e80f927709fea2b1de308bb1349bccf63e50cf934352e83b",
"peer_id": "hx262afdeda4eba10fe41fa5ef21796ac2bdcc6629",
"next_leader": "hx262afdeda4eba10fe41fa5ef21796ac2bdcc6629"
}
If you weren’t able to get the latest block data, I’d recommend going through the tutorial again to see if you missed any steps. If you’re unable to successfully debug the issue, feel free to reach out to me in the RHIZOME DAO Discord – our exclusive community for ICON contributors.
Conclusion
In this tutorial, you learned about the ICON SDK, how to communicate with ICON nodes, how to set up a local Python development environment, and how to write a basic Python script to query for the latest block data. With this foundational understanding of Python development, you’ll soon be able to build more complex apps that utilize the ICON blockchain. In the next tutorial, we’ll dig deeper into the ICON SDK to learn how to query for transaction result, make transactions, timestamp data, and more.
If you’re interested in following along with this tutorial series, be sure to follow me on Twitter so you can be notified when the next post drops.