diff --git a/apps/explorer/lib/explorer/exchange_rates/source.ex b/apps/explorer/lib/explorer/exchange_rates/source.ex index 0c7a092965..52b846b3a7 100644 --- a/apps/explorer/lib/explorer/exchange_rates/source.ex +++ b/apps/explorer/lib/explorer/exchange_rates/source.ex @@ -35,10 +35,9 @@ defmodule Explorer.ExchangeRates.Source do @spec fetch_market_data_for_token_addresses([Hash.Address.t()]) :: {:ok, %{Hash.Address.t() => %{fiat_value: float() | nil, circulating_market_cap: float() | nil}}} | {:error, any} - def fetch_market_data_for_token_addresses(address_hashes) do - source_url = CoinGecko.source_url(address_hashes) - headers = CoinGecko.headers() - fetch_exchange_rates_request(CoinGecko, source_url, headers) + def fetch_market_data_for_token_addresses(address_hashes, source \\ exchange_rates_source()) do + source_url = source.source_url(address_hashes) + fetch_exchange_rates_request(source, source_url, source.headers()) end @spec fetch_token_hashes_with_market_data :: {:ok, [String.t()]} | {:error, any} diff --git a/apps/explorer/lib/explorer/exchange_rates/source/mobula.ex b/apps/explorer/lib/explorer/exchange_rates/source/mobula.ex new file mode 100644 index 0000000000..9e5484c56f --- /dev/null +++ b/apps/explorer/lib/explorer/exchange_rates/source/mobula.ex @@ -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 diff --git a/apps/explorer/lib/explorer/market/history/source/market_cap/mobula.ex b/apps/explorer/lib/explorer/market/history/source/market_cap/mobula.ex new file mode 100644 index 0000000000..f5195e58de --- /dev/null +++ b/apps/explorer/lib/explorer/market/history/source/market_cap/mobula.ex @@ -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 diff --git a/apps/explorer/lib/explorer/market/history/source/price/mobula.ex b/apps/explorer/lib/explorer/market/history/source/price/mobula.ex new file mode 100644 index 0000000000..1f799777ad --- /dev/null +++ b/apps/explorer/lib/explorer/market/history/source/price/mobula.ex @@ -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 diff --git a/config/config_helper.exs b/config/config_helper.exs index bccd722b8b..d75f9aaba7 100644 --- a/config/config_helper.exs +++ b/config/config_helper.exs @@ -170,20 +170,22 @@ defmodule ConfigHelper do 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 case System.get_env("EXCHANGE_RATES_MARKET_CAP_SOURCE") do "coin_gecko" -> Source.CoinGecko "coin_market_cap" -> Source.CoinMarketCap + "mobula" -> Source.Mobula _ -> Source.CoinGecko 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 case System.get_env("EXCHANGE_RATES_MARKET_CAP_SOURCE") do "coin_gecko" -> MarketCap.CoinGecko "coin_market_cap" -> MarketCap.CoinMarketCap + "mobula" -> MarketCap.Mobula _ -> MarketCap.CoinGecko end end @@ -196,26 +198,30 @@ defmodule ConfigHelper do 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 case System.get_env("EXCHANGE_RATES_PRICE_SOURCE") do "coin_gecko" -> Price.CoinGecko "coin_market_cap" -> Price.CoinMarketCap "crypto_compare" -> Price.CryptoCompare + "mobula" -> Price.Mobula _ -> Price.CryptoCompare 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 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") 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 cg_secondary_coin_id && cg_secondary_coin_id !== "" -> Price.CoinGecko cmc_secondary_coin_id && cmc_secondary_coin_id !== "" -> Price.CoinMarketCap cc_secondary_coin_symbol && cc_secondary_coin_symbol !== "" -> Price.CryptoCompare + mobula_secondary_coin_id && mobula_secondary_coin_id !== "" -> Price.Mobula true -> Price.CryptoCompare end end diff --git a/config/runtime.exs b/config/runtime.exs index cd2072166e..f00a02c4f9 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -367,6 +367,13 @@ config :explorer, Explorer.ExchangeRates.Source.CoinGecko, coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_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") cc_secondary_coin_symbol = System.get_env("EXCHANGE_RATES_CRYPTOCOMPARE_SECONDARY_COIN_SYMBOL") diff --git a/cspell.json b/cspell.json index 0f0cf285ed..aa1edcf326 100644 --- a/cspell.json +++ b/cspell.json @@ -54,6 +54,7 @@ "LUKSO", "Limegreen", "MARKETCAP", + "Mobula", "MDWW", "Mainnets", "Mendonça",