feat: Add icon for secondary coin (#10241)

pull/10604/head
Maxim Filonov 2 months ago committed by GitHub
parent 8101bfc808
commit 4fbbb3f4b1
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex
  2. 4
      apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs
  3. 2
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs
  4. 3
      apps/explorer/config/config.exs
  5. 84
      apps/explorer/lib/explorer/exchange_rates/exchange_rates.ex
  6. 21
      apps/explorer/lib/explorer/exchange_rates/source.ex
  7. 1
      apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
  8. 12
      apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex
  9. 5
      apps/explorer/lib/explorer/exchange_rates/source/defillama.ex
  10. 6
      apps/explorer/lib/explorer/exchange_rates/source/mobula.ex
  11. 2
      apps/explorer/lib/explorer/market/history/source/price/coin_market_cap.ex
  12. 12
      apps/explorer/lib/explorer/market/market.ex
  13. 2
      apps/explorer/lib/explorer/market/market_history.ex
  14. 24
      apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs
  15. 3
      apps/explorer/test/support/fakes/no_op_source.ex
  16. 3
      apps/explorer/test/support/fakes/one_coin_source.ex
  17. 12
      config/config_helper.exs
  18. 23
      config/runtime.exs
  19. 2
      config/runtime/test.exs
  20. 2
      docker-compose/envs/common-blockscout.env

@ -65,6 +65,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do
"total_transactions" => TransactionCache.estimated_count() |> to_string(),
"average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(),
"coin_image" => exchange_rate.image_url,
"secondary_coin_image" => secondary_coin_exchange_rate.image_url,
"coin_price" => exchange_rate.usd_value,
"coin_price_change_percentage" => coin_price_change,
"secondary_coin_price" => secondary_coin_exchange_rate.usd_value,

@ -44,7 +44,7 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
describe "new_rate" do
test "subscribed user is notified", %{token: token} do
ExchangeRates.handle_info({nil, {:ok, [token]}}, %{})
ExchangeRates.handle_info({nil, {:ok, false, [token]}}, %{})
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})
Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})
@ -64,7 +64,7 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
end
test "subscribed user is notified with market history", %{token: token} do
ExchangeRates.handle_info({nil, {:ok, [token]}}, %{})
ExchangeRates.handle_info({nil, {:ok, false, [token]}}, %{})
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})
Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})

@ -177,7 +177,7 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
image_url: nil
}
ExchangeRates.handle_info({nil, {:ok, [eth]}}, %{})
ExchangeRates.handle_info({nil, {:ok, false, [eth]}}, %{})
params = %{
"module" => "stats",

@ -85,9 +85,6 @@ config :explorer, Explorer.Chain.Cache.TransactionActionTokensData, enabled: tru
config :explorer, Explorer.Chain.Cache.TransactionActionUniswapPools, enabled: true
config :explorer, Explorer.ExchangeRates,
cache_period: ConfigHelper.parse_time_env_var("CACHE_EXCHANGE_RATES_PERIOD", "10m")
config :explorer, Explorer.ExchangeRates.TokenExchangeRates, enabled: true
config :explorer, Explorer.Counters.TokenHoldersCounter,

@ -13,7 +13,6 @@ defmodule Explorer.ExchangeRates do
alias Explorer.Market
alias Explorer.ExchangeRates.{Source, Token}
@interval Application.compile_env(:explorer, __MODULE__)[:cache_period]
@table_name :exchange_rates
@impl GenServer
@ -27,23 +26,30 @@ defmodule Explorer.ExchangeRates do
# Callback for successful fetch
@impl GenServer
def handle_info({_ref, {:ok, tokens}}, state) do
def handle_info({_ref, {:ok, secondary_coin?, [coin]}}, state) do
if store() == :ets do
records = Enum.map(tokens, &Token.to_tuple/1)
:ets.insert(table_name(), records)
:ets.insert(table_name(), {secondary_coin?, coin})
end
broadcast_event(:exchange_rate)
unless secondary_coin? do
schedule_next_consolidation()
end
{:noreply, state}
end
# Callback for errored fetch
@impl GenServer
def handle_info({_ref, {:error, reason}}, state) do
Logger.warning(fn -> "Failed to get exchange rates with reason '#{reason}'." end)
def handle_info({_ref, {:error, secondary_coin?, reason}}, state) do
Logger.warning(fn ->
"Failed to get #{if secondary_coin?, do: "secondary", else: ""} exchange rates with reason '#{reason}'."
end)
schedule_next_consolidation()
unless secondary_coin? do
schedule_next_consolidation()
end
{:noreply, state}
end
@ -57,7 +63,6 @@ defmodule Explorer.ExchangeRates do
@impl GenServer
def init(_) do
send(self(), :update)
:timer.send_interval(@interval, :update)
table_opts = [
:set,
@ -79,7 +84,29 @@ defmodule Explorer.ExchangeRates do
end
defp schedule_next_consolidation do
Process.send_after(self(), :update, :timer.minutes(1))
if consolidate?() do
Process.send_after(self(), :update, cache_period())
end
end
@spec get_coin_exchange_rate() :: Token.t() | nil
def get_coin_exchange_rate do
if store() == :ets && enabled?() do
case :ets.lookup(table_name(), false) do
[{_, coin} | _] -> coin
_ -> nil
end
end
end
@spec get_secondary_coin_exchange_rate() :: Token.t() | nil
def get_secondary_coin_exchange_rate do
if store() == :ets && enabled?() do
case :ets.lookup(table_name(), true) do
[{_, coin} | _] -> coin
_ -> nil
end
end
end
@doc """
@ -123,12 +150,29 @@ defmodule Explorer.ExchangeRates do
@spec fetch_rates :: Task.t()
defp fetch_rates do
Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fn ->
Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fetch_rates_task(false))
if secondary_coin_enabled?() do
Task.Supervisor.async_nolink(Explorer.MarketTaskSupervisor, fetch_rates_task(true))
end
end
defp fetch_rates_task(false) do
fn ->
case Source.fetch_exchange_rates() do
{:ok, tokens} -> {:ok, add_coin_info_from_db(tokens)}
err -> err
{:ok, coin} -> {:ok, false, add_coin_info_from_db(coin)}
{:error, reason} -> {:error, false, reason}
end
end)
end
end
defp fetch_rates_task(true) do
fn ->
case Source.fetch_secondary_exchange_rates() do
{:ok, coin} -> {:ok, true, coin}
{:error, reason} -> {:error, true, reason}
end
end
end
defp add_coin_info_from_db(tokens) do
@ -150,7 +194,7 @@ defmodule Explorer.ExchangeRates do
defp list_from_store(:ets) do
table_name()
|> :ets.tab2list()
|> Enum.map(&Token.from_tuple/1)
|> Enum.map(fn {_, coin} -> coin end)
|> Enum.sort_by(fn %Token{symbol: symbol} -> symbol end)
end
@ -160,7 +204,19 @@ defmodule Explorer.ExchangeRates do
config(:store) || :ets
end
defp cache_period do
Application.get_env(:explorer, __MODULE__, [])[:cache_period]
end
defp enabled? do
Application.get_env(:explorer, __MODULE__, [])[:enabled] == true
end
defp consolidate? do
Application.get_env(:explorer, __MODULE__, [])[:enable_consolidation]
end
defp secondary_coin_enabled? do
Application.get_env(:explorer, __MODULE__, [])[:secondary_coin_enabled]
end
end

@ -18,6 +18,17 @@ defmodule Explorer.ExchangeRates.Source do
fetch_exchange_rates_request(source, source_url, source.headers())
end
@doc """
Fetches exchange rates for secondary coin.
"""
@spec fetch_secondary_exchange_rates(module) :: {:ok, [Token.t()]} | {:error, any}
def fetch_secondary_exchange_rates(source \\ secondary_exchange_rates_source()) do
case source.secondary_source_url() do
:ignore -> {:error, "Secondary coin fetching is not implemented for source #{inspect(source)}"}
source_url -> fetch_exchange_rates_request(source, source_url, source.headers())
end
end
@spec fetch_exchange_rates_for_token(String.t()) :: {:ok, [Token.t()]} | {:error, any}
def fetch_exchange_rates_for_token(symbol) do
source_url = CoinGecko.source_url(symbol)
@ -105,6 +116,11 @@ defmodule Explorer.ExchangeRates.Source do
@callback source_url(String.t()) :: String.t() | :ignore
@doc """
Url for the api to query to get the market info for secondary coin.
"""
@callback secondary_source_url :: String.t() | nil | :ignore
@callback headers :: [any]
def headers do
@ -128,6 +144,11 @@ defmodule Explorer.ExchangeRates.Source do
config(:source) || Explorer.ExchangeRates.Source.CoinGecko
end
@spec secondary_exchange_rates_source() :: module()
defp secondary_exchange_rates_source do
config(:secondary_coin_source) || exchange_rates_source()
end
@spec config(atom()) :: term
defp config(key) do
Application.get_env(:explorer, __MODULE__, [])[key]

@ -106,6 +106,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
"#{source_url}/market_chart?#{URI.encode_query(query_params)}"
end
@impl Source
def secondary_source_url do
id = config(:secondary_coin_id)

@ -71,12 +71,6 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCap do
end
end
@impl Source
def source_url(:secondary_coin) do
coin_id = config(:secondary_coin_id)
if coin_id, do: "#{api_quotes_latest_url()}?id=#{coin_id}&CMC_PRO_API_KEY=#{api_key()}", else: nil
end
@impl Source
def source_url(input) do
case Chain.Hash.Address.cast(input) do
@ -93,6 +87,12 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCap do
end
end
@impl Source
def secondary_source_url do
coin_id = config(:secondary_coin_id)
if coin_id, do: "#{api_quotes_latest_url()}?id=#{coin_id}&CMC_PRO_API_KEY=#{api_key()}", else: nil
end
@impl Source
def headers do
[]

@ -26,6 +26,11 @@ defmodule Explorer.ExchangeRates.Source.DefiLlama do
""
end
@impl Source
def secondary_source_url do
:ignore
end
@impl Source
def headers do
[]

@ -81,6 +81,12 @@ defmodule Explorer.ExchangeRates.Source.Mobula do
"#{base_url()}/market/data&asset=#{symbol}"
end
@impl Source
def secondary_source_url do
id = config(:secondary_coin_id)
if id, do: "#{base_url()}/market/data?asset=#{id}", else: nil
end
@spec secondary_history_source_url() :: String.t()
def secondary_history_source_url do
id = config(:secondary_coin_id)

@ -13,7 +13,7 @@ defmodule Explorer.Market.History.Source.Price.CoinMarketCap do
def fetch_price_history(_previous_days \\ nil, secondary_coin? \\ false) do
url =
if secondary_coin?,
do: ExchangeRatesSourceCoinMarketCap.source_url(:secondary_coin),
do: ExchangeRatesSourceCoinMarketCap.secondary_source_url(),
else: ExchangeRatesSourceCoinMarketCap.source_url()
if url do

@ -55,7 +55,7 @@ defmodule Explorer.Market do
"""
@spec get_coin_exchange_rate() :: Token.t()
def get_coin_exchange_rate do
get_native_coin_exchange_rate_from_cache() || get_native_coin_exchange_rate_from_db() || Token.null()
ExchangeRates.get_coin_exchange_rate() || get_native_coin_exchange_rate_from_db() || Token.null()
end
@doc """
@ -63,7 +63,7 @@ defmodule Explorer.Market do
"""
@spec get_secondary_coin_exchange_rate() :: Token.t()
def get_secondary_coin_exchange_rate do
get_native_coin_exchange_rate_from_db(true)
ExchangeRates.get_secondary_coin_exchange_rate() || get_native_coin_exchange_rate_from_db(true)
end
@doc false
@ -146,12 +146,4 @@ defmodule Explorer.Market do
market_history.closing_price == 0
)
end
@spec get_native_coin_exchange_rate_from_cache :: Token.t() | nil
defp get_native_coin_exchange_rate_from_cache do
case ExchangeRates.list() do
[native_coin] -> native_coin
_ -> nil
end
end
end

@ -22,7 +22,7 @@ defmodule Explorer.Market.MarketHistory do
field(:opening_price, :decimal)
field(:market_cap, :decimal)
field(:tvl, :decimal)
field(:secondary_coin, :boolean)
field(:secondary_coin, :boolean, default: false)
end
@doc """

@ -64,7 +64,7 @@ defmodule Explorer.ExchangeRatesTest do
set_mox_global()
assert {:noreply, ^state} = ExchangeRates.handle_info(:update, state)
assert_receive {_, {:ok, [%Token{}]}}
assert_receive {_, {:ok, false, [%Token{}]}}
end
describe "ticker fetch task" do
@ -89,14 +89,11 @@ defmodule Explorer.ExchangeRatesTest do
image_url: nil
}
expected_symbol = expected_token.symbol
expected_tuple = Token.to_tuple(expected_token)
state = %{}
assert {:noreply, ^state} = ExchangeRates.handle_info({nil, {:ok, [expected_token]}}, state)
assert {:noreply, ^state} = ExchangeRates.handle_info({nil, {:ok, false, [expected_token]}}, state)
assert [^expected_tuple] = :ets.lookup(ExchangeRates.table_name(), expected_symbol)
assert [false: ^expected_token] = :ets.lookup(ExchangeRates.table_name(), false)
end
test "with failed fetch" do
@ -114,25 +111,30 @@ defmodule Explorer.ExchangeRatesTest do
expect(TestSource, :headers, fn -> [] end)
set_mox_global()
assert {:noreply, ^state} = ExchangeRates.handle_info({nil, {:error, "some error"}}, state)
assert {:noreply, ^state} = ExchangeRates.handle_info({nil, {:error, false, "some error"}}, state)
assert_receive :update
assert {:noreply, ^state} = ExchangeRates.handle_info(:update, state)
assert_receive {_, {:ok, [%Token{}]}}
assert_receive {_, {:ok, false, [%Token{}]}}
end
end
test "list/0" do
ExchangeRates.init([])
rate_a = %Token{Token.null() | symbol: "a"}
rate_z = %Token{Token.null() | symbol: "z"}
rates = [
%Token{Token.null() | symbol: "z"},
%Token{Token.null() | symbol: "a"}
rate_z,
rate_a
]
expected_rates = Enum.reverse(rates)
for rate <- rates, do: :ets.insert(ExchangeRates.table_name(), Token.to_tuple(rate))
:ets.insert(ExchangeRates.table_name(), {false, rate_z})
:ets.insert(ExchangeRates.table_name(), {true, rate_a})
assert expected_rates == ExchangeRates.list()
end

@ -14,6 +14,9 @@ defmodule Explorer.ExchangeRates.Source.NoOpSource do
@impl Source
def source_url(_), do: :ignore
@impl Source
def secondary_source_url, do: :ignore
@impl Source
def headers, do: []
end

@ -32,6 +32,9 @@ defmodule Explorer.ExchangeRates.Source.OneCoinSource do
@impl Source
def source_url(_), do: :ignore
@impl Source
def secondary_source_url, do: :ignore
@impl Source
def headers, do: []
end

@ -173,7 +173,17 @@ defmodule ConfigHelper do
@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
case System.get_env("EXCHANGE_RATES_SOURCE") do
"coin_gecko" -> Source.CoinGecko
"coin_market_cap" -> Source.CoinMarketCap
"mobula" -> Source.Mobula
_ -> Source.CoinGecko
end
end
@spec exchange_rates_secondary_coin_source() :: Source.CoinGecko | Source.CoinMarketCap | Source.Mobula
def exchange_rates_secondary_coin_source do
case System.get_env("EXCHANGE_RATES_SECONDARY_COIN_SOURCE") do
"coin_gecko" -> Source.CoinGecko
"coin_market_cap" -> Source.CoinMarketCap
"mobula" -> Source.Mobula

@ -344,28 +344,34 @@ config :explorer, Explorer.Counters.FreshPendingTransactionsCounter,
cache_period: ConfigHelper.parse_time_env_var("CACHE_FRESH_PENDING_TRANSACTIONS_COUNTER_PERIOD", "5m"),
enable_consolidation: true
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")
config :explorer, Explorer.ExchangeRates,
store: :ets,
enabled: !disable_exchange_rates?,
fetch_btc_value: ConfigHelper.parse_bool_env_var("EXCHANGE_RATES_FETCH_BTC_VALUE")
enable_consolidation: true,
secondary_coin_enabled:
!disable_exchange_rates? && (cmc_secondary_coin_id || cg_secondary_coin_id || mobula_secondary_coin_id),
fetch_btc_value: ConfigHelper.parse_bool_env_var("EXCHANGE_RATES_FETCH_BTC_VALUE"),
cache_period: ConfigHelper.parse_time_env_var("CACHE_EXCHANGE_RATES_PERIOD", "10m")
config :explorer, Explorer.ExchangeRates.Source,
source: ConfigHelper.exchange_rates_source(),
secondary_coin_source: ConfigHelper.exchange_rates_secondary_coin_source(),
price_source: ConfigHelper.exchange_rates_price_source(),
secondary_coin_price_source: ConfigHelper.exchange_rates_secondary_coin_price_source(),
market_cap_source: ConfigHelper.exchange_rates_market_cap_source(),
tvl_source: ConfigHelper.exchange_rates_tvl_source()
cmc_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINMARKETCAP_SECONDARY_COIN_ID")
config :explorer, Explorer.ExchangeRates.Source.CoinMarketCap,
base_url: System.get_env("EXCHANGE_RATES_COINMARKETCAP_BASE_URL"),
api_key: System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY"),
coin_id: System.get_env("EXCHANGE_RATES_COINMARKETCAP_COIN_ID"),
secondary_coin_id: cmc_secondary_coin_id
cg_secondary_coin_id = System.get_env("EXCHANGE_RATES_COINGECKO_SECONDARY_COIN_ID")
config :explorer, Explorer.ExchangeRates.Source.CoinGecko,
platform: System.get_env("EXCHANGE_RATES_COINGECKO_PLATFORM_ID"),
base_url: System.get_env("EXCHANGE_RATES_COINGECKO_BASE_URL"),
@ -379,12 +385,10 @@ config :explorer, Explorer.ExchangeRates.Source.Mobula,
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")
secondary_coin_id: 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")
config :explorer, Explorer.Market.History.Source.Price.CryptoCompare, secondary_coin_symbol: cc_secondary_coin_symbol
config :explorer, Explorer.ExchangeRates.TokenExchangeRates,
@ -408,7 +412,8 @@ config :explorer, Explorer.Market.History.Cataloger,
enabled: !disable_indexer? && !disable_exchange_rates?,
history_fetch_interval: ConfigHelper.parse_time_env_var("MARKET_HISTORY_FETCH_INTERVAL", "1h"),
secondary_coin_enabled:
cmc_secondary_coin_id || cg_secondary_coin_id || cc_secondary_coin_symbol || cryptorank_secondary_coin_id
cmc_secondary_coin_id || cg_secondary_coin_id || cc_secondary_coin_symbol || mobula_secondary_coin_id ||
cryptorank_secondary_coin_id
config :explorer, Explorer.Chain.Transaction, suave_bid_contracts: System.get_env("SUAVE_BID_CONTRACTS", "")

@ -20,6 +20,8 @@ config :explorer, Explorer.Counters.Transactions24hStats,
cache_period: ConfigHelper.parse_time_env_var("CACHE_TRANSACTIONS_24H_STATS_PERIOD", "1h"),
enable_consolidation: false
config :explorer, Explorer.ExchangeRates, enable_consolidation: false
variant = Variant.get()
Code.require_file("#{variant}.exs", "apps/explorer/config/test")

@ -43,6 +43,8 @@ EMISSION_FORMAT=DEFAULT
# SUPPLY_MODULE=
COIN=
EXCHANGE_RATES_COIN=
# EXCHANGE_RATES_SOURCE=
# EXCHANGE_RATES_SECONDARY_COIN_SOURCE=
# EXCHANGE_RATES_MARKET_CAP_SOURCE=
# EXCHANGE_RATES_TVL_SOURCE=
# EXCHANGE_RATES_PRICE_SOURCE=

Loading…
Cancel
Save