Skip to content

Commit

Permalink
add route for aeswap's tvl
Browse files Browse the repository at this point in the history
  • Loading branch information
bchamagne committed Aug 23, 2024
1 parent 3844c11 commit 0b621a4
Show file tree
Hide file tree
Showing 23 changed files with 455 additions and 91 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ Available cryptoassets in this API:
- usdc: 3408
- eure: 20920
- usdt: 825
- bnb: 1839
- ... more later

Providers requested:
Expand Down Expand Up @@ -54,6 +53,7 @@ The result is a list of pairs `[timestamp, value]`.
**The values are cached for a few minutes.**

Available intervals:

- "hourly"
- "daily"
- "weekly"
Expand Down
65 changes: 46 additions & 19 deletions config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,54 @@ import Config
# to minimize any confusion that may arise from assets that share identical tickers/symbols.
config :archethic_fas,
api_port: 3000,
ucids: [
# uco
6887,
# matic
3890,
# bnb
1839,
# btc
1,
# eth
1027,
# usdc
3408,
# monerium eure
20920,
# usdt
825,
# bnb
1839
coins: [
%{
ucid: 1,
coingecko: "bitcoin",
archethic: "00002CEC79D588D5CDD24331968BEF0A9CFE8B1B03B8AEFC4454726DEF79AA10C125"
},
%{
ucid: 825,
coingecko: "tether",
archethic: nil
},
%{
ucid: 1027,
coingecko: "ethereum",
archethic: "0000457EACA7FBAA96DB4A8D506A0B69684F546166FBF3C55391B1461907EFA58EAF"
},
%{
ucid: 1839,
coingecko: "binancecoin",
archethic: nil
},
%{
ucid: 3408,
coingecko: "usd-coin",
archethic: nil
},
%{
ucid: 3890,
coingecko: "matic-network",
archethic: nil
},
%{
ucid: 6887,
coingecko: "archethic",
archethic: "UCO"
},
%{
ucid: 20920,
coingecko: "monerium-eur-money",
archethic: "00005751A05BA007E7E2518DEA171DBBD67B0527C637232F923830C39BFF9E8F159A"
}
]

config :archethic_fas, ArchethicFAS.AESwap,
router_address: "000077CEC9D9DBC0183CAF843CBB4828A932BB1457E382AC83B31AD6F9755DD50FFC"

config :archethic_fas, ArchethicFAS.ArchethicApi, endpoint: "mainnet.archethic.net"

config :archethic_fas, ArchethicFAS.QuotesLatest.Scheduler, schedule_interval: :timer.minutes(5)

import_config("#{Mix.env()}.exs")
72 changes: 72 additions & 0 deletions lib/archethic_fas/aeswap.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
defmodule ArchethicFAS.AESwap do
require Logger

alias __MODULE__.Cache
alias ArchethicFAS.ArchethicApi
alias ArchethicFAS.Quotes
alias ArchethicFAS.UCID

@spec get_tvl() :: {:ok, float()} | :error
def get_tvl() do
case Cache.get_tvl() do
{:ok, tvl} ->
{:ok, tvl}

{:error, :not_cached} ->
fetch_tvl()
|> tap(fn
{:ok, tvl} -> Cache.set_tvl(tvl)
_ -> :ignore
end)
end
end

@spec fetch_tvl() :: {:ok, float()} | :error
def fetch_tvl() do
{:ok, quotes} = UCID.list() |> Quotes.get_latest()

case get_pools() do
{:ok, pools} ->
tvl =
Enum.reduce(pools, 0, fn pool, acc ->
[token1, token2] = pool["tokens"] |> String.split("/")

case {UCID.from_archethic(token1), UCID.from_archethic(token2)} do
{ucid1, ucid2} when is_integer(ucid1) and is_integer(ucid2) ->
{:ok, info} = get_pool_infos(pool["address"])

acc +
info["token1"]["reserve"] * quotes[ucid1] +
info["token2"]["reserve"] * quotes[ucid2]

{ucid, _} when is_integer(ucid) ->
{:ok, info} = get_pool_infos(pool["address"])
acc + info["token1"]["reserve"] * quotes[ucid] * 2

{_, ucid} when is_integer(ucid) ->
{:ok, info} = get_pool_infos(pool["address"])
acc + info["token2"]["reserve"] * quotes[ucid] * 2

_ ->
acc
end
end)

{:ok, tvl}

err ->
Logger.error("Fetch TVL err: #{inspect(err)}")
:error
end
end

defp get_pools() do
Application.fetch_env!(:archethic_fas, __MODULE__)
|> Keyword.fetch!(:router_address)
|> ArchethicApi.call_contract_function("get_pool_list", [])
end

defp get_pool_infos(pool_address) do
ArchethicApi.call_contract_function(pool_address, "get_pool_infos", [])
end
end
40 changes: 40 additions & 0 deletions lib/archethic_fas/aeswap/cache.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
defmodule ArchethicFAS.AESwap.Cache do
@moduledoc """
"""

use GenServer
require Logger

@vsn 1
@table :archethic_fas_aeswap_cache

@spec start_link(Keyword.t()) :: GenServer.on_start()
def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end

@spec get_tvl() :: {:ok, float()} | {:error, :not_cached}
def get_tvl() do
case :ets.lookup(@table, :latest) do
[{:latest, value}] ->
{:ok, value}

[] ->
{:error, :not_cached}
end
end

@spec set_tvl(float()) :: :ok
def set_tvl(tvl) when is_float(tvl) do
:ets.insert(@table, {:latest, tvl})
:ets.insert(@table, {DateTime.utc_now(), tvl})
:ok
end

def init([]) do
:ets.new(@table, [:named_table, :set, :public])

{:ok, %{tasks: %{}}}
end
end
34 changes: 34 additions & 0 deletions lib/archethic_fas/aeswap/scheduler.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
defmodule ArchethicFAS.AESwap.Scheduler do
@moduledoc """
Hydrate the cache once per hour
"""

alias ArchethicFAS.AESwap
alias ArchethicFAS.AESwap.Cache
require Logger

use GenServer
@vsn 1

def start_link(opts) do
GenServer.start_link(__MODULE__, opts, name: __MODULE__)
end

def init([]) do
:timer.send_interval(:timer.hours(1), :tick)

# first request after a few seconds
# to give some time for the quotes to be ready
Process.send_after(self(), :tick, 2000)

{:ok, :no_state}
end

def handle_info(:tick, state) do
Logger.debug("hydrate AESwap TVL")
{:ok, tvl} = AESwap.fetch_tvl()
:ok = Cache.set_tvl(tvl)

{:noreply, state}
end
end
21 changes: 21 additions & 0 deletions lib/archethic_fas/aeswap/supervisor.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
defmodule ArchethicFAS.AESwap.Supervisor do
@moduledoc false

alias ArchethicFAS.AESwap.Cache
alias ArchethicFAS.AESwap.Scheduler

use Supervisor

def start_link(args \\ []) do
Supervisor.start_link(__MODULE__, args, name: __MODULE__)
end

def init(_args) do
children = [
Cache,
Scheduler
]

Supervisor.init(children, strategy: :one_for_one)
end
end
3 changes: 2 additions & 1 deletion lib/archethic_fas/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@ defmodule ArchethicFAS.Application do

children = [
{Plug.Cowboy, scheme: :http, plug: ArchethicFAS.Router, options: [port: port]},
ArchethicFAS.Quotes.Supervisor
ArchethicFAS.Quotes.Supervisor,
ArchethicFAS.AESwap.Supervisor
]

# See https://hexdocs.pm/elixir/Supervisor.html
Expand Down
116 changes: 116 additions & 0 deletions lib/archethic_fas/archethic_api.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
defmodule ArchethicFAS.ArchethicApi do
@spec call_contract_function(String.t(), String.t(), list()) :: {:ok, any()} | {:error, atom()}
def call_contract_function(address, function, args) do
body =
%{
"jsonrpc" => "2.0",
"method" => "contract_fun",
"id" => 1,
"params" => %{
"contract" => address,
"function" => function,
"args" => args
}
}
|> Jason.encode!()

path = "/api/rpc"
opts = [transport_opts: conf(:transport_opts, [])]

with {:ok, conn} <- Mint.HTTP.connect(:https, conf(:endpoint), 443, opts),
{:ok, conn, _} <- Mint.HTTP.request(conn, "POST", path, headers(), body),
{:ok, conn, %{body: body, status: 200}} <- stream_response(conn),
{:ok, _} <- Mint.HTTP.close(conn),
{:ok, response} <- Jason.decode(body) do
cond do
Map.has_key?(response, "result") ->
{:ok, response["result"]}

Map.has_key?(response, "error") ->
{:error, response["error"]}

true ->
{:error, "non json-rpc response"}
end
else
# connect errors
{:error, error = %Mint.TransportError{}} ->
{:error, Exception.message(error)}

{:error, error = %Mint.HTTPError{}} ->
{:error, Exception.message(error)}

# request errors
{:error, _conn, error = %Mint.TransportError{}} ->
{:error, Exception.message(error)}

{:error, _conn, error = %Mint.HTTPError{}} ->
{:error, Exception.message(error)}

# stream errors
{:error, _conn, error = %Mint.TransportError{}, _responses} ->
{:error, Exception.message(error)}

{:error, _conn, error = %Mint.HTTPError{}, _responses} ->
{:error, Exception.message(error)}

# jason
{:error, %Jason.DecodeError{}} ->
{:error, "provider returned an invalid json"}
end
end

defp headers() do
[
{"Accept", "application/json"},
{"Content-Type", "application/json"}
]
end

defp conf(key) do
case conf(key, nil) do
nil -> raise "Missing config #{key}"
value -> value
end
end

defp conf(key, default) do
config = Application.get_env(:archethic_fas, __MODULE__, [])
Keyword.get(config, key, default)
end

defp stream_response(conn, acc0 \\ %{status: 0, data: [], done: false}) do
receive do
message ->
case Mint.HTTP.stream(conn, message) do
:unknown ->
IO.inspect(message)

{:ok, conn, responses} ->
acc2 =
Enum.reduce(responses, acc0, fn
{:status, _, status}, acc1 ->
%{acc1 | status: status}

{:data, _, data}, acc1 ->
%{acc1 | data: acc1.data ++ [data]}

{:headers, _, _}, acc1 ->
acc1

{:done, _}, acc1 ->
%{acc1 | done: true}
end)

if acc2.done do
{:ok, conn, %{status: acc2.status, body: Enum.join(acc2.data)}}
else
stream_response(conn, acc2)
end

{:error, conn, reason, responses} ->
{:error, conn, reason, responses}
end
end
end
end
Loading

0 comments on commit 0b621a4

Please sign in to comment.