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