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
parent
e29f3e2a52
commit
b8730cdfe0
@ -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 |
Loading…
Reference in new issue