feat: Adding Mobula price source (#9971)

* Adding Mobula as data provider

* push Mobula source

* add chain setup

* remove useless params

* remove useless functions

* add mobula price history provider

* adding mobula history url

* add possibility to fetch_market_data_for_token_addresses with Mobula on source.ex

* update chain => chain_id

* mix format

* mix credo

* removed useless alias Helper & Chain

* Adding "Mobula" to cspell.json

* Adding Mobula to config_helper

* Fix dialyzer

* mix format

* Set Mobula as a default source if the EXCHANGE_RATES_COINGECKO_API_KEY is not set

* fix compilation error on config_helper

* Remove Mobula fallback on config_helper exchange_rates_source

* Update apps/explorer/lib/explorer/exchange_rates/source/mobula.ex

Co-authored-by: Fedor Ivanov <ivnfedor@gmail.com>

* Update mobula.ex

* add Mobula to exchange_rates_market_cap_source

* Adding secondary_coin support

* EXCHANGE_RATES_MOBULA_PLATFORM_ID into EXCHANGE_RATES_MOBULA_CHAIN_ID

* should fix mix credo

* adding mobula secondary id env

* update env in runtime.exs

* Push requests

---------

Co-authored-by: Fedor Ivanov <ivnfedor@gmail.com>
pull/10216/head
NBMXyeu 6 months ago committed by GitHub
parent e29f3e2a52
commit b8730cdfe0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 7
      apps/explorer/lib/explorer/exchange_rates/source.ex
  2. 174
      apps/explorer/lib/explorer/exchange_rates/source/mobula.ex
  3. 54
      apps/explorer/lib/explorer/market/history/source/market_cap/mobula.ex
  4. 48
      apps/explorer/lib/explorer/market/history/source/price/mobula.ex
  5. 14
      config/config_helper.exs
  6. 7
      config/runtime.exs
  7. 1
      cspell.json

@ -35,10 +35,9 @@ defmodule Explorer.ExchangeRates.Source do
@spec fetch_market_data_for_token_addresses([Hash.Address.t()]) :: @spec fetch_market_data_for_token_addresses([Hash.Address.t()]) ::
{:ok, %{Hash.Address.t() => %{fiat_value: float() | nil, circulating_market_cap: float() | nil}}} {:ok, %{Hash.Address.t() => %{fiat_value: float() | nil, circulating_market_cap: float() | nil}}}
| {:error, any} | {:error, any}
def fetch_market_data_for_token_addresses(address_hashes) do def fetch_market_data_for_token_addresses(address_hashes, source \\ exchange_rates_source()) do
source_url = CoinGecko.source_url(address_hashes) source_url = source.source_url(address_hashes)
headers = CoinGecko.headers() fetch_exchange_rates_request(source, source_url, source.headers())
fetch_exchange_rates_request(CoinGecko, source_url, headers)
end end
@spec fetch_token_hashes_with_market_data :: {:ok, [String.t()]} | {:error, any} @spec fetch_token_hashes_with_market_data :: {:ok, [String.t()]} | {:error, any}

@ -0,0 +1,174 @@
defmodule Explorer.ExchangeRates.Source.Mobula do
@moduledoc """
Adapter for fetching exchange rates from https://mobula.io
"""
require Logger
alias Explorer.ExchangeRates.{Source, Token}
import Source, only: [to_decimal: 1]
@behaviour Source
@impl Source
def format_data(%{"data" => %{"market_cap" => _} = market_data}) do
current_price = market_data["price"]
image_url = market_data["logo"]
id = market_data["symbol"]
btc_value =
if Application.get_env(:explorer, Explorer.ExchangeRates)[:fetch_btc_value], do: get_btc_value(id, market_data)
[
%Token{
available_supply: to_decimal(market_data["circulating_supply"]),
total_supply: to_decimal(market_data["total_supply"]) || to_decimal(market_data["circulating_supply"]),
btc_value: btc_value,
id: id,
last_updated: nil,
market_cap_usd: to_decimal(market_data["market_cap"]),
tvl_usd: nil,
name: market_data["name"],
symbol: String.upcase(market_data["symbol"]),
usd_value: current_price,
volume_24h_usd: to_decimal(market_data["volume"]),
image_url: image_url
}
]
end
@impl Source
def format_data(%{"data" => data}) do
data
|> Enum.reduce(%{}, fn
{address_hash_string, market_data}, acc ->
case Explorer.Chain.Hash.Address.cast(address_hash_string) do
{:ok, address_hash} ->
acc
|> Map.put(address_hash, %{
fiat_value: Map.get(market_data, "price"),
circulating_market_cap: Map.get(market_data, "market_cap"),
volume_24h: Map.get(market_data, "volume")
})
_ ->
acc
end
_, acc ->
acc
end)
end
@impl Source
def format_data(_), do: []
@impl Source
def source_url do
"#{base_url()}/market/data?asset=#{Explorer.coin()}"
end
@impl Source
def source_url(token_addresses) when is_list(token_addresses) do
joined_addresses = token_addresses |> Enum.map_join(",", &to_string/1)
"#{base_url()}/market/multi-data?blockchains=#{chain()}&assets=#{joined_addresses}"
end
@impl Source
def source_url(input) do
symbol = input
"#{base_url()}/market/data&asset=#{symbol}"
end
@spec secondary_history_source_url() :: String.t()
def secondary_history_source_url do
id = config(:secondary_coin_id)
if id, do: "#{base_url()}/market/history?asset=#{id}", else: nil
end
@spec history_source_url() :: String.t()
def history_source_url do
"#{base_url()}/market/history?asset=#{Explorer.coin()}"
end
@spec history_url(non_neg_integer(), boolean()) :: String.t()
def history_url(previous_days, secondary_coin?) do
now = DateTime.utc_now()
date_days_ago = DateTime.add(now, -previous_days, :day)
timestamp_ms = DateTime.to_unix(date_days_ago) * 1000
source_url = if secondary_coin?, do: secondary_history_source_url(), else: history_source_url()
"#{source_url}&from=#{timestamp_ms}"
end
@spec market_cap_history_url(non_neg_integer()) :: String.t()
def market_cap_history_url(previous_days) do
now = DateTime.utc_now()
date_days_ago = DateTime.add(now, -previous_days, :day)
timestamp_ms = DateTime.to_unix(date_days_ago) * 1000
"#{history_source_url()}&from=#{timestamp_ms}&period=5"
end
@impl Source
def headers do
if config(:api_key) do
[{"Authorization", "#{config(:api_key)}"}]
else
[]
end
end
defp get_current_price(market_data) do
if market_data["price"] do
to_decimal(market_data["price"])
else
1
end
end
defp get_btc_value(id, market_data) do
case get_btc_price() do
{:ok, price} ->
btc_price = to_decimal(price)
current_price = get_current_price(market_data)
if id != "btc" && current_price && btc_price do
Decimal.div(current_price, btc_price)
else
1
end
_ ->
1
end
end
defp chain do
config(:platform) || "ethereum"
end
defp base_url do
config(:base_url)
end
defp get_btc_price do
url = "#{base_url()}/market/data?asset=Bitcoin"
case Source.http_request(url, headers()) do
{:ok, %{"price" => current_price}} ->
{:ok, current_price}
resp ->
resp
end
end
@spec config(atom()) :: term
defp config(key) do
Application.get_env(:explorer, __MODULE__, [])[key]
end
end

@ -0,0 +1,54 @@
defmodule Explorer.Market.History.Source.MarketCap.Mobula do
@moduledoc """
Adapter for fetching current market from Mobula.
The current market is fetched for the configured coin. You can specify a
different coin by changing the targeted coin.
# In config.exs
config :explorer, coin: "POA"
"""
require Logger
alias Explorer.ExchangeRates.Source
alias Explorer.ExchangeRates.Source.Mobula, as: ExchangeRatesSourceMobula
alias Explorer.Market.History.Source.MarketCap, as: SourceMarketCap
alias Explorer.Market.History.Source.Price.CryptoCompare
@behaviour SourceMarketCap
@impl SourceMarketCap
def fetch_market_cap(previous_days) do
url = ExchangeRatesSourceMobula.market_cap_history_url(previous_days)
case Source.http_request(url, ExchangeRatesSourceMobula.headers()) do
{:ok, data} ->
result =
data
|> format_data()
{:ok, result}
_ ->
:error
end
end
@spec format_data(term()) :: SourceMarketCap.record() | nil
defp format_data(nil), do: nil
defp format_data(data) do
market_caps = data["data"]["market_cap_history"]
for [date, market_cap] <- market_caps do
date = Decimal.to_integer(Decimal.round(Decimal.from_float(date / 1000)))
%{
market_cap: Decimal.new(to_string(market_cap)),
date: CryptoCompare.date(date)
}
end
end
end

@ -0,0 +1,48 @@
defmodule Explorer.Market.History.Source.Price.Mobula do
@moduledoc """
Adapter for fetching current market from Mobula.
"""
require Logger
alias Explorer.ExchangeRates.Source
alias Explorer.ExchangeRates.Source.Mobula, as: ExchangeRatesSourceMobula
alias Explorer.Market.History.Source.Price, as: SourcePrice
alias Explorer.Market.History.Source.Price.CryptoCompare
@behaviour SourcePrice
@impl SourcePrice
def fetch_price_history(previous_days, secondary_coin? \\ false) do
url = ExchangeRatesSourceMobula.history_url(previous_days, secondary_coin?)
case Source.http_request(url, ExchangeRatesSourceMobula.headers()) do
{:ok, data} ->
result =
data
|> format_data(secondary_coin?)
{:ok, result}
_ ->
:error
end
end
@spec format_data(term(), boolean()) :: SourcePrice.record() | nil
defp format_data(nil, _), do: nil
defp format_data(data, secondary_coin?) do
prices = data["data"]["price_history"]
for [date, price] <- prices do
date = Decimal.to_integer(Decimal.round(Decimal.from_float(date / 1000)))
%{
closing_price: Decimal.new(to_string(price)),
date: CryptoCompare.date(date),
opening_price: Decimal.new(to_string(price)),
secondary_coin: secondary_coin?
}
end
end
end

@ -170,20 +170,22 @@ defmodule ConfigHelper do
end end
end end
@spec exchange_rates_source() :: Source.CoinGecko | Source.CoinMarketCap @spec exchange_rates_source() :: Source.CoinGecko | Source.CoinMarketCap | Source.Mobula
def exchange_rates_source do def exchange_rates_source do
case System.get_env("EXCHANGE_RATES_MARKET_CAP_SOURCE") do case System.get_env("EXCHANGE_RATES_MARKET_CAP_SOURCE") do
"coin_gecko" -> Source.CoinGecko "coin_gecko" -> Source.CoinGecko
"coin_market_cap" -> Source.CoinMarketCap "coin_market_cap" -> Source.CoinMarketCap
"mobula" -> Source.Mobula
_ -> Source.CoinGecko _ -> Source.CoinGecko
end end
end end
@spec exchange_rates_market_cap_source() :: MarketCap.CoinGecko | MarketCap.CoinMarketCap @spec exchange_rates_market_cap_source() :: MarketCap.CoinGecko | MarketCap.CoinMarketCap | MarketCap.Mobula
def exchange_rates_market_cap_source do def exchange_rates_market_cap_source do
case System.get_env("EXCHANGE_RATES_MARKET_CAP_SOURCE") do case System.get_env("EXCHANGE_RATES_MARKET_CAP_SOURCE") do
"coin_gecko" -> MarketCap.CoinGecko "coin_gecko" -> MarketCap.CoinGecko
"coin_market_cap" -> MarketCap.CoinMarketCap "coin_market_cap" -> MarketCap.CoinMarketCap
"mobula" -> MarketCap.Mobula
_ -> MarketCap.CoinGecko _ -> MarketCap.CoinGecko
end end
end end
@ -196,26 +198,30 @@ defmodule ConfigHelper do
end end
end end
@spec exchange_rates_price_source() :: Price.CoinGecko | Price.CoinMarketCap | Price.CryptoCompare @spec exchange_rates_price_source() :: Price.CoinGecko | Price.CoinMarketCap | Price.CryptoCompare | Price.Mobula
def exchange_rates_price_source do def exchange_rates_price_source do
case System.get_env("EXCHANGE_RATES_PRICE_SOURCE") do case System.get_env("EXCHANGE_RATES_PRICE_SOURCE") do
"coin_gecko" -> Price.CoinGecko "coin_gecko" -> Price.CoinGecko
"coin_market_cap" -> Price.CoinMarketCap "coin_market_cap" -> Price.CoinMarketCap
"crypto_compare" -> Price.CryptoCompare "crypto_compare" -> Price.CryptoCompare
"mobula" -> Price.Mobula
_ -> Price.CryptoCompare _ -> Price.CryptoCompare
end end
end end
@spec exchange_rates_secondary_coin_price_source() :: Price.CoinGecko | Price.CoinMarketCap | Price.CryptoCompare @spec exchange_rates_secondary_coin_price_source() ::
Price.CoinGecko | Price.CoinMarketCap | Price.CryptoCompare | Price.Mobula
def exchange_rates_secondary_coin_price_source do def exchange_rates_secondary_coin_price_source do
cmc_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINMARKETCAP_SECONDARY_COIN_ID") cmc_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINMARKETCAP_SECONDARY_COIN_ID")
cg_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINGECKO_SECONDARY_COIN_ID") cg_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINGECKO_SECONDARY_COIN_ID")
cc_secondary_coin_symbol = System.get_env("EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL") cc_secondary_coin_symbol = System.get_env("EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL")
mobula_secondary_coin_id = System.get_env("EXCHANGE_RATES_MOBULA_SECONDARY_COIN_ID")
cond do cond do
cg_secondary_coin_id && cg_secondary_coin_id !== "" -> Price.CoinGecko cg_secondary_coin_id && cg_secondary_coin_id !== "" -> Price.CoinGecko
cmc_secondary_coin_id && cmc_secondary_coin_id !== "" -> Price.CoinMarketCap cmc_secondary_coin_id && cmc_secondary_coin_id !== "" -> Price.CoinMarketCap
cc_secondary_coin_symbol && cc_secondary_coin_symbol !== "" -> Price.CryptoCompare cc_secondary_coin_symbol && cc_secondary_coin_symbol !== "" -> Price.CryptoCompare
mobula_secondary_coin_id && mobula_secondary_coin_id !== "" -> Price.Mobula
true -> Price.CryptoCompare true -> Price.CryptoCompare
end end
end end

@ -367,6 +367,13 @@ config :explorer, Explorer.ExchangeRates.Source.CoinGecko,
coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"), coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"),
secondary_coin_id: cg_secondary_coin_id secondary_coin_id: cg_secondary_coin_id
config :explorer, Explorer.ExchangeRates.Source.Mobula,
platform: System.get_env("EXCHANGE_RATES_MOBULA_CHAIN_ID"),
base_url: System.get_env("EXCHANGE_RATES_MOBULA_BASE_URL", "https://api.mobula.io/api/1"),
api_key: System.get_env("EXCHANGE_RATES_MOBULA_API_KEY"),
coin_id: System.get_env("EXCHANGE_RATES_MOBULA_COIN_ID"),
secondary_coin_id: System.get_env("EXCHANGE_RATES_MOBULA_SECONDARY_COIN_ID")
config :explorer, Explorer.ExchangeRates.Source.DefiLlama, coin_id: System.get_env("EXCHANGE_RATES_DEFILLAMA_COIN_ID") config :explorer, Explorer.ExchangeRates.Source.DefiLlama, coin_id: System.get_env("EXCHANGE_RATES_DEFILLAMA_COIN_ID")
cc_secondary_coin_symbol = System.get_env("EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL") cc_secondary_coin_symbol = System.get_env("EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL")

@ -54,6 +54,7 @@
"LUKSO", "LUKSO",
"Limegreen", "Limegreen",
"MARKETCAP", "MARKETCAP",
"Mobula",
"MDWW", "MDWW",
"Mainnets", "Mainnets",
"Mendonça", "Mendonça",

Loading…
Cancel
Save