Balanced for Developers | Set up your development environment

Learn how to set up a Python development environment and create a simple Balanced alert bot.

Balanced for Developers | Set up your development environment

Balanced is powered by a collection of smart contracts on the ICON blockchain. Not only are the Balanced smart contracts open source (anyone can contribute code to them), they're also operationally transparent. This means anyone can query and interact with the Balanced smart contracts at any time.

At its core, Balanced is a platform that lets you deposit collateral (ICX) and borrow "mint" a stablecoin (bnUSD). The amount of bnUSD you can borrow (also known as LTV or loan-to-value) depends on the USD value of the deposited ICX. Because ICX is not a pegged asset, the LTV of a Balanced loan constantly fluctuates in response to the ICX price. If the price of ICX drops too much, Balanced will automatically liquidate collateral to make sure the bnUSD is properly backed.

With this in mind, it's easy to see why it's important to always be aware of the status of your position on Balanced. To keep track of your LTV, you can sign in to Balanced at any time. However, constantly signing in to Balanced just to check the status of your position can be time-consuming and repetitive. Instead, it's more efficient to let a bot check your LTV, and only alert you if your position is in jeopardy.

In this tutorial series, you'll learn how create a variety of bots to interact with the Balanced smart contracts. In this first tutorial, we'll cover how to set up a Python development environment to start developing Balanced bots.

IMPORTANT: Make sure to install Python 3.7+ before you proceed with this tutorial. If you don't have a Python version manager, get pyenv and use it to install the latest release of Python 3.7, 3.8, or 3.9.


Install a dependency manager

First, you'll need to install a dependency manager – an organizational tool that helps you keep track of project dependencies. If you're working on multiple Python projects simultaneously, a dependency manager will help you spend more time coding, and less time troubleshooting package-related issues.

For dependency management, we recommend Poetry. Poetry is a tool that keeps track of your packages on a per-project basis, and even lets you create project-specific virtual environments to execute your code.

Learn how to install Poetry.


Create a new project

After you've installed Poetry, create a new project using this command:

poetry new my-balanced-project

This will create a new folder named my-balanced-project in your working directory. Within that folder, you'll see a few additional items:

  • README.rst - A README file for your project
  • pyproject.toml - A configuration file for your project
  • my_balanced_project - The source code for your project
  • tests - Tests for your project

Now that your new project has been created, install the iconsdk package with these commands:

# Launch a virtual environment
poetry shell

# Add the iconsdk package to your project
poetry add iconsdk

# Install all dependencies
poetry install

How to create a Balanced bot

At this point, you should be ready to start coding. To make sure everything is working correctly, let's create a simple bot to fetch information about your position on Balanced.

First, navigate to my-balanced-project/my_balanced_project and create a file named main.py – this file is where the bot code will go.

Import packages from iconsdk

In main.py, add this code to import a few modules from the iconsdk package, and create an instance of IconService:

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))
  • CallBuilder is an interface for querying smart contracts
  • IconService provides APIs for interacting with an ICON node
  • HTTPProvider is used to specify an ICON node to connect to

In the example, the HTTPProvider URL is set to https://ctz.solidwallet.io and API version is set to 3. The ctz.solidwallet.io domain is a public node endpoint operated by ICON Foundation. If you want to use another ICON node, feel free to change the URL to something else.

Query the Balanced Loans contract

Data about Balanced loan positions are stored in the Balanced Loans smart contract. As you can see on the ICON tracker, the contract address for the Balanced Loans contract is cx66d4d90f5f113eba575bf793570135f9b10cece1.

We'll need this contract address later, so let's go ahead and set it to a variable:

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

To obtain the information we need, we'll need to make a few smart contract calls. To simplify the process, let's create a utility function that can be used to query the ICON blockchain:

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

def call(to: str, method: str, params: dict = {}):
    call = CallBuilder()\
        .to(to)\
        .method(method)\
        .params(params)\
        .build()
    result = ICON_SERVICE.call(call)
    return result

The call() function takes arguments – to, method, and params.

  • to is the contract address
  • method is the method to call on the contract
  • params are values should be passed to the contract as part of the call

Within the call() function, you can see that call is an instance of CallBuilder. After the call has been constructed, ICON_SERVICE (an instance of IconService) makes the contract call before returning result.

Next, let's create another function to fetch information about a Balanced position:

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

def call(to: str, method: str, params: dict = {}):
    call = CallBuilder()\
        .to(to)\
        .method(method)\
        .params(params)\
        .build()
    result = ICON_SERVICE.call(call)
    return result

def query_position(address: str):
    params = {"_owner": address}
    position = call(BALANCED_LOANS_CONTRACT, "getAccountPositions", params)
    return position

The query_position() function accepts an address argument, which is the ICX address to query. If you're looking for information about your own position, you would specify your ICX address when calling the function.

Within the query_position() function, position is a contract call to the getAccountPositions method on the Balanaced Loans smart contract. The ICX address represented by address is mapped to the _owner key, and the resulting dictionary is also passed into the call() function as params.

Now, let's create a main() function to test out the bot:

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

def call(to: str, method: str, params: dict = {}):
    call = CallBuilder()\
        .to(to)\
        .method(method)\
        .params(params)\
        .build()
    result = ICON_SERVICE.call(call)
    return result

def query_position(address: str):
    params = {"_owner": address}
    position = call(BALANCED_LOANS_CONTRACT, "getAccountPositions", params)
    return position

def main():
    address = "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113"
    position = query_position(address)
    print(position)
    
if __name__ == "__main__":
    main()

For this example, we've selected a random ICX address with an open position on Balanced. Feel free to replace address with another ICX address of your choosing.

Now, run the script in your virtual environment:

python my_balanced_project/main.py

You should see a response that looks something like this:

{'pos_id': '0xaa3', 'created': '0x5c142ed631584', 'address': 'hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113', 'snap_id': '0x8d', 'snaps_length': '0x11', 'last_snap': '0x8d', 'first day': '0x6', 'assets': {'sICX': '0xd0b48c82a540f38f07b', 'bnUSD': '0x42c98a5250ebf8fc62c'}, 'total_debt': '0x28b95457a7fa08325a4', 'collateral': '0xd8dd0cad4b49ccbfbbb', 'ratio': '0x49e6f09b5da24a6d', 'standing': 'Mining'}

Congratulations! If you see this response, it means you've successfully made a smart contract call to the Balanced Loans contract. If you see an error that you can't solve, join the official Balanced Discord where some community developers may be able to help you solve the problem.

Let's convert the Python dictionary into JSON format to make it a little easier to read:

{
    "pos_id": "0xaa3",
    "created": "0x5c142ed631584",
    "address": "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113",
    "snap_id": "0x8d",
    "snaps_length": "0x11",
    "last_snap": "0x8d",
    "first day": "0x6",
    "assets": {
        "sICX": "0xd0b48c82a540f38f07b",
        "bnUSD": "0x42c98a5250ebf8fc62c"
    },
    "total_debt": "0x28b95457a7fa08325a4",
    "collateral": "0xd8dd0cad4b49ccbfbbb",
    "ratio": "0x49e6f09b5da24a6d",
    "standing": "Mining"
}

As you can see, the result of getAccountPositions contains a lot of useful information about the queried position – some of which is not visible in the Balanced UI. This is because the UI is optimized for user interaction, so it only shows the information that a typical user needs. For use cases that require more information like developing bots or integrating Balanced into other dapps, having access to more data via direct smart contract calls is immensely useful.

If you look at a few of the items in the dictionary, you'll see strings that start with 0x. For example, "pos_id": "0x20" and "total_debt": "0x868d2f986e69ec1dd". These strings are actually hexidecimal representations of integers. For our use case, we need human-readable decimal integers, so let's write a function to process the dictionary and perform the necessary conversions:

def format_position(position):
    for k, v in position.items():
        if isinstance(v, str) and v[:2] == "0x":
            position[k] = int(v, 16)
        if isinstance(v, dict) and k == "assets":
            for asset, amount in position["assets"].items():
                position["assets"][asset] = int(amount, 16)

Now, let's update the query_position() function to include the new format_position() function:

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

def call(to: str, method: str, params: dict = {}):
    call = CallBuilder()\
        .to(to)\
        .method(method)\
        .params(params)\
        .build()
    result = ICON_SERVICE.call(call)
    return result
    
def format_position(position):
    for k, v in position.items():
        if isinstance(v, str) and v[:2] == "0x":
            position[k] = int(v, 16)
        if isinstance(v, dict) and k == "assets":
            for asset, amount in position["assets"].items():
                position["assets"][asset] = int(amount, 16)

def query_position(address: str):
    params = {"_owner": address}
    position = call(BALANCED_LOANS_CONTRACT, "getAccountPositions", params)
    format_position(position)
    return position

def main():
    address = "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113"
    position = query_position(address)
    print(position)
    
if __name__ == "__main__":
    main()

If you run the script again, you should see a response like this:

{
    "pos_id": 2723,
    "created": 1619868078249348,
    "address": "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113",
    "snap_id": 141,
    "snaps_length": 17,
    "last_snap": 141,
    "first day": 6,
    "assets": {
        "sICX": 61598922950422786797691,
        "bnUSD": 19712121909131997070892
    },
    "total_debt": 12027573131873541069932,
    "collateral": 64006800263075808934843,
    "ratio": 5321672091392674634,
    "standing": "Mining"
}

That looks much better! For the purposes of this tutorial (building an LTV alert bot), the only parameter that matters is ratio. On Balanced, positions are liquidated when the collateralization ratio falls below 1.5 or 150%. Thus, by monitoring the ratio of a position, a bot can send an alert if ratio falls below a certain threshold.

In the response above, ratio has a value of 5321672091392674634. The reason this number is so large is because they represent 18 decimal places of information. In Python and other programming languages, it's difficult to perform precise calculations on floating point numbers (floats) with decimal points. Thus, it's often better to perform calculations on large integers, and account for the decimal places when displaying information only.

If we divide 5321672091392674634 by 10 ** 18, the result is 5.321672091392674, which translates into a collateralization ratio of ~5.32 or 532%.

Create an alert condition

Armed with information about our Balanced position, let's create an alert condition. Since liquidation occurs at 150%, let's define 200% as the alert threshold for our bot.

The code below checks whether position["ratio"] is less than 2 * 10 ** 18, which is equivalent to 200%. If you wanted to set the threshold to 175%, you would use 1.75 * 10 ** 18 instead.

if position["ratio"] < 2 * 10 ** 18 and position["ratio"] > 0:
    print(f"Warning, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%.")
else:
    print(f"No problem, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%.")

If position["ratio"] is under 200%, the code prints a warning message. If position is over 200% collateralized, the code prints a "no problem" message. This snippet of code should go in the main() function of your script like so:

from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

def call(to: str, method: str, params: dict = {}):
    call = CallBuilder()\
        .to(to)\
        .method(method)\
        .params(params)\
        .build()
    result = ICON_SERVICE.call(call)
    return result
    
def format_position(position):
    for k, v in position.items():
        if isinstance(v, str) and v[:2] == "0x":
            position[k] = int(v, 16)
        if isinstance(v, dict) and k == "assets":
            for asset, amount in position["assets"].items():
                position["assets"][asset] = int(amount, 16)

def query_position(address: str):
    params = {"_owner": address}
    position = call(BALANCED_LOANS_CONTRACT, "getAccountPositions", params)
    format_position(position)
    return position

def main():
    address = "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113"
    position = query_position(address)
    if position["ratio"] < 2 * 10 ** 18 and position["ratio"] > 0:
        print(f"Warning, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%.")
    else:
        print(f"No problem, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%.")
    
if __name__ == "__main__":
    main()

Send a Discord notification

Now that our script is capable of fetching information about a Balanced position and determining when to send an alert, let's walk through how to send the actual notification. There are a variety of ways to dispatch a message like SMS, Discord, email, and more. For the purposes of this tutorial, we'll use Discord because it's free and easy to set up.

Before you can send a Discord notification, you'll first need to set up a Discord server. If you're not sure how to create a server, check out the official Discord guide. After creating the server, go ahead and create a channel and name it whatever you'd like. For this example, we've using the #balanced-position-alerts channel in our Discord server.

Discord channel for receiving Balanced position alerts.

Now, click on the gear (settings) icon next to the channel name:

Click on the gear icon.

Under Integration, click Create Webhook:

Click Create Webhook.

Feel free to change the webhook name from Spidey Bot to something else – this is the name your bot will use when sending messages to the channel. Next, click Copy Webhook URL, then click Save Changes to finalize the webhook creation.

Create a Discord webhook URL.

If you paste the webhook URL into a text editor, it should look something like this:

https://discord.com/api/webhooks/886840847465873448/WPdxa05o1Jsxou-qO5z0TkI_mWE2tJTw5J5g1WPmhwF_XKe_ljMFOOHxYfNRZAZjvhmd

You can think of a Discord webhook URL as a message relayer. If you send a message to the webhook URL, Discord will go ahead and forward that to the correct channel – in this case, our #balanced-position-alerts channel.

Let's create another utility function to send a message to our Discord channel via the webhook URL:

def send_discord_notification(message: str):
    payload = { "content": message }
    requests.post(DISCORD_WEBHOOK_URL, json=payload)

The send_discord_notification function accepts a message and makes a POST request to DISCORD_WEBHOOK_URL using the requests package, which is already included as a dependency with the iconsdk package. With that said, you'll need to import the requests package and set DISCORD_WEBHOOK_URL to your webhook URL.

Now, let's add send_discord_notification() to the main() function:

def main():
    address = "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113"
    position = query_position(address)
    if position["ratio"] < 2 * 10 ** 18 and position["ratio"] > 0:
        message = f"Warning, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%."
        print(message)
        send_discord_notification(message)
    else:
        message = f"No problem, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%."
        print(message)
        send_discord_notification(message)

At this point, your script should look like this:

import requests
from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/886840847465873448/WPdxa05o1Jsxou-qO5z0TkI_mWE2tJTw5J5g1WPmhwF_XKe_ljMFOOHxYfNRZAZjvhmd"

def call(to: str, method: str, params: dict = {}):
    call = CallBuilder()\
        .to(to)\
        .method(method)\
        .params(params)\
        .build()
    result = ICON_SERVICE.call(call)
    return result
    
def format_position(position):
    for k, v in position.items():
        if isinstance(v, str) and v[:2] == "0x":
            position[k] = int(v, 16)
        if isinstance(v, dict) and k == "assets":
            for asset, amount in position["assets"].items():
                position["assets"][asset] = int(amount, 16)

def query_position(address: str):
    params = {"_owner": address}
    position = call(BALANCED_LOANS_CONTRACT, "getAccountPositions", params)
    format_position(position)
    return position

def send_discord_notification(message: str):
    payload = { "content": message }
    requests.post(DISCORD_WEBHOOK_URL, json=payload)

def main():
    address = "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113"
    position = query_position(address)
    if position["ratio"] < 2 * 10 ** 18 and position["ratio"] > 0:
        message = f"Warning, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%."
        print(message)
        send_discord_notification(message)
    else:
        message = f"No problem, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%."
        print(message)
        send_discord_notification(message)
    
if __name__ == "__main__":
    main()

Now if you run the script, you should also receive a Discord notification.

Balanced position alerts in Discord.

Now that the script is working, let's remove the notification sending function from the alert condition when collateralization is over 200% – that way notifications will only be sent when collateralization is below 200%.

Finally, let's import the time module and wrap the code within the main() function in a while loop so it runs forever with 10 seconds between attempts.

Your final script should look like this:

import requests
import time
from iconsdk.builder.call_builder import CallBuilder
from iconsdk.icon_service import IconService
from iconsdk.providers.http_provider import HTTPProvider

ICON_SERVICE = IconService(HTTPProvider("https://ctz.solidwallet.io", 3))

BALANCED_LOANS_CONTRACT = "cx66d4d90f5f113eba575bf793570135f9b10cece1"

DISCORD_WEBHOOK_URL = "https://discord.com/api/webhooks/886840847465873448/WPdxa05o1Jsxou-qO5z0TkI_mWE2tJTw5J5g1WPmhwF_XKe_ljMFOOHxYfNRZAZjvhmd"

def call(to: str, method: str, params: dict = {}):
    call = CallBuilder()\
        .to(to)\
        .method(method)\
        .params(params)\
        .build()
    result = ICON_SERVICE.call(call)
    return result
    
def format_position(position):
    for k, v in position.items():
        if isinstance(v, str) and v[:2] == "0x":
            position[k] = int(v, 16)
        if isinstance(v, dict) and k == "assets":
            for asset, amount in position["assets"].items():
                position["assets"][asset] = int(amount, 16)

def query_position(address: str):
    params = {"_owner": address}
    position = call(BALANCED_LOANS_CONTRACT, "getAccountPositions", params)
    format_position(position)
    return position

def send_discord_notification(message: str):
    payload = { "content": message }
    requests.post(DISCORD_WEBHOOK_URL, json=payload)

def main():
    address = "hxcfe4b63c9870ee8e6e8757eeda27bfe2b5a34113"

    while True:
        position = query_position(address)
        if position["ratio"] < 2 * 10 ** 18 and position["ratio"] > 0:
            message = f"Warning, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%."
            print(message)
            send_discord_notification(message)
        else:
            message = f"No problem, your collateralization is {round(position['ratio'] / 10 ** 16, 2)}%."
            print(message)
        time.sleep(10)

if __name__ == "__main__":
    main()

Summary

In this tutorial, you learned how to set up a local Python development environment, interact with the Balanced Loans smart contract, and create a simple bot to alert you if your position collateralization falls below 200%. In the next tutorial, we'll build on the concepts introduced above to create a bot that triggers loan rebalancing to stabilize the bnUSD peg.

If you have any questions or comments, feel free to join the discussion in the Balanced Discord.