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!
Prerequisites
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.
Quote Prices
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.
Getting the Swap Log
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"
}
Analyzing the Swap Event
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:
-
_idis the token pair identifier (indexed1st position). -
_baseTokenis the SCORE address of the base token of the pair (indexed2nd position). -
_fromTokenis the SCORE address of the token being sold (data1st position). -
_toTokenis the SCORE address of the token being bought (data2nd position). -
_timestampis the UNIX epoch timestamp in UTC in microseconds (data7th position). -
_endingPriceis the quote price after the swap inloop(data12th 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
getPoolStatsand 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_toTokenaddresses can help us figure out the actual pair name.
Realtime Updates
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_000field to the channel'snamefor subscribing to an older block height. This way, we don't need to wait for aSwapevent 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.
Showing the Prices
The events we'll receive will have:
- The
_timestampin microseconds in the 7th position ofdata. - The
_endingPricein the 12th position ofdata(_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.
Conclusion
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!
Cover image by Maxim Hopman





Top comments (0)