Exploring the Terra Blockchain with Python

December 28 2021 · tech python terra defi

Decentralised Finance (DeFi) has crept into a couple of my go-to podcasts; folks I respect are talking about it so I wanted to learn it for myself. In this post I describe the approach I took to learning the Terra Blockchain. We set aside GUIs and Web Applications and instead interact with it programatically through the Python API.

DeFi

What is DeFi? I’m not an expert and I’m not a Twitter shill so I can’t give a succinct answer. Cryptocurrencies such as Bitcoin and Ethereum have been around awhile and have been pretty well litigated across the internet. To me it looks like DeFi will be the next generation of cryptocurrencies. Whether or not they get widely adopted is a separate concern. DeFi systems still run on blockchains but are designed more like platforms. Financial protocols and multiple tokens can operate on them; they’re more than just digital gold. End users own a mixture of stablecoins for day-to-day spending (these are pegged to currencies such as the US Dollar and Euro), and the more volatile crypto token, whatever symbols they have.

I read the book How to DeFi - Advanced to learn some of the concepts. I want to learn about Decentralised Exchanges, Automated Market Makers, Oracles, Cross blockchain protocols, and how Lending, Insurance, Derivatives work on decentralised platforms. The book promises all of this but only gives a few pages introducing the theory of each subject. The majority of the book is examples and implementations of each subject: here are 3 algorithmic stablecoins and how you can purchase them. Being new to the whole DeFi space I learned a bit but on the whole I think better books will be published.

I started looking into Terra, a blockchain that is designed to run a wider DeFi ecosystem. Perhaps crucially it does have an ecosystem, with parties offering lending, high interest savings, liquidity pools, and automated arbitrage systems that any average Joe can buy into. I find the ecosystem fascinating and I’m still in the early days of learning it. Unfortunately most of the community knowledge is trapped within Discord rooms. It’s not on the public web and this makes it difficult to learn.

Setting up Terra Locally

We’ll spin up a local instance of Terra within Docker. This is completely disconnected from the publicly hosted Terra blockchain so we can do whatever we want with it. The Terra folks make this easy by providing LocalTerra, a Docker Compose stack of the Terra blockchain and some associated services.

The repository stores binaries of the server so do a shallow checkout to speed things up.

git clone --depth 1 https://www.github.com/terra-money/LocalTerra

cd LocalTerra

docker-compose up

Running a docker ps we can see all of the services are running.

CONTAINER ID   IMAGE                              COMMAND                  CREATED      STATUS      PORTS                                                                                                                                                     NAMES
969369a641b5   terramoney/fcd:1.0.5               "./entrypoint.sh col…"   8 days ago   Up 8 days                                                                                                                                                             localterra-fcd-collector-1
eff9117d0e0a   terramoney/fcd:1.0.5               "./entrypoint.sh sta…"   8 days ago   Up 8 days   0.0.0.0:3060->3060/tcp, :::3060->3060/tcp                                                                                                                 localterra-fcd-api-1
f4060ba0ee87   terramoney/pseudo-feeder:bombay    "npm run start"          8 days ago   Up 8 days                                                                                                                                                             localterra-oracle-1
1ea77f2ffba6   postgres:12                        "docker-entrypoint.s…"   8 days ago   Up 8 days   5432/tcp                                                                                                                                                  localterra-postgres-1
3998eab83bb2   terramoney/localterra-core:0.5.9   "terrad start"           8 days ago   Up 8 days   0.0.0.0:1317->1317/tcp, :::1317->1317/tcp, 0.0.0.0:9090-9091->9090-9091/tcp, :::9090-9091->9090-9091/tcp, 0.0.0.0:26657->26657/tcp, :::26657->26657/tcp   localterra-terrad-1

The Services are:

  • terramoney/localterra-core:0.5.9 - The Terra Blockchain itself (git).
  • terramoney/pseudo-feeder:bombay - A simple Oracle implementation which issues votes on the blockchain. (git).
  • terramoney/fcd:1.0.5 - Full Client Daemon (FCD). The collector takes block information from the Light Client Daemon (LCD; part of terra-core) and stores it in the PostgreSQL database. The api exposes this data as a REST web service. (git).
  • postgres:12 - FCD data store.

The API

There are two native language bindings for the TerraSDK: terra.py and terra.js. These libraries make HTTP requests to the Light Client Daemon (LCD). The LCD exposes a subset of data provided by the FCD. In any case, both have OpenAPI specs (LCD, FCD) and you can make your own HTTP requests to them outside of the language bindings.

I’ve been experimenting around with the Python SDK and found it OK. The documentation is great and it’s easy to explore the different modules. Error reporting is the drawback. It’s difficult to know why certain requests failed. Instead of an explanation detailing missing fields or incorrect key names you get a cryptic response from the server. This isn’t a limitation of the Python SDK, rather the LCD server and how it responds to requests.

Connecting to the API

Let’s connect to the API using the Python SDK and make a basic request.

from terra_sdk.client.lcd import LCDClient
client = LCDClient(url="http://localhost:1317", chain_id="localterra")
client.tendermint.node_info()['node_info']['network']
localterra

Terra hosts a test network (like a UAT system) which integrates with other services in the Terra ecosystem. Connect to the testnet with chain_id="tequila-0004" and url="https://bombay-lcd.terra.dev/".

Likewise to connect to the real (“Production”) Terra network use chain_id="columbus-5" and url="https://lcd.terra.dev".

Keys, Accounts, Wallets

These terms are used interchangeably and somewhat differently between the Terra ecosystem and the Terra SDKs.

  • Key - Generated using public/private key cryptography. There is the public part (Address) which you can share with anyone, and the password, private key, and mnemonic which you must keep private.
  • Account - Generally refers to the Account Address - the public address of the key. eg. terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v. This can be shared with anyone and identifies your account. Transactions are addressed to account addresses. Think of this like a bank account number.
  • Wallet - In the Terra SDK the Wallet provides utility methods which use your Key to sign transactions. It’s an abstract entity. Within the wider Terra ecosystem the term Wallet generally refers to the Key or Account.

Creating keys is easy. You can map the Python code below to the documentation on creating a new wallet within the Terra Station GUI.

from terra_sdk.key.mnemonic import MnemonicKey

new_key = MnemonicKey()
wallet = client.wallet(new_key)

print(f"Account address: {new_key.acc_address}")
print(f"Private seed phrase: {new_key.mnemonic}")

TerraLocal comes with some pre-defined accounts already filled with coins. For the rest of this exploration we’ll use those accounts instead. The accounts are defined in the config/genesis.json file which you can edit to add your own accounts and predefined state of the blockchain. Querying the balance of an address is done through the Bank module.

client.bank.balance("terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v")
Coins('10000000000000000ueur,1000000000000000000ukrw,
       87439uluna,10000000000000000usdr,10000000000000000uusd')

We see there are five different types of coin in this account balance. Luna is the main token of Terra, the remaining four are stablecoins. A Coin has an amount (eg. 21294) and denomination (eg. uluna). Coins, the return type of the balance call is an iterable of Coin.

Stringly Typed

The terra.py SDK is written with type annotations. This doesn’t escape from the fact that Python is dynamically typed. Since we’re dealing with money we want to be confident that code constructs of coins and wallet addresses are correct. The terra_sdk.core.strings module provides a few utility methods to validate values.

from terra_sdk.core.strings import is_acc_address

assert is_acc_address("terra1awlsyjx032z04mw7dph948mkxzyy3g26ltz78c")
assert is_acc_address("something else") == False

There are a few other validators for addresses and keys. Unfortunately there are no validators for coins so they remain stringly typed. This isn’t a big deal, if you tried to send a coin that didn’t exist you’ll only get a server error.

Sending Money to an Address

We instantiate our keys, create a MsgSend object, sign the transaction with Alice’s key, and then broadcast it.

from terra_sdk.core.bank import MsgSend
from terra_sdk.core.coins import Coins
from terra_sdk.key.mnemonic import MnemonicKey

alice = MnemonicKey(mnemonic="notice oak worry limit wrap speak medal online prefer cluster roof addict wrist behave treat actual wasp year salad speed social layer crew genius")

bob = MnemonicKey(mnemonic="quality vacuum heart guard buzz spike sight swarm shove special gym robust assume sudden deposit grid alcohol choice devote leader tilt noodle tide penalty")

print(f"Alice's balance: {client.bank.balance(alice.acc_address)['uluna']}")
print(f"Bob's balance: {client.bank.balance(bob.acc_address)['uluna']}")

t = client.bank.balance(alice.acc_address)['uluna']
coins = Coins([t - 100000])

msg = MsgSend(from_address=alice.acc_address, to_address=bob.acc_address, amount=coins)

alice_wallet = client.wallet(alice)
bob_wallet = client.wallet(bob)

tx = alice_wallet.create_and_sign_tx(msgs=[msg], memo="Here are some of my uluna")
print(tx)

result = client.tx.broadcast(tx)
print(result)
Alice's balance: 1000000000000000uluna
Bob's balance: 1000000000000000uluna

StdTx(
    msg=[MsgSend(from_address='terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v', to_address='terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp', 
    amount=Coins('999999999900000uluna'))], 
    fee=StdFee(gas=55706, amount=Coins()), 
    signatures=[
        StdSignature(
            signature='l0QcniMxw8/kcDq6hkRL8Eu3C/ChNgj7B/PiF3SawBsCV2jKtV0F78SPAUyy3bmwFHIUbyCAN79/RVq708wgsQ==', 
            pub_key=PublicKey(type='tendermint/PubKeySecp256k1', 
            value='AjszqFJDRAYbEjZMuiD+ChqzbUSGq/RRu3zr0R6iJB5b'))], 
    memo='Here are some of my uluna')

BlockTxBroadcastResult(
    height=0, 
    txhash='7BD2710BAD123771C034906B5236778AE195B87D4A75F8E1A9BD37045760F0AA', raw_log='insufficient fees; got: "", 
    required: "10585uaud,10585ucad,7799uchf,54592ucny,6964ueur,6128ugbp,606082uinr,911908ujpy,9457208ukrw,632uluna,23873977umnt,5846usdr,69633usek,11142usgd,257362uthb,8356uusd" = "10585uaud,10585ucad,7799uchf,54592ucny,6964ueur,6128ugbp,606082uinr,911908ujpy,9457208ukrw,632uluna,23873977umnt,5846usdr,69633usek,11142usgd,257362uthb,8356uusd"(gas) +""(stability): insufficient fee', 
    gas_wanted=55706, 
    gas_used=1054, 
    logs=None, 
    code=13, 
    codespace='sdk')

But we get an error

insufficient fee', gas_wanted=55706, gas_used=1054

Committing transactions to the Terra Blockchain requires a small fee.

Gas and Tax

We broadcast the MsgSend transaction and asked Terra Validator nodes (a single node since we’re running on LocalTerra) to validate the transaction. This is a compute operation and those validators want a fee for processing our transaction and committing it to the blockchain. The fee is made up of two parts: gas and tax.

The gas fee is determined by: Amount(gas) * Price(gas) where

  • Amount(gas) is immutable based on the amount of compute done in a transaction. MsgSend transactions are relatively simple. You could imagine more complex smart contract transactions requiring more gas. Terra automatically calculates this for you, but you can do it manually through the API client.tx.estimate_fee(sender=alice.acc_address, msgs=[msg]).
  • Price(gas) is set by validators. You can get the updated gas prices from /txs/gas_prices endpoint. There’s currently no method in terra.py which fetches these for us. For the example below I’ve hardcoded the gas_prices value in. In the real world you could request the above URL and use the appropriate value.

Some transactions will also require other fees. See the Terra Documentation for more detail.

By adding gas to the create_and_sign_tx call a validator will now commit the transaction to the Terra blockchain.

tx2 = tx = alice_wallet.create_and_sign_tx(
    msgs=[msg], 
    memo="Here are some of my uluna", 
    fee_denoms=["uluna"], 
    gas_prices="0.15uluna", 
    gas_adjustment="1.5")

result2 = client.tx.broadcast(tx2)
BlockTxBroadcastResult(
    height=162, 
    txhash='D23CC0C8E18CF00929DBA428BFFB0A245E8AAD4C2DE86684ED220BE20FA127E5', 
    raw_log='[
        {"events":[
            {"type":"coin_received","attributes":[
                {"key":"receiver","value":"terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp"},{"key":"amount","value":"999999999900000uluna"}]},
            {"type":"coin_spent","attributes":[
                {"key":"spender","value":"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"},{"key":"amount","value":"999999999900000uluna"}]},
            {"type":"message","attributes":[
                {"key":"action","value":"/cosmos.bank.v1beta1.MsgSend"},
                {"key":"sender","value":"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"},{"key":"module","value":"bank"}]},
            {"type":"transfer","attributes":[
                {"key":"recipient","value":"terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp"},{"key":"sender","value":"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"},{"key":"amount","value":"999999999900000uluna"}]}]}]', 
    gas_wanted=83739, 
    gas_used=69334, 
    logs=[
        TxLog(
            msg_index=0, 
            log=None, 
            events=[
                {'type': 'coin_received', 'attributes': [
                    {'key': 'receiver', 'value': 'terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp'}, {'key': 'amount', 'value': '999999999900000uluna'}]},
                {'type': 'coin_spent', 'attributes': [
                    {'key': 'spender', 'value': 'terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'}, {'key': 'amount', 'value': '999999999900000uluna'}]},
                {'type': 'message', 'attributes': [
                    {'key': 'action', 'value': '/cosmos.bank.v1beta1.MsgSend'}, 
                    {'key': 'sender', 'value': 'terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'}, {'key': 'module', 'value': 'bank'}]},
                {'type': 'transfer', 'attributes': [
                    {'key': 'recipient', 'value': 'terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp'}, {'key': 'sender', 'value': 'terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'}, {'key': 'amount', 'value': '999999999900000uluna'}]}], 
            events_by_type={
                'coin_received': {
                    'receiver': ['terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp'], 
                    'amount': ['999999999900000uluna']}, 
                    'coin_spent': {
                        'spender': ['terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'], 
                        'amount': ['999999999900000uluna']}, 
                    'message': {
                        'action': ['/cosmos.bank.v1beta1.MsgSend'], 
                        'sender': ['terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'], 
                        'module': ['bank']}, 
                    'transfer': {
                        'recipient': ['terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp'], 
                        'sender': ['terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'], 
                        'amount': ['999999999900000uluna']}})], 
    code=None, 
    codespace=None)

Success! We can now look at the account balances and see the uluna has been transferred.

print(f"Alice's balance: {client.bank.balance(alice.acc_address)['uluna']}")
print(f"Bob's balance: {client.bank.balance(bob.acc_address)['uluna']}")
Alice's balance: 87439uluna
Bob's balance: 1999999999900000uluna

Searching for Transactions by Account

Another way of confirming the transaction would be to search the blockchain for transactions involving Alice’s account address.

client.tx.search({"message.sender": "terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"})
{'total_count': '1',
'count': '1',
'page_number': '1',
'page_total': '1',
'limit': '30',
'txs': [{
'height': '26',
'txhash': 'D23CC0C8E18CF00929DBA428BFFB0A245E8AAD4C2DE86684ED220BE20FA127E5',
'data': '0A1E0A1C2F636F736D6F732E62616E6B2E763162657461312E4D736753656E64',
'raw_log': '[{
    "events":[
        {"type":"coin_received","attributes":[
            {"key":"receiver","value":"terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp"},
            {"key":"amount","value":"999999999900000uluna"}]},
            {"type":"coin_spent","attributes":[
                {"key":"spender","value":"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"},
                {"key":"amount","value":"999999999900000uluna"}]},
            {"type":"message","attributes":[
                {"key":"action","value":"/cosmos.bank.v1beta1.MsgSend"},
                {"key":"sender","value":"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"},
                {"key":"module","value":"bank"}]},
            {"type":"transfer","attributes":[
                {"key":"recipient","value":"terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp"},
                {"key":"sender","value":"terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v"},
                {"key":"amount","value":"999999999900000uluna"}]}
        ]
    }]',
'logs': [{
    'events': [
            {'type': 'coin_received', 'attributes': [
            {'key': 'receiver', 'value': 'terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp'},
            {'key': 'amount', 'value': '999999999900000uluna'}]},
            {'type': 'coin_spent', 'attributes': [
                {'key': 'spender', 'value': 'terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'},
                {'key': 'amount', 'value': '999999999900000uluna'}]},
            {'type': 'message', 'attributes': [
                {'key': 'action', 'value': '/cosmos.bank.v1beta1.MsgSend'},
                {'key': 'sender', 'value': 'terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'},
                {'key': 'module', 'value': 'bank'}]},
            {'type': 'transfer', 'attributes': [
                {'key': 'recipient', 'value': 'terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp'},
                {'key': 'sender', 'value': 'terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v'},
                {'key': 'amount', 'value': '999999999900000uluna'}]}
        ]
    }],
'gas_wanted': '83739',
'gas_used': '69334',
'tx': {'type': 'core/StdTx',
    'value': 
        {'msg': [
            {'type': 'bank/MsgSend',
            'value': {
                'from_address': 'terra1x46rqay4d3cssq8gxxvqz8xt6nwlz4td20k38v',
                'to_address': 'terra17lmam6zguazs5q5u6z5mmx76uj63gldnse2pdp',
                'amount': [{'denom': 'uluna', 'amount': '999999999900000'}]}}
        ],
    'fee': {'amount': [{'denom': 'uluna', 'amount': '12561'}], 'gas': '83739'},
    'signatures': [{'pub_key': {'type': 'tendermint/PubKeySecp256k1',
        'value': 'AjszqFJDRAYbEjZMuiD+ChqzbUSGq/RRu3zr0R6iJB5b'},
        'signature': '1r4PO4M2T0My2M9o2B6VlekD6mX1jDrjjY5rtnd0brB92XBusFVwUzRhzgTbUGRDwpVzbwEo50W40NnzRI5xsw=='}],
    'memo': 'Here are some of my uluna',
    'timeout_height': '0'}},
'timestamp': '2022-01-08T10:03:38Z'}]}

Conclusion

Within a minute we’ve opened an account and executed a transaction which sends money to another account. What I’ve covered here is generalisable to most other blockchains. In the future I want to explore more about the DeFi aspects of the Terra protocol and how we can programatically interact with DeFi Services built upon it.

References


Related Posts