Written by Alex de Sousa
ICON 2.0 is a smart contract platform with a unique interoperability proposition: Blockchain Transmission Protocol (BTP). BTP will enable decentralized and trustless interoperability between ICON 2.0 and all blockchain that implement the protocol.
At the time of publishing this article, the full BTP is not yet released. However, some of its features are already available to everyone. One of these features is a websocket to receive realtime updates on either (or both) blocks produced and event logs emitted.
This article will focus on how we can leverage this websocket connection to retrieve quote prices from Balanced Decentralized Exchange by using the Elixir ICON 2.0 SDK I wrote in the past few months.
Without further ado, let's go!
This article assumes you have downloaded icon
either within a new project
using mix new
or using a script e.g:
#/usr/bin/env elixir
Mix.install([
{:icon, "~> 0.1"}
])
# ... rest of the script ...
You can also find the full script for getting sICX/bnUSD quotes here.
When we want to exchange one token for another, we're presented with token pairs. These pairs have a base token, a quote token and, a quote price:
If we, somehow, capture the event logs emitted every time someone exchanges one token for another, then we'll have realtime quote prices for any pair we want. In this article, we'll subscribe to Balanced sICX/bnUSD prices.
Note: sICX is staked ICX token and bnUSD is an algorithmic stablecoin pegged to the dollar called Balanced Dollars.
Balanced is built from several SCOREs (Smart Contract On Reliable Environment) and one of these SCOREs handles all operations related to the decentralized exchange (DEx). The main DEx operation is swapping tokens e.g. we can use the contract to exchange our sICX with bnUSD.
Furthermore, every time someone swaps a token, the SCORE will emit the following event:
Swap(int,Address,Address,Address,Address,Address,int,int,int,int,int,int,int,int,int)
We can query the SCORE's API with Icon.get_score_api/2
and get the meaning of
each input. For that, we'll need the SCORE address, which is
cxa0af3165c08318e988cb30993b3048335b94af6c
in the Mainnet:
dex_score = "cxa0af3165c08318e988cb30993b3048335b94af6c"
identity = Icon.RPC.Identity.new(network_id: :mainnet)
{:ok, api} = Icon.get_score_api(identity, dex_score)
swap_definition =
Enum.find(api, fn %{"name" => name, "type" => type} ->
name == "Swap" and type == "eventlog"
end)
In the variable swap_definition
, we'll have the Swap
event log definition:
%{
"inputs" => [
%{"indexed" => "0x1", "name" => "_id", "type" => "int"},
%{"indexed" => "0x1", "name" => "_baseToken", "type" => "Address"},
%{"name" => "_fromToken", "type" => "Address"},
%{"name" => "_toToken", "type" => "Address"},
%{"name" => "_sender", "type" => "Address"},
%{"name" => "_receiver", "type" => "Address"},
%{"name" => "_fromValue", "type" => "int"},
%{"name" => "_toValue", "type" => "int"},
%{"name" => "_timestamp", "type" => "int"},
%{"name" => "_lpFees", "type" => "int"},
%{"name" => "_balnFees", "type" => "int"},
%{"name" => "_poolBase", "type" => "int"},
%{"name" => "_poolQuote", "type" => "int"},
%{"name" => "_endingPrice", "type" => "int"},
%{"name" => "_effectiveFillPrice", "type" => "int"}
],
"name" => "Swap",
"type" => "eventlog"
}
With the previous definition, we can start making some assumptions about each event input and what things we need to get from them in order to have the price for sICX/bnUSD every time the event is emitted:
_id
is the token pair identifier (indexed
1st position)._baseToken
is the SCORE address of the base token of the pair (indexed
2nd position)._fromToken
is the SCORE address of the token being sold (data
1st
position)._toToken
is the SCORE address of the token being bought (data
2nd
position)._timestamp
is the UNIX epoch timestamp in UTC in microseconds (data
7th
position)._endingPrice
is the quote price after the swap in loop
(data
12th
position).Either partially or fully, we can use the previous inputs to find the quote price we're looking for.
Protip: We can query the pool stats with the DEx function
getPoolStats
and search by ID. This will give us sICX/bnUSD pair has the ID 2. You can check it out yourself by running the following:Icon.call(identity, dex_score, "getPoolStats", %{_id: 2}, call_schema: %{_id: :integer} )
At the time of writing this article, there are 39 pools, but not all of them have a name, so in this case using
_baseToken
,_fromToken
, and_toToken
addresses can help us figure out the actual pair name.
Elixir ICON 2.0 SDK has an
Yggdrasil adapter for ICON's websocket.
Thus we can use an Yggdrasil process to subscribe to the Swap
event. In this
case, the most important part is to define out channel correctly:
defmodule Quotes do
use Yggdrasil
alias Icon.Schema.Types.EventLog
@dex_contract "cxa0af3165c08318e988cb30993b3048335b94af6c"
@sicx_bnusd_id 2
@signature "Swap(int,Address,Address,Address,Address,Address,int,int,int,int,int,int,int,int,int)"
@channel [
adapter: :icon,
name: %{
source: :event,
data: %{
addr: @dex_contract,
event: @signature,
indexed: [@sicx_bnusd_id, nil]
},
from_height: 47_077_000
}
]
def start_link, do: Yggdrasil.start_link(__MODULE__, [@channel])
# ... handle_event/3 definition ...
end
Protip: I added
from_height: 47_077_000
field to the channel'sname
for subscribing to an older block height. This way, we don't need to wait for aSwap
event to happen and we can see some results right away. In general, this is only needed for testing purposes, but it wouldn't be needed if we just want the latest price.
The events we'll receive will have:
_timestamp
in microseconds in the 7th position of data
._endingPrice
in the 12th position of data
(_endingPrice * 10¹⁸
).So we can now define our handle_event/3
callback and finally print our quote
price in the console:
defmodule Quotes do
use Yggdrasil
alias Icon.Schema.Types.EventLog
# ... channel definition ...
@impl Yggdrasil
def handle_event(_channel, %EventLog{} = swap_event, _state) do
[_, _, _, _, _, _, timestamp, _, _, _, _, price, _] = swap_event.data
datetime =
timestamp
|> DateTime.from_unix!(:microsecond)
|> DateTime.to_iso8601()
price = price / 1_000_000_000_000_000_000
IO.puts("[#{datetime}] sICX/bnUSD price: #{price}")
{:ok, nil}
end
end
If we execute this process:
Quotes.start_link()
then we'll get the following output:
[2022-03-07T16:17:15.095799Z] sICX/bnUSD price: 0.6762914917929946
[2022-03-07T16:33:31.390351Z] sICX/bnUSD price: 0.6765223727152583
[2022-03-07T16:19:25.140181Z] sICX/bnUSD price: 0.6763077935623969
[2022-03-07T16:06:10.885982Z] sICX/bnUSD price: 0.6696113966372721
[2022-03-07T16:28:17.210851Z] sICX/bnUSD price: 0.6765180152082558
[2022-03-07T16:27:41.288288Z] sICX/bnUSD price: 0.6764913565725723
... continues ...
Note: You can check out the full script here.
Though BTP is not fully released, we can already start leveraging some of its core features for our own advantage. Given ICON's block time is 3 seconds, this websocket connection gives us a tremendous advantage for building realtime bots for different purposes: from maximizing our yield to securing our favorite NFT latest drop.
ICON 2.0 definitely has a great advantage over many other slow blockchains and I believe its DeFi ecosystem will have a bright future!
Alex de Sousa
Elixir alchemist. Tech enthusiast.