mirror of
https://github.com/luxfi/explorer-v1.git
synced 2026-07-25 16:26:26 +00:00
feat: Add DIA market source (#12678)
This commit is contained in:
@@ -9,8 +9,5 @@
|
||||
{"lib/explorer/smart_contract/stylus/publisher_worker.ex", :pattern_match, 1},
|
||||
{"lib/explorer/smart_contract/stylus/publisher_worker.ex", :exact_eq, 14},
|
||||
{"lib/explorer/smart_contract/stylus/publisher_worker.ex", :pattern_match, 14},
|
||||
~r/lib\/phoenix\/router.ex/,
|
||||
{"lib/explorer/chain/search.ex", :pattern_match, 100},
|
||||
{"lib/explorer/chain/search.ex", :pattern_match, 282},
|
||||
{"lib/explorer/chain/search.ex", :pattern_match, 379}
|
||||
~r/lib\/phoenix\/router.ex/
|
||||
]
|
||||
|
||||
@@ -65,6 +65,13 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
|
||||
end
|
||||
|
||||
test "subscribed user is notified with market history", %{token: token} do
|
||||
initial_value = :persistent_term.get(:market_history_fetcher_enabled, false)
|
||||
:persistent_term.put(:market_history_fetcher_enabled, true)
|
||||
|
||||
on_exit(fn ->
|
||||
:persistent_term.put(:market_history_fetcher_enabled, initial_value)
|
||||
end)
|
||||
|
||||
Coin.handle_info({nil, {{:ok, token}, false}}, %{})
|
||||
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})
|
||||
Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})
|
||||
|
||||
+26
-2
@@ -5,7 +5,7 @@ defmodule BlockScoutWeb.V2.ExchangeRateChannelTest do
|
||||
|
||||
alias BlockScoutWeb.Notifier
|
||||
alias Explorer.Market.Fetcher.{Coin, History}
|
||||
alias Explorer.Market.{MarketHistory, Token}
|
||||
alias Explorer.Market.{MarketHistory, MarketHistoryCache, Token}
|
||||
alias Explorer.Market.Source.OneCoinSource
|
||||
alias Explorer.Market
|
||||
|
||||
@@ -64,7 +64,31 @@ defmodule BlockScoutWeb.V2.ExchangeRateChannelTest do
|
||||
end
|
||||
|
||||
test "subscribed user is notified with market history", %{token: token} do
|
||||
Coin.handle_info({nil, {{:ok, token}, false}}, %{})
|
||||
initial_market_history_fetcher_enabled_value = :persistent_term.get(:market_history_fetcher_enabled, false)
|
||||
:persistent_term.put(:market_history_fetcher_enabled, true)
|
||||
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()})
|
||||
Supervisor.restart_child(Explorer.Supervisor, {ConCache, MarketHistoryCache.cache_name()})
|
||||
|
||||
source_configuration = Application.get_env(:explorer, Explorer.Market.Source)
|
||||
fetcher_configuration = Application.get_env(:explorer, Coin)
|
||||
|
||||
Application.put_env(:explorer, Explorer.Market.Source,
|
||||
native_coin_source: OneCoinSource,
|
||||
secondary_coin_source: OneCoinSource
|
||||
)
|
||||
|
||||
Application.put_env(:explorer, Coin, Keyword.merge(fetcher_configuration, table_name: :rates, enabled: true))
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:explorer, Explorer.Market.Source, source_configuration)
|
||||
Application.put_env(:explorer, Coin, fetcher_configuration)
|
||||
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
|
||||
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
|
||||
:persistent_term.put(:market_history_fetcher_enabled, initial_market_history_fetcher_enabled_value)
|
||||
end)
|
||||
|
||||
{:ok, state} = Coin.init([])
|
||||
Coin.handle_info({nil, {{:ok, token}, false}}, state)
|
||||
Supervisor.terminate_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})
|
||||
Supervisor.restart_child(Explorer.Supervisor, {ConCache, Explorer.Market.MarketHistoryCache.cache_name()})
|
||||
|
||||
|
||||
+9
@@ -21,6 +21,15 @@ defmodule BlockScoutWeb.Account.Api.V2.UserControllerTest do
|
||||
end
|
||||
|
||||
describe "Test account/api/account/v2/user" do
|
||||
setup do
|
||||
initial_value = :persistent_term.get(:market_token_fetcher_enabled, false)
|
||||
:persistent_term.put(:market_token_fetcher_enabled, true)
|
||||
|
||||
on_exit(fn ->
|
||||
:persistent_term.put(:market_token_fetcher_enabled, initial_value)
|
||||
end)
|
||||
end
|
||||
|
||||
test "get user info", %{conn: conn, user: user} do
|
||||
result_conn =
|
||||
conn
|
||||
|
||||
+7
@@ -3228,6 +3228,13 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do
|
||||
end
|
||||
|
||||
test "get tokens", %{conn: conn} do
|
||||
initial_value = :persistent_term.get(:market_token_fetcher_enabled, false)
|
||||
:persistent_term.put(:market_token_fetcher_enabled, true)
|
||||
|
||||
on_exit(fn ->
|
||||
:persistent_term.put(:market_token_fetcher_enabled, initial_value)
|
||||
end)
|
||||
|
||||
address = insert(:address)
|
||||
|
||||
ctbs_erc_20 =
|
||||
|
||||
+9
@@ -7,6 +7,15 @@ defmodule BlockScoutWeb.API.V2.SearchControllerTest do
|
||||
alias Plug.Conn.Query
|
||||
|
||||
describe "/search" do
|
||||
setup do
|
||||
initial_value = :persistent_term.get(:market_token_fetcher_enabled, false)
|
||||
:persistent_term.put(:market_token_fetcher_enabled, true)
|
||||
|
||||
on_exit(fn ->
|
||||
:persistent_term.put(:market_token_fetcher_enabled, initial_value)
|
||||
end)
|
||||
end
|
||||
|
||||
test "get token-transfers with ok reputation", %{conn: conn} do
|
||||
init_value = Application.get_env(:block_scout_web, :hide_scam_addresses)
|
||||
Application.put_env(:block_scout_web, :hide_scam_addresses, true)
|
||||
|
||||
+2
-2
@@ -242,7 +242,7 @@ defmodule BlockScoutWeb.API.V2.StatsControllerTest do
|
||||
addresses = insert_transactions_in_last_seconds(55, 10800)
|
||||
|
||||
addresses =
|
||||
Enum.map(addresses, fn addr -> to_string(addr.hash) end) |> dbg(limit: :infinity, printable_limit: :infinity)
|
||||
Enum.map(addresses, fn addr -> to_string(addr.hash) end)
|
||||
|
||||
request = get(conn, "/api/v2/stats/hot-smart-contracts", %{scale: "3h"})
|
||||
assert %{"items" => items, "next_page_params" => next_page_params} = json_response(request, 200)
|
||||
@@ -250,7 +250,7 @@ defmodule BlockScoutWeb.API.V2.StatsControllerTest do
|
||||
assert is_map(next_page_params)
|
||||
|
||||
request = get(conn, "/api/v2/stats/hot-smart-contracts", Map.merge(next_page_params, %{scale: "3h"}))
|
||||
assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200) |> dbg()
|
||||
assert %{"items" => items, "next_page_params" => nil} = json_response(request, 200)
|
||||
assert length(items) == 5
|
||||
end
|
||||
|
||||
|
||||
@@ -469,6 +469,15 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
|
||||
end
|
||||
|
||||
describe "/tokens" do
|
||||
setup do
|
||||
initial_value = :persistent_term.get(:market_token_fetcher_enabled, false)
|
||||
:persistent_term.put(:market_token_fetcher_enabled, true)
|
||||
|
||||
on_exit(fn ->
|
||||
:persistent_term.put(:market_token_fetcher_enabled, initial_value)
|
||||
end)
|
||||
end
|
||||
|
||||
defp check_tokens_pagination(tokens, conn, additional_params \\ %{}) do
|
||||
request = get(conn, "/api/v2/tokens", additional_params)
|
||||
assert response = json_response(request, 200)
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
defmodule Explorer.Chain.FiatValueBenchmark do
|
||||
@moduledoc """
|
||||
Benchmark for the performance of fetching fiat value type data from the database.
|
||||
"""
|
||||
|
||||
use Explorer.BenchmarkCase
|
||||
|
||||
alias Explorer.Repo
|
||||
alias Explorer.Chain.Token
|
||||
alias Explorer.Market.Fetcher.Token, as: TokenFetcher
|
||||
|
||||
def list_tokens do
|
||||
Benchee.run(%{"Fiat value type performance" => fn _ -> Repo.all(Token) end},
|
||||
inputs:
|
||||
for with_market_data <- [false, true],
|
||||
enabled_token_fetcher? when with_market_data or not enabled_token_fetcher? <- [false, true],
|
||||
token_count <- [50, 100, 10000],
|
||||
into: %{} do
|
||||
{"#{token_count} tokens#{if with_market_data, do: " with market data", else: ""}#{if enabled_token_fetcher?, do: " with enabled token fetcher", else: ""}",
|
||||
%{
|
||||
token_count: token_count,
|
||||
with_market_data: with_market_data,
|
||||
enabled_token_fetcher: enabled_token_fetcher?
|
||||
}}
|
||||
end,
|
||||
before_scenario: fn %{
|
||||
token_count: token_count,
|
||||
with_market_data: with_market_data,
|
||||
enabled_token_fetcher: enabled_token_fetcher?
|
||||
} = input ->
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Repo, ownership_timeout: :infinity)
|
||||
|
||||
Repo.delete_all(Token)
|
||||
|
||||
market_data = fn i ->
|
||||
if with_market_data do
|
||||
[fiat_value: Decimal.new(i), circulating_market_cap: Decimal.new(i)]
|
||||
else
|
||||
[fiat_value: nil, circulating_market_cap: nil]
|
||||
end
|
||||
end
|
||||
|
||||
1..token_count
|
||||
|> Enum.each(fn i ->
|
||||
insert(:token, market_data.(i))
|
||||
end)
|
||||
|
||||
if enabled_token_fetcher? do
|
||||
Application.put_env(:explorer, Explorer.Market.Source, tokens_source: Explorer.Market.Source.OneCoinSource)
|
||||
TokenFetcher.start_link([])
|
||||
end
|
||||
end,
|
||||
load: @path,
|
||||
save: [
|
||||
path: @path,
|
||||
tag: "fiat-value-no-check"
|
||||
],
|
||||
time: 5,
|
||||
formatters: [Benchee.Formatters.Console]
|
||||
)
|
||||
end
|
||||
end
|
||||
|
||||
Explorer.Chain.FiatValueBenchmark.list_tokens()
|
||||
@@ -19,4 +19,22 @@ defmodule Explorer do
|
||||
def coin_name do
|
||||
Application.get_env(:explorer, :coin_name)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieves the current operational mode of the Explorer application.
|
||||
|
||||
The mode determines which components of the Explorer application are active:
|
||||
- `:all` - Both API web server and blockchain indexer run together
|
||||
- `:indexer` - Only blockchain indexer modules run without the web server
|
||||
- `:api` - Only the web server runs without performing indexing operations
|
||||
|
||||
## Returns
|
||||
- `:all` if both API and indexer components should be active
|
||||
- `:indexer` if only the indexer component should be active
|
||||
- `:api` if only the API web server component should be active
|
||||
"""
|
||||
@spec mode() :: :all | :indexer | :api
|
||||
def mode do
|
||||
Application.get_env(:explorer, :mode)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,10 +116,10 @@ defmodule Explorer.Application do
|
||||
defp configurable_children do
|
||||
configurable_children_set =
|
||||
[
|
||||
configure_libcluster(),
|
||||
configure_mode_dependent_process(Explorer.Market.Fetcher.Coin, :api),
|
||||
configure_mode_dependent_process(Explorer.Market.Fetcher.Token, :indexer),
|
||||
configure_mode_dependent_process(Explorer.Market.Fetcher.History, :indexer),
|
||||
configure_mode_dependent_process(Explorer.Market, :api),
|
||||
configure(Explorer.ChainSpec.GenesisData),
|
||||
configure(Explorer.Chain.Cache.Counters.ContractsCount),
|
||||
configure(Explorer.Chain.Cache.Counters.NewContractsCount),
|
||||
@@ -328,7 +328,9 @@ defmodule Explorer.Application do
|
||||
configure(Explorer.Chain.Fetcher.AddressesBlacklist),
|
||||
Explorer.Migrator.SwitchPendingOperations,
|
||||
configure_mode_dependent_process(Explorer.Utility.RateLimiter, :api),
|
||||
Hammer.child_for_supervisor() |> configure_mode_dependent_process(:api)
|
||||
Hammer.child_for_supervisor() |> configure_mode_dependent_process(:api),
|
||||
# keep at the end
|
||||
configure_libcluster()
|
||||
]
|
||||
|> List.flatten()
|
||||
|
||||
@@ -431,7 +433,7 @@ defmodule Explorer.Application do
|
||||
end
|
||||
|
||||
defp configure_mode_dependent_process(process, mode) do
|
||||
if should_start?(process) and Application.get_env(:explorer, :mode) in [mode, :all] do
|
||||
if should_start?(process) and Explorer.mode() in [mode, :all] do
|
||||
process
|
||||
else
|
||||
[]
|
||||
@@ -499,7 +501,7 @@ defmodule Explorer.Application do
|
||||
end
|
||||
|
||||
defp configure_libcluster do
|
||||
if Application.get_env(:explorer, :mode) in [:indexer, :api] do
|
||||
if Explorer.mode() in [:indexer, :api] do
|
||||
libcluster()
|
||||
else
|
||||
[]
|
||||
|
||||
@@ -267,20 +267,22 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
|
||||
fragment("COALESCE(EXCLUDED.circulating_market_cap, ?)", token.circulating_market_cap),
|
||||
volume_24h: fragment("COALESCE(EXCLUDED.volume_24h, ?)", token.volume_24h),
|
||||
icon_url: fragment("COALESCE(?, EXCLUDED.icon_url)", token.icon_url),
|
||||
decimals: fragment("COALESCE(?, EXCLUDED.decimals)", token.decimals),
|
||||
inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", token.inserted_at),
|
||||
updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", token.updated_at)
|
||||
]
|
||||
],
|
||||
where:
|
||||
fragment(
|
||||
"(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.type, EXCLUDED.fiat_value, EXCLUDED.circulating_market_cap, EXCLUDED.volume_24h, EXCLUDED.icon_url) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?)",
|
||||
"(EXCLUDED.name, EXCLUDED.symbol, EXCLUDED.type, EXCLUDED.fiat_value, EXCLUDED.circulating_market_cap, EXCLUDED.volume_24h, EXCLUDED.icon_url, EXCLUDED.decimals) IS DISTINCT FROM (?, ?, ?, ?, ?, ?, ?, ?)",
|
||||
token.name,
|
||||
token.symbol,
|
||||
token.type,
|
||||
token.fiat_value,
|
||||
token.circulating_market_cap,
|
||||
token.volume_24h,
|
||||
token.icon_url
|
||||
token.icon_url,
|
||||
token.decimals
|
||||
)
|
||||
)
|
||||
end
|
||||
@@ -297,10 +299,10 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
|
||||
`:volume_24h`
|
||||
"""
|
||||
@spec market_data_fields_to_update() :: [
|
||||
:name | :symbol | :type | :fiat_value | :circulating_market_cap | :volume_24h
|
||||
:name | :symbol | :type | :fiat_value | :circulating_market_cap | :volume_24h | :decimals
|
||||
]
|
||||
def market_data_fields_to_update do
|
||||
[:name, :symbol, :type, :fiat_value, :circulating_market_cap, :volume_24h]
|
||||
[:name, :symbol, :type, :fiat_value, :circulating_market_cap, :volume_24h, :decimals]
|
||||
end
|
||||
|
||||
defp should_update?(_new_token, nil, _fields_to_replace), do: true
|
||||
|
||||
@@ -3,6 +3,7 @@ defmodule Explorer.Chain.Token.Schema do
|
||||
use Utils.CompileTimeEnvHelper, bridged_tokens_enabled: [:explorer, [Explorer.Chain.BridgedToken, :enabled]]
|
||||
|
||||
alias Explorer.Chain.{Address, Address.Reputation, Hash}
|
||||
alias Explorer.Chain.Token.FiatValue
|
||||
|
||||
if @bridged_tokens_enabled do
|
||||
@bridged_field [
|
||||
@@ -28,11 +29,11 @@ defmodule Explorer.Chain.Token.Schema do
|
||||
field(:skip_metadata, :boolean)
|
||||
field(:total_supply_updated_at_block, :integer)
|
||||
field(:metadata_updated_at, :utc_datetime_usec)
|
||||
field(:fiat_value, :decimal)
|
||||
field(:circulating_market_cap, :decimal)
|
||||
field(:fiat_value, FiatValue)
|
||||
field(:circulating_market_cap, FiatValue)
|
||||
field(:icon_url, :string)
|
||||
field(:is_verified_via_admin_panel, :boolean)
|
||||
field(:volume_24h, :decimal)
|
||||
field(:volume_24h, FiatValue)
|
||||
field(:transfer_count, :integer)
|
||||
|
||||
belongs_to(
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
defmodule Explorer.Chain.Token.FiatValue do
|
||||
@moduledoc """
|
||||
Represents money values, used to hide the value if there is a chance that the value is not relevant.
|
||||
"""
|
||||
|
||||
use Ecto.Type
|
||||
|
||||
alias Explorer.Market
|
||||
|
||||
@type t :: Decimal.t() | nil
|
||||
|
||||
@impl Ecto.Type
|
||||
def type, do: :decimal
|
||||
|
||||
@impl Ecto.Type
|
||||
def cast(value) when is_binary(value) do
|
||||
case Decimal.parse(value) do
|
||||
{decimal, ""} ->
|
||||
{:ok, decimal}
|
||||
|
||||
_ ->
|
||||
:error
|
||||
end
|
||||
end
|
||||
|
||||
@impl Ecto.Type
|
||||
def cast(value) when is_integer(value) do
|
||||
{:ok, Decimal.new(value)}
|
||||
end
|
||||
|
||||
@impl Ecto.Type
|
||||
def cast(value) when is_float(value) do
|
||||
{:ok, Decimal.from_float(value)}
|
||||
end
|
||||
|
||||
@impl Ecto.Type
|
||||
def cast(%Decimal{} = decimal) do
|
||||
{:ok, decimal}
|
||||
end
|
||||
|
||||
@impl Ecto.Type
|
||||
def cast(_), do: :error
|
||||
|
||||
@impl Ecto.Type
|
||||
def dump(%Decimal{} = decimal) do
|
||||
{:ok, decimal}
|
||||
end
|
||||
|
||||
@impl Ecto.Type
|
||||
def dump(_), do: :error
|
||||
|
||||
@impl Ecto.Type
|
||||
def load(%Decimal{} = decimal) do
|
||||
if Market.token_fetcher_enabled?() do
|
||||
{:ok, decimal}
|
||||
else
|
||||
{:ok, nil}
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -660,7 +660,7 @@ defmodule Explorer.Helper do
|
||||
"""
|
||||
@spec indexer_node?(Node.t()) :: boolean()
|
||||
def indexer_node?(node) do
|
||||
(node |> :rpc.call(Application, :get_env, [:explorer, :mode]) |> process_rpc_response(node, nil)) in [
|
||||
(node |> :rpc.call(Explorer, :mode, []) |> process_rpc_response(node, nil)) in [
|
||||
:all,
|
||||
:indexer
|
||||
]
|
||||
|
||||
@@ -64,11 +64,16 @@ defmodule Explorer.Market.Fetcher.History do
|
||||
}}
|
||||
end)
|
||||
|
||||
state = %{types_states: types_states}
|
||||
if Enum.all?(types_states, fn {_type, type_state} -> is_nil(type_state.source) end) do
|
||||
Logger.info("No market history sources are configured")
|
||||
:ignore
|
||||
else
|
||||
state = %{types_states: types_states}
|
||||
|
||||
send(self(), {:fetch_all, config(:first_fetch_day_count)})
|
||||
send(self(), {:fetch_all, config(:first_fetch_day_count)})
|
||||
|
||||
{:ok, state}
|
||||
{:ok, state}
|
||||
end
|
||||
end
|
||||
|
||||
def handle_info({:fetch_all, day_count}, state) do
|
||||
@@ -121,6 +126,7 @@ defmodule Explorer.Market.Fetcher.History do
|
||||
|
||||
new_types_states = put_in(state.types_states, [type, :failed_attempts], failed_attempts)
|
||||
new_state = %{state | types_states: new_types_states}
|
||||
maybe_insert_and_schedule_refetch(new_state)
|
||||
|
||||
{:noreply, new_state}
|
||||
end
|
||||
|
||||
@@ -3,9 +3,68 @@ defmodule Explorer.Market do
|
||||
Context for data related to the cryptocurrency market.
|
||||
"""
|
||||
|
||||
alias Explorer.Market.Fetcher.Coin
|
||||
use GenServer
|
||||
|
||||
require Logger
|
||||
alias Explorer.Helper
|
||||
alias Explorer.Market.Fetcher.Coin, as: CoinFetcher
|
||||
alias Explorer.Market.Fetcher.History, as: HistoryFetcher
|
||||
alias Explorer.Market.Fetcher.Token, as: TokenFetcher
|
||||
|
||||
alias Explorer.Market.{MarketHistory, MarketHistoryCache, Token}
|
||||
|
||||
@history_key :market_history_fetcher_enabled
|
||||
@tokens_key :market_token_fetcher_enabled
|
||||
|
||||
@spec start_link(term()) :: GenServer.on_start()
|
||||
def start_link(_) do
|
||||
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def init(_opts) do
|
||||
if Explorer.mode() == :all do
|
||||
{history_pid, token_pid} = find_history_and_token_fetchers()
|
||||
:persistent_term.put(@history_key, !is_nil(history_pid))
|
||||
:persistent_term.put(@tokens_key, !is_nil(token_pid))
|
||||
:ignore
|
||||
else
|
||||
{:ok, nil, {:continue, 1}}
|
||||
end
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_continue(attempt, _state) do
|
||||
attempt |> Kernel.**(3) |> :timer.seconds() |> :timer.sleep()
|
||||
|
||||
case Node.list()
|
||||
|> Enum.filter(&Helper.indexer_node?/1) do
|
||||
[] ->
|
||||
if attempt < 5 do
|
||||
{:noreply, nil, {:continue, attempt + 1}}
|
||||
else
|
||||
raise "No indexer nodes discovered after #{attempt} attempts"
|
||||
end
|
||||
|
||||
[indexer] ->
|
||||
{history_pid, token_pid} =
|
||||
indexer
|
||||
|> :rpc.call(__MODULE__, :find_history_and_token_fetchers, [])
|
||||
|> Helper.process_rpc_response(indexer, {nil, nil})
|
||||
|
||||
:persistent_term.put(@history_key, !is_nil(history_pid))
|
||||
:persistent_term.put(@tokens_key, !is_nil(token_pid))
|
||||
{:stop, :normal}
|
||||
|
||||
multiple_indexers ->
|
||||
if attempt < 5 do
|
||||
{:noreply, nil, {:continue, attempt + 1}}
|
||||
else
|
||||
raise "Multiple indexer nodes discovered: #{inspect(multiple_indexers)}"
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieves the history for the recent specified amount of days.
|
||||
|
||||
@@ -13,18 +72,11 @@ defmodule Explorer.Market do
|
||||
"""
|
||||
@spec fetch_recent_history(boolean()) :: [MarketHistory.t()]
|
||||
def fetch_recent_history(secondary_coin? \\ false) do
|
||||
MarketHistoryCache.fetch(secondary_coin?)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Retrieves today's native coin exchange rate from the database.
|
||||
"""
|
||||
@spec get_native_coin_exchange_rate_from_db(boolean()) :: Token.t()
|
||||
def get_native_coin_exchange_rate_from_db(secondary_coin? \\ false) do
|
||||
secondary_coin?
|
||||
|> fetch_recent_history()
|
||||
|> List.first()
|
||||
|> MarketHistory.to_token()
|
||||
if history_fetcher_enabled?() do
|
||||
MarketHistoryCache.fetch(secondary_coin?)
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -32,7 +84,7 @@ defmodule Explorer.Market do
|
||||
"""
|
||||
@spec get_coin_exchange_rate() :: Token.t()
|
||||
def get_coin_exchange_rate do
|
||||
Coin.get_coin_exchange_rate() || get_native_coin_exchange_rate_from_db()
|
||||
CoinFetcher.get_coin_exchange_rate() || Token.null()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -40,7 +92,7 @@ defmodule Explorer.Market do
|
||||
"""
|
||||
@spec get_secondary_coin_exchange_rate() :: Token.t()
|
||||
def get_secondary_coin_exchange_rate do
|
||||
Coin.get_secondary_coin_exchange_rate() || get_native_coin_exchange_rate_from_db(true)
|
||||
CoinFetcher.get_secondary_coin_exchange_rate() || Token.null()
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -70,4 +122,27 @@ defmodule Explorer.Market do
|
||||
|> MarketHistory.price_at_date(false, options)
|
||||
|> MarketHistory.to_token()
|
||||
end
|
||||
|
||||
@doc """
|
||||
Checks if the market token fetcher is enabled in the application.
|
||||
|
||||
This function retrieves the enablement status from persistent term storage
|
||||
using the `:market_token_fetcher_enabled` key. If the key is not set, it
|
||||
defaults to `false`.
|
||||
|
||||
## Returns
|
||||
- `true` if the market token fetcher is enabled
|
||||
- `false` if the market token fetcher is disabled or not configured
|
||||
"""
|
||||
@spec token_fetcher_enabled?() :: boolean()
|
||||
def token_fetcher_enabled? do
|
||||
:persistent_term.get(@tokens_key, false)
|
||||
end
|
||||
|
||||
@spec history_fetcher_enabled?() :: boolean()
|
||||
defp history_fetcher_enabled? do
|
||||
:persistent_term.get(@history_key, false)
|
||||
end
|
||||
|
||||
defp find_history_and_token_fetchers, do: {GenServer.whereis(HistoryFetcher), GenServer.whereis(TokenFetcher)}
|
||||
end
|
||||
|
||||
@@ -41,7 +41,7 @@ defmodule Explorer.Market.MarketHistory do
|
||||
end
|
||||
|
||||
@doc false
|
||||
@spec bulk_insert([map()]) :: {non_neg_integer(), nil | [term()]}
|
||||
@spec bulk_insert([map()]) :: {:ok, {non_neg_integer(), nil | [term()]}} | {:error, term()}
|
||||
def bulk_insert(records) do
|
||||
records_without_zeroes =
|
||||
records
|
||||
@@ -53,10 +53,12 @@ defmodule Explorer.Market.MarketHistory do
|
||||
# Enforce MarketHistory ShareLocks order (see docs: sharelocks.md)
|
||||
|> Enum.sort_by(& &1.date)
|
||||
|
||||
Repo.safe_insert_all(__MODULE__, records_without_zeroes,
|
||||
on_conflict: market_history_on_conflict(),
|
||||
conflict_target: [:date, :secondary_coin]
|
||||
)
|
||||
Repo.transaction(fn ->
|
||||
Repo.safe_insert_all(__MODULE__, records_without_zeroes,
|
||||
on_conflict: market_history_on_conflict(),
|
||||
conflict_target: [:date, :secondary_coin]
|
||||
)
|
||||
end)
|
||||
end
|
||||
|
||||
@spec to_token(t() | nil) :: Token.t()
|
||||
|
||||
@@ -42,6 +42,7 @@ defmodule Explorer.Market.Source do
|
||||
CryptoCompare,
|
||||
CryptoRank,
|
||||
DefiLlama,
|
||||
DIA,
|
||||
Mobula
|
||||
}
|
||||
|
||||
@@ -234,7 +235,7 @@ defmodule Explorer.Market.Source do
|
||||
Decimal.new(value)
|
||||
end
|
||||
|
||||
@sources [CoinGecko, CoinMarketCap, CryptoCompare, CryptoRank, DefiLlama, Mobula]
|
||||
@sources [CoinGecko, CoinMarketCap, CryptoCompare, CryptoRank, DefiLlama, Mobula, DIA]
|
||||
|
||||
@doc """
|
||||
Returns a module for fetching native coin market data.
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
defmodule Explorer.Market.Source.DIA do
|
||||
@moduledoc """
|
||||
Adapter for fetching exchange rates from https://www.diadata.org/
|
||||
"""
|
||||
|
||||
require Logger
|
||||
|
||||
alias Explorer.Chain.Hash
|
||||
alias Explorer.Market.{Source, Token}
|
||||
|
||||
@behaviour Source
|
||||
|
||||
@impl Source
|
||||
def native_coin_fetching_enabled?, do: not is_nil(config(:coin_address_hash))
|
||||
|
||||
@impl Source
|
||||
def fetch_native_coin, do: do_fetch_coin(config(:coin_address_hash), "Coin address hash not specified")
|
||||
|
||||
@impl Source
|
||||
def secondary_coin_fetching_enabled?, do: not is_nil(config(:secondary_coin_address_hash))
|
||||
|
||||
@impl Source
|
||||
def fetch_secondary_coin,
|
||||
do: do_fetch_coin(config(:secondary_coin_address_hash), "Secondary coin address hash not specified")
|
||||
|
||||
@impl Source
|
||||
def tokens_fetching_enabled?, do: not is_nil(config(:blockchain))
|
||||
|
||||
@impl Source
|
||||
def fetch_tokens(state, batch_size) when state in [[], nil] do
|
||||
case init_tokens_fetching() do
|
||||
{:error, _reason} = error ->
|
||||
error
|
||||
|
||||
tokens_to_fetch when is_list(tokens_to_fetch) and length(tokens_to_fetch) > 0 ->
|
||||
fetch_tokens(tokens_to_fetch, batch_size)
|
||||
|
||||
_ ->
|
||||
{:error, "Tokens not found for configured blockchain: #{config(:blockchain)}"}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Source
|
||||
def fetch_tokens(state, batch_size) do
|
||||
# it safe to pattern match here because init_tokens_fetching/0
|
||||
# would have returned an error otherwise
|
||||
blockchain = config(:blockchain)
|
||||
{to_fetch, remaining} = Enum.split(state, batch_size)
|
||||
|
||||
tasks_results =
|
||||
to_fetch
|
||||
|> Task.async_stream(
|
||||
fn token ->
|
||||
case Source.http_request(
|
||||
base_url()
|
||||
|> URI.append_path("/assetQuotation")
|
||||
|> URI.append_path("/#{blockchain}")
|
||||
|> URI.append_path("/#{token.contract_address_hash}")
|
||||
|> URI.to_string(),
|
||||
[]
|
||||
) do
|
||||
{:ok, data} ->
|
||||
token_to_import =
|
||||
Map.merge(token, %{
|
||||
symbol: data["Symbol"],
|
||||
name: data["Name"],
|
||||
fiat_value: Source.to_decimal(data["Price"]),
|
||||
volume_24h: Source.to_decimal(data["VolumeYesterdayUSD"]),
|
||||
type: "ERC-20"
|
||||
})
|
||||
|
||||
{:ok, token_to_import}
|
||||
|
||||
{:error, reason} ->
|
||||
{:error, {token, reason}}
|
||||
end
|
||||
end,
|
||||
max_concurrency: 5,
|
||||
timeout: :timer.seconds(60),
|
||||
zip_input_on_exit: true
|
||||
)
|
||||
|> Enum.group_by(
|
||||
fn
|
||||
{:ok, {:ok, _}} -> :ok
|
||||
_ -> :error
|
||||
end,
|
||||
fn
|
||||
{:ok, {:ok, token_to_import}} -> token_to_import
|
||||
{:ok, {:error, {token, reason}}} -> {token, reason}
|
||||
{:error, {token, reason}} -> {token, reason}
|
||||
end
|
||||
)
|
||||
|
||||
to_import = Map.get(tasks_results, :ok, [])
|
||||
tokens_with_errors = Map.get(tasks_results, :error, [])
|
||||
|
||||
remaining_with_errors =
|
||||
if Enum.empty?(tokens_with_errors) do
|
||||
remaining
|
||||
else
|
||||
Logger.error("Errors while fetching tokens from DIA: #{inspect(tokens_with_errors)}")
|
||||
remaining ++ Enum.map(tokens_with_errors, fn {token, _reason} -> token end)
|
||||
end
|
||||
|
||||
if Enum.empty?(to_import) and !Enum.empty?(tokens_with_errors) do
|
||||
{:error, tokens_with_errors}
|
||||
else
|
||||
{:ok, remaining_with_errors, Enum.empty?(remaining_with_errors), to_import}
|
||||
end
|
||||
end
|
||||
|
||||
@impl Source
|
||||
def native_coin_price_history_fetching_enabled?, do: not is_nil(config(:coin_address_hash))
|
||||
|
||||
@impl Source
|
||||
def fetch_native_coin_price_history(previous_days), do: do_fetch_coin_price_history(previous_days, false)
|
||||
|
||||
@impl Source
|
||||
def secondary_coin_price_history_fetching_enabled?, do: not is_nil(config(:secondary_coin_address_hash))
|
||||
|
||||
@impl Source
|
||||
def fetch_secondary_coin_price_history(previous_days), do: do_fetch_coin_price_history(previous_days, true)
|
||||
|
||||
@impl Source
|
||||
def market_cap_history_fetching_enabled?, do: :ignore
|
||||
|
||||
@impl Source
|
||||
def fetch_market_cap_history(_previous_days), do: :ignore
|
||||
|
||||
@impl Source
|
||||
def tvl_history_fetching_enabled?, do: :ignore
|
||||
|
||||
@impl Source
|
||||
def fetch_tvl_history(_previous_days), do: :ignore
|
||||
|
||||
defp do_fetch_coin(coin_address_hash, coin_address_hash_not_specified_error) do
|
||||
with {:coin, coin_address_hash} when not is_nil(coin_address_hash) <- {:coin, coin_address_hash},
|
||||
{:blockchain, blockchain} when not is_nil(blockchain) <- {:blockchain, config(:blockchain)},
|
||||
{:ok, %{"Price" => _price} = data} <-
|
||||
Source.http_request(
|
||||
base_url()
|
||||
|> URI.append_path("/assetQuotation/#{blockchain}/#{coin_address_hash}")
|
||||
|> URI.to_string(),
|
||||
[]
|
||||
) do
|
||||
{:ok,
|
||||
%Token{
|
||||
available_supply: nil,
|
||||
total_supply: nil,
|
||||
btc_value: nil,
|
||||
last_updated: Source.maybe_get_date(data["Time"]),
|
||||
market_cap: nil,
|
||||
tvl: nil,
|
||||
name: data["Name"],
|
||||
symbol: data["Symbol"],
|
||||
fiat_value: Source.to_decimal(data["Price"]),
|
||||
volume_24h: Source.to_decimal(data["VolumeYesterdayUSD"]),
|
||||
image_url: nil
|
||||
}}
|
||||
else
|
||||
{:coin, nil} -> {:error, coin_address_hash_not_specified_error}
|
||||
{:blockchain, nil} -> {:error, "Blockchain not specified"}
|
||||
{:ok, unexpected_response} -> {:error, Source.unexpected_response_error("DIA", unexpected_response)}
|
||||
{:error, _reason} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp init_tokens_fetching do
|
||||
with blockchain when not is_nil(blockchain) <- config(:blockchain),
|
||||
coin_address_hash = config(:coin_address_hash),
|
||||
{:ok, tokens} <-
|
||||
Source.http_request(
|
||||
base_url()
|
||||
|> URI.append_path("/quotedAssets")
|
||||
|> URI.append_query("blockchain=#{blockchain}")
|
||||
|> URI.to_string(),
|
||||
[]
|
||||
) do
|
||||
tokens
|
||||
|> Enum.reduce([], fn
|
||||
%{
|
||||
"Asset" => %{
|
||||
"Address" => token_contract_address_hash_string,
|
||||
"Decimals" => decimals
|
||||
}
|
||||
},
|
||||
acc ->
|
||||
case (is_nil(coin_address_hash) ||
|
||||
String.downcase(token_contract_address_hash_string) != String.downcase(coin_address_hash)) &&
|
||||
Hash.Address.cast(token_contract_address_hash_string) do
|
||||
{:ok, token_contract_address_hash} ->
|
||||
token = %{
|
||||
contract_address_hash: token_contract_address_hash,
|
||||
decimals: decimals
|
||||
}
|
||||
|
||||
[token | acc]
|
||||
|
||||
_ ->
|
||||
acc
|
||||
end
|
||||
|
||||
_, acc ->
|
||||
acc
|
||||
end)
|
||||
else
|
||||
nil -> {:error, "Blockchain not specified"}
|
||||
{:error, reason} -> {:error, reason}
|
||||
end
|
||||
end
|
||||
|
||||
defp do_fetch_coin_price_history(previous_days, secondary_coin?) do
|
||||
datetime_now = DateTime.utc_now()
|
||||
unix_now = datetime_now |> DateTime.to_unix()
|
||||
unix_from = unix_now - previous_days * 24 * 60 * 60
|
||||
|
||||
with {:coin, coin_address_hash} when not is_nil(coin_address_hash) <-
|
||||
{:coin, if(secondary_coin?, do: config(:secondary_coin_address_hash), else: config(:coin_address_hash))},
|
||||
{:blockchain, blockchain} when not is_nil(blockchain) <- {:blockchain, config(:blockchain)},
|
||||
{:ok, %{"DataPoints" => [%{"Series" => [%{"values" => values}]}]}} <-
|
||||
Source.http_request(
|
||||
base_url()
|
||||
|> URI.append_path("/assetChartPoints/MA120/#{blockchain}/#{coin_address_hash}")
|
||||
|> URI.append_query("starttime=#{unix_from}")
|
||||
|> URI.append_query("endtime=#{unix_now}")
|
||||
|> URI.to_string(),
|
||||
[]
|
||||
) do
|
||||
values
|
||||
|> Enum.reduce_while(%{}, fn value, acc ->
|
||||
with time when not is_nil(time) <- List.first(value),
|
||||
{:ok, datetime, _} <- DateTime.from_iso8601(time),
|
||||
date = DateTime.to_date(datetime),
|
||||
price when not is_nil(price) <- List.last(value) do
|
||||
{:cont,
|
||||
Map.update(
|
||||
acc,
|
||||
date,
|
||||
%{
|
||||
closing_price: Source.to_decimal(price),
|
||||
date: date,
|
||||
opening_price: Source.to_decimal(price),
|
||||
secondary_coin: secondary_coin?
|
||||
},
|
||||
fn existing_entry ->
|
||||
%{
|
||||
existing_entry
|
||||
| opening_price: Source.to_decimal(price)
|
||||
}
|
||||
end
|
||||
)}
|
||||
else
|
||||
_ ->
|
||||
{:halt, {:error, "Wrong format of DIA coin price history response: #{inspect(value)}"}}
|
||||
end
|
||||
end)
|
||||
|> case do
|
||||
{:error, _reason} = error ->
|
||||
error
|
||||
|
||||
price_history_map ->
|
||||
{:ok, Map.values(price_history_map)}
|
||||
end
|
||||
else
|
||||
{:coin, nil} -> {:error, "#{Source.secondary_coin_string(secondary_coin?)} address hash not specified"}
|
||||
{:blockchain, nil} -> {:error, "Blockchain not specified"}
|
||||
{:ok, _} -> {:ok, []}
|
||||
{:error, _reason} = error -> error
|
||||
end
|
||||
end
|
||||
|
||||
defp base_url do
|
||||
:base_url |> config() |> URI.parse()
|
||||
end
|
||||
|
||||
@spec config(atom()) :: term
|
||||
defp config(key) do
|
||||
Application.get_env(:explorer, __MODULE__)[key]
|
||||
end
|
||||
end
|
||||
@@ -66,7 +66,10 @@ defmodule Explorer.Market.Source.Mobula do
|
||||
end
|
||||
end)
|
||||
|
||||
{:ok, offset + batch_size, initial_tokens_len < batch_size, tokens_to_import}
|
||||
fetch_finished? = initial_tokens_len < batch_size
|
||||
new_state = if fetch_finished?, do: nil, else: offset + batch_size
|
||||
|
||||
{:ok, new_state, fetch_finished?, tokens_to_import}
|
||||
else
|
||||
nil -> {:error, "Platform ID not specified"}
|
||||
{:ok, unexpected_response} -> {:error, Source.unexpected_response_error("Mobula", unexpected_response)}
|
||||
|
||||
@@ -292,6 +292,15 @@ defmodule Explorer.Chain.Address.TokenTest do
|
||||
end
|
||||
|
||||
describe "page_tokens/2" do
|
||||
setup do
|
||||
initial_value = :persistent_term.get(:market_token_fetcher_enabled, false)
|
||||
:persistent_term.put(:market_token_fetcher_enabled, true)
|
||||
|
||||
on_exit(fn ->
|
||||
:persistent_term.put(:market_token_fetcher_enabled, initial_value)
|
||||
end)
|
||||
end
|
||||
|
||||
test "just bring the normal query when PagingOptions.key is nil" do
|
||||
options = %PagingOptions{key: nil}
|
||||
|
||||
|
||||
@@ -148,4 +148,43 @@ defmodule Explorer.Chain.TokenTest do
|
||||
assert {:ok, _updated_token} = Token.update(token, update_params)
|
||||
end
|
||||
end
|
||||
|
||||
describe "list_top/2" do
|
||||
test "returns market data with enabled token fetcher" do
|
||||
old_source_env = Application.get_env(:explorer, Explorer.Market.Source)
|
||||
old_fetcher_env = Application.get_env(:explorer, Explorer.Market.Fetcher.Token)
|
||||
|
||||
Application.put_env(
|
||||
:explorer,
|
||||
Explorer.Market.Source,
|
||||
Keyword.merge(old_source_env, tokens_source: Explorer.Market.Source.OneCoinSource)
|
||||
)
|
||||
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.Token, Keyword.merge(old_fetcher_env, enabled: true))
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:explorer, Explorer.Market.Source, old_source_env)
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.Token, old_fetcher_env)
|
||||
end)
|
||||
|
||||
start_supervised!(Explorer.Market.Fetcher.Token)
|
||||
start_supervised!(Explorer.Market)
|
||||
|
||||
insert_list(10, :token)
|
||||
|
||||
market_data = Token.list_top(nil)
|
||||
|
||||
assert Enum.all?(market_data, fn token -> not is_nil(token.fiat_value) end)
|
||||
end
|
||||
|
||||
test "ignores market data with disabled token fetcher" do
|
||||
insert_list(10, :token)
|
||||
|
||||
start_supervised!(Explorer.Market)
|
||||
|
||||
market_data = Token.list_top(nil)
|
||||
|
||||
assert Enum.all?(market_data, fn token -> is_nil(token.fiat_value) end)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -11,14 +11,14 @@ defmodule Explorer.Market.MarketHistoryTest do
|
||||
%{date: ~D[2023-01-02], opening_price: Decimal.new("7.50"), closing_price: Decimal.new("5.00")}
|
||||
]
|
||||
|
||||
assert {2, _} = MarketHistory.bulk_insert(records)
|
||||
assert {:ok, {2, _}} = MarketHistory.bulk_insert(records)
|
||||
|
||||
records = [
|
||||
%{date: ~D[2023-01-01], opening_price: Decimal.new("50.00"), closing_price: Decimal.new("55.00")},
|
||||
%{date: ~D[2023-01-02], opening_price: Decimal.new("10.00"), closing_price: Decimal.new("8.00")}
|
||||
]
|
||||
|
||||
assert {2, _} = MarketHistory.bulk_insert(records)
|
||||
assert {:ok, {2, _}} = MarketHistory.bulk_insert(records)
|
||||
|
||||
assert [date1, date2] = Repo.all(MarketHistory)
|
||||
assert date1.opening_price == Decimal.new("50.00")
|
||||
|
||||
@@ -3,7 +3,6 @@ defmodule Explorer.MarketTest do
|
||||
|
||||
alias Explorer.Market
|
||||
alias Explorer.Market.MarketHistory
|
||||
alias Explorer.Repo
|
||||
|
||||
setup do
|
||||
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
|
||||
@@ -17,24 +16,123 @@ defmodule Explorer.MarketTest do
|
||||
:ok
|
||||
end
|
||||
|
||||
test "fetch_recent_history/1" do
|
||||
ConCache.delete(:market_history, :last_update)
|
||||
describe "fetch_recent_history/1" do
|
||||
test "with enabled history fetcher" do
|
||||
ConCache.delete(:market_history, :last_update)
|
||||
|
||||
today = Date.utc_today()
|
||||
start_supervised!(Explorer.Market.Fetcher.History)
|
||||
start_supervised!(Explorer.Market)
|
||||
|
||||
records =
|
||||
for i <- 0..29 do
|
||||
%{
|
||||
date: Timex.shift(today, days: i * -1),
|
||||
closing_price: Decimal.new(1),
|
||||
opening_price: Decimal.new(1)
|
||||
}
|
||||
end
|
||||
today = Date.utc_today()
|
||||
|
||||
MarketHistory.bulk_insert(records)
|
||||
records =
|
||||
for i <- 0..29 do
|
||||
%{
|
||||
date: Timex.shift(today, days: i * -1),
|
||||
closing_price: Decimal.new(1),
|
||||
opening_price: Decimal.new(1)
|
||||
}
|
||||
end
|
||||
|
||||
history = Market.fetch_recent_history()
|
||||
assert length(history) == 30
|
||||
assert Enum.at(history, 0).date == Enum.at(records, 0).date
|
||||
MarketHistory.bulk_insert(records)
|
||||
|
||||
history = Market.fetch_recent_history()
|
||||
assert length(history) == 30
|
||||
assert Enum.at(history, 0).date == Enum.at(records, 0).date
|
||||
end
|
||||
|
||||
test "with disabled history fetcher" do
|
||||
ConCache.delete(:market_history, :last_update)
|
||||
start_supervised!(Explorer.Market)
|
||||
|
||||
old_env = Application.get_env(:explorer, Explorer.Market.Fetcher.History)
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.History, Keyword.merge(old_env, enabled: false))
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.History, old_env)
|
||||
end)
|
||||
|
||||
today = Date.utc_today()
|
||||
|
||||
records =
|
||||
for i <- 0..29 do
|
||||
%{
|
||||
date: Timex.shift(today, days: i * -1),
|
||||
closing_price: Decimal.new(1),
|
||||
opening_price: Decimal.new(1)
|
||||
}
|
||||
end
|
||||
|
||||
MarketHistory.bulk_insert(records)
|
||||
|
||||
history = Market.fetch_recent_history()
|
||||
assert length(history) == 0
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_coin_exchange_rate/0" do
|
||||
test "with enabled coin fetcher" do
|
||||
old_source_env = Application.get_env(:explorer, Explorer.Market.Source)
|
||||
old_fetcher_env = Application.get_env(:explorer, Explorer.Market.Fetcher.Coin)
|
||||
|
||||
Application.put_env(
|
||||
:explorer,
|
||||
Explorer.Market.Source,
|
||||
Keyword.merge(old_source_env, native_coin_source: Explorer.Market.Source.OneCoinSource)
|
||||
)
|
||||
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, Keyword.merge(old_fetcher_env, enabled: true))
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:explorer, Explorer.Market.Source, old_source_env)
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, old_fetcher_env)
|
||||
end)
|
||||
|
||||
start_supervised!(Explorer.Market.Fetcher.Coin)
|
||||
|
||||
:timer.sleep(100)
|
||||
|
||||
exchange_rate = Market.get_coin_exchange_rate()
|
||||
|
||||
assert exchange_rate.fiat_value == Decimal.new(1)
|
||||
end
|
||||
|
||||
test "with disabled coin fetcher" do
|
||||
exchange_rate = Market.get_coin_exchange_rate()
|
||||
assert exchange_rate.fiat_value == nil
|
||||
end
|
||||
end
|
||||
|
||||
describe "get_secondary_coin_exchange_rate/0" do
|
||||
test "with enabled coin fetcher" do
|
||||
old_source_env = Application.get_env(:explorer, Explorer.Market.Source)
|
||||
old_fetcher_env = Application.get_env(:explorer, Explorer.Market.Fetcher.Coin)
|
||||
|
||||
Application.put_env(
|
||||
:explorer,
|
||||
Explorer.Market.Source,
|
||||
Keyword.merge(old_source_env, secondary_coin_source: Explorer.Market.Source.OneCoinSource)
|
||||
)
|
||||
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, Keyword.merge(old_fetcher_env, enabled: true))
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:explorer, Explorer.Market.Source, old_source_env)
|
||||
Application.put_env(:explorer, Explorer.Market.Fetcher.Coin, old_fetcher_env)
|
||||
end)
|
||||
|
||||
start_supervised!(Explorer.Market.Fetcher.Coin)
|
||||
|
||||
:timer.sleep(100)
|
||||
|
||||
exchange_rate = Market.get_secondary_coin_exchange_rate()
|
||||
|
||||
assert exchange_rate.fiat_value == Decimal.new(1)
|
||||
end
|
||||
|
||||
test "with disabled coin fetcher" do
|
||||
exchange_rate = Market.get_secondary_coin_exchange_rate()
|
||||
assert exchange_rate.fiat_value == nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -106,11 +106,11 @@ defmodule Explorer.Market.Source.CryptoRankTest do
|
||||
end
|
||||
|
||||
describe "tokens_fetching_enabled?" do
|
||||
test "returns true if coin_id is configured" do
|
||||
test "returns true if platform is configured" do
|
||||
assert CryptoRank.tokens_fetching_enabled?()
|
||||
end
|
||||
|
||||
test "returns false if coin_id is not configured", %{old_env: old_env} do
|
||||
test "returns false if platform is not configured", %{old_env: old_env} do
|
||||
Application.put_env(:explorer, CryptoRank, Keyword.merge(old_env, platform: nil))
|
||||
|
||||
refute CryptoRank.tokens_fetching_enabled?()
|
||||
|
||||
@@ -0,0 +1,437 @@
|
||||
defmodule Explorer.Market.Source.DIATest do
|
||||
use ExUnit.Case
|
||||
|
||||
alias Explorer.Market.Source.DIA
|
||||
alias Plug.Conn
|
||||
|
||||
@native_coin_history_json File.read!("./test/support/fixture/market/dia/native_coin_history.json")
|
||||
@secondary_coin_history_json File.read!("./test/support/fixture/market/dia/secondary_coin_history.json")
|
||||
|
||||
setup do
|
||||
bypass = Bypass.open()
|
||||
old_env = Application.get_env(:explorer, DIA, [])
|
||||
|
||||
new_env =
|
||||
Keyword.merge(
|
||||
old_env,
|
||||
blockchain: "Ethereum",
|
||||
base_url: "http://localhost:#{bypass.port}",
|
||||
coin_address_hash: "0x0000000000000000000000000000000000000000",
|
||||
secondary_coin_address_hash: "0x0000000000000000000000000000000000000001"
|
||||
)
|
||||
|
||||
Application.put_env(
|
||||
:explorer,
|
||||
DIA,
|
||||
new_env
|
||||
)
|
||||
|
||||
Application.put_env(:tesla, :adapter, Tesla.Adapter.Mint)
|
||||
|
||||
on_exit(fn ->
|
||||
Application.put_env(:explorer, DIA, old_env)
|
||||
Application.put_env(:tesla, :adapter, Explorer.Mock.TeslaAdapter)
|
||||
end)
|
||||
|
||||
{:ok, env: new_env, bypass: bypass}
|
||||
end
|
||||
|
||||
describe "native_coin_fetching_enabled?" do
|
||||
test "returns true if coin_address_hash is configured" do
|
||||
assert DIA.native_coin_fetching_enabled?()
|
||||
end
|
||||
|
||||
test "returns false if coin_address_hash is not configured", %{env: env} do
|
||||
Application.put_env(:explorer, DIA, Keyword.merge(env, coin_address_hash: nil))
|
||||
refute DIA.native_coin_fetching_enabled?()
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_native_coin" do
|
||||
test "fetches native coin", %{bypass: bypass} do
|
||||
Bypass.expect_once(
|
||||
bypass,
|
||||
"GET",
|
||||
"/assetQuotation/Ethereum/0x0000000000000000000000000000000000000000",
|
||||
fn conn ->
|
||||
Conn.resp(
|
||||
conn,
|
||||
200,
|
||||
json_coin("0x0000000000000000000000000000000000000000", 3027.98578732332, "ETH", "Ethereum")
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
assert {:ok,
|
||||
%Explorer.Market.Token{
|
||||
available_supply: nil,
|
||||
btc_value: nil,
|
||||
fiat_value: Decimal.new("3027.98578732332"),
|
||||
image_url: nil,
|
||||
last_updated: ~U[2025-11-27 08:09:59Z],
|
||||
market_cap: nil,
|
||||
name: "Ethereum",
|
||||
symbol: "ETH",
|
||||
total_supply: nil,
|
||||
tvl: nil,
|
||||
volume_24h: Decimal.new("6577658642.868258")
|
||||
}} ==
|
||||
DIA.fetch_native_coin()
|
||||
end
|
||||
end
|
||||
|
||||
describe "secondary_coin_fetching_enabled?" do
|
||||
test "returns true if secondary_coin_address_hash is configured" do
|
||||
assert DIA.secondary_coin_fetching_enabled?()
|
||||
end
|
||||
|
||||
test "returns false if secondary_coin_address_hash is not configured", %{env: env} do
|
||||
Application.put_env(:explorer, DIA, Keyword.merge(env, secondary_coin_address_hash: nil))
|
||||
|
||||
refute DIA.secondary_coin_fetching_enabled?()
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_secondary_coin" do
|
||||
test "fetches secondary coin", %{bypass: bypass} do
|
||||
Bypass.expect_once(
|
||||
bypass,
|
||||
"GET",
|
||||
"/assetQuotation/Ethereum/0x0000000000000000000000000000000000000001",
|
||||
fn conn ->
|
||||
Conn.resp(
|
||||
conn,
|
||||
200,
|
||||
json_coin("0x0000000000000000000000000000000000000001", 91169.97153780921, "WBTC", "Wrapped BTC")
|
||||
)
|
||||
end
|
||||
)
|
||||
|
||||
assert {:ok,
|
||||
%Explorer.Market.Token{
|
||||
available_supply: nil,
|
||||
btc_value: nil,
|
||||
fiat_value: Decimal.new("91169.97153780921"),
|
||||
image_url: nil,
|
||||
last_updated: ~U[2025-11-27 08:09:59Z],
|
||||
market_cap: nil,
|
||||
name: "Wrapped BTC",
|
||||
symbol: "WBTC",
|
||||
total_supply: nil,
|
||||
tvl: nil,
|
||||
volume_24h: Decimal.new("6577658642.868258")
|
||||
}} ==
|
||||
DIA.fetch_secondary_coin()
|
||||
end
|
||||
end
|
||||
|
||||
describe "tokens_fetching_enabled?" do
|
||||
test "returns true if blockchain is configured" do
|
||||
assert DIA.tokens_fetching_enabled?()
|
||||
end
|
||||
|
||||
test "returns false if blockchain is not configured", %{env: env} do
|
||||
Application.put_env(:explorer, DIA, Keyword.merge(env, blockchain: nil))
|
||||
|
||||
refute DIA.tokens_fetching_enabled?()
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_tokens" do
|
||||
test "fetches tokens", %{bypass: bypass} do
|
||||
Bypass.expect(bypass, "GET", "/quotedAssets", fn conn ->
|
||||
assert conn.query_string == "blockchain=Ethereum"
|
||||
Conn.resp(conn, 200, json_tokens_list())
|
||||
end)
|
||||
|
||||
Bypass.expect(bypass, "GET", "/assetQuotation/Ethereum/0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", fn conn ->
|
||||
Conn.resp(
|
||||
conn,
|
||||
200,
|
||||
json_coin("0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", 0.9997188333561603, "USDC", "USD Coin")
|
||||
)
|
||||
end)
|
||||
|
||||
Bypass.expect(bypass, "GET", "/assetQuotation/Ethereum/0xdac17f958d2ee523a2206206994597c13d831ec7", fn conn ->
|
||||
Conn.resp(
|
||||
conn,
|
||||
200,
|
||||
json_coin("0xdAC17F958D2ee523a2206206994597C13D831ec7", 0.9999313493241913, "USDT", "Tether USD")
|
||||
)
|
||||
end)
|
||||
|
||||
usdt_and_usdc_to_fetch = [
|
||||
%{
|
||||
contract_address_hash: %Explorer.Chain.Hash{
|
||||
byte_count: 20,
|
||||
bytes: "\xDA\xC1\d\x95\x8D.\xE5#\xA2 b\x06\x99E\x97\xC1=\x83\x1E\xC7"
|
||||
},
|
||||
decimals: 6
|
||||
},
|
||||
%{
|
||||
contract_address_hash: %Explorer.Chain.Hash{
|
||||
byte_count: 20,
|
||||
bytes: "\xA0\xB8i\x91\xC6!\x8B6\xC1ѝJ.\x9E\xB0\xCE6\x06\xEBH"
|
||||
},
|
||||
decimals: 6
|
||||
}
|
||||
]
|
||||
|
||||
usdt_and_usdc = [
|
||||
%{
|
||||
name: "Tether USD",
|
||||
type: "ERC-20",
|
||||
symbol: "USDT",
|
||||
contract_address_hash: %Explorer.Chain.Hash{
|
||||
byte_count: 20,
|
||||
bytes: <<218, 193, 127, 149, 141, 46, 229, 35, 162, 32, 98, 6, 153, 69, 151, 193, 61, 131, 30, 199>>
|
||||
},
|
||||
fiat_value: Decimal.new("0.9999313493241913"),
|
||||
volume_24h: Decimal.new("6577658642.868258"),
|
||||
decimals: 6
|
||||
},
|
||||
%{
|
||||
name: "USD Coin",
|
||||
type: "ERC-20",
|
||||
symbol: "USDC",
|
||||
contract_address_hash: %Explorer.Chain.Hash{
|
||||
byte_count: 20,
|
||||
bytes: <<160, 184, 105, 145, 198, 33, 139, 54, 193, 209, 157, 74, 46, 158, 176, 206, 54, 6, 235, 72>>
|
||||
},
|
||||
fiat_value: Decimal.new("0.9997188333561603"),
|
||||
volume_24h: Decimal.new("6577658642.868258"),
|
||||
decimals: 6
|
||||
}
|
||||
]
|
||||
|
||||
assert {:ok, [usdt_or_usdc_to_fetch] = state, false, [usdc_or_usdt_a]} = DIA.fetch_tokens(nil, 1)
|
||||
|
||||
assert usdt_or_usdc_to_fetch in usdt_and_usdc_to_fetch
|
||||
|
||||
assert {:ok, [], true, [usdc_or_usdt_b]} = DIA.fetch_tokens(state, 1)
|
||||
|
||||
assert usdc_or_usdt_a in usdt_and_usdc
|
||||
assert usdc_or_usdt_b in usdt_and_usdc
|
||||
assert usdc_or_usdt_a != usdc_or_usdt_b
|
||||
end
|
||||
end
|
||||
|
||||
describe "native_coin_price_history_fetching_enabled?" do
|
||||
test "returns true if coin_address_hash is configured" do
|
||||
assert DIA.native_coin_price_history_fetching_enabled?()
|
||||
end
|
||||
|
||||
test "returns false if coin_address_hash is not configured", %{env: env} do
|
||||
Application.put_env(:explorer, DIA, Keyword.merge(env, coin_address_hash: nil))
|
||||
|
||||
refute DIA.native_coin_price_history_fetching_enabled?()
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_native_coin_price_history" do
|
||||
test "fetches native coin price history", %{bypass: bypass} do
|
||||
previous_days = 5
|
||||
|
||||
Bypass.expect_once(
|
||||
bypass,
|
||||
"GET",
|
||||
"/assetChartPoints/MA120/Ethereum/0x0000000000000000000000000000000000000000",
|
||||
fn conn ->
|
||||
"starttime=" <> from_str = conn.query_string
|
||||
{from_int, "&endtime=" <> to_str} = Integer.parse(from_str)
|
||||
{to_int, ""} = Integer.parse(to_str)
|
||||
assert to_int - from_int == previous_days * 24 * 60 * 60
|
||||
Conn.resp(conn, 200, @native_coin_history_json)
|
||||
end
|
||||
)
|
||||
|
||||
assert {:ok,
|
||||
[
|
||||
%{
|
||||
closing_price: Decimal.new("2769.8064113406717"),
|
||||
date: ~D[2025-11-22],
|
||||
opening_price: Decimal.new("2765.03927910001"),
|
||||
secondary_coin: false
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("2801.5141364778983"),
|
||||
date: ~D[2025-11-23],
|
||||
opening_price: Decimal.new("2768.2062977060264"),
|
||||
secondary_coin: false
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("2952.88025966878"),
|
||||
date: ~D[2025-11-24],
|
||||
opening_price: Decimal.new("2798.783002464827"),
|
||||
secondary_coin: false
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("2959.1210339664754"),
|
||||
date: ~D[2025-11-25],
|
||||
opening_price: Decimal.new("2949.9206856724004"),
|
||||
secondary_coin: false
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("3027.151520522141"),
|
||||
date: ~D[2025-11-26],
|
||||
opening_price: Decimal.new("2959.062301250391"),
|
||||
secondary_coin: false
|
||||
}
|
||||
]} ==
|
||||
DIA.fetch_native_coin_price_history(previous_days)
|
||||
end
|
||||
end
|
||||
|
||||
describe "secondary_coin_price_history_fetching_enabled?" do
|
||||
test "returns true if secondary_coin_address_hash is configured" do
|
||||
assert DIA.secondary_coin_price_history_fetching_enabled?()
|
||||
end
|
||||
|
||||
test "returns false if secondary_coin_address_hash is not configured", %{env: env} do
|
||||
Application.put_env(:explorer, DIA, Keyword.merge(env, secondary_coin_address_hash: nil))
|
||||
refute DIA.secondary_coin_price_history_fetching_enabled?()
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_secondary_coin_price_history" do
|
||||
test "fetches secondary coin price history", %{bypass: bypass} do
|
||||
previous_days = 5
|
||||
|
||||
Bypass.expect_once(
|
||||
bypass,
|
||||
"GET",
|
||||
"/assetChartPoints/MA120/Ethereum/0x0000000000000000000000000000000000000001",
|
||||
fn conn ->
|
||||
"starttime=" <> from_str = conn.query_string
|
||||
{from_int, "&endtime=" <> to_str} = Integer.parse(from_str)
|
||||
{to_int, ""} = Integer.parse(to_str)
|
||||
assert to_int - from_int == previous_days * 24 * 60 * 60
|
||||
Conn.resp(conn, 200, @secondary_coin_history_json)
|
||||
end
|
||||
)
|
||||
|
||||
assert {:ok,
|
||||
[
|
||||
%{
|
||||
closing_price: Decimal.new("1.0014961501515858"),
|
||||
date: ~D[2025-11-22],
|
||||
opening_price: Decimal.new("0.9998295379999973"),
|
||||
secondary_coin: true
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("1.0003179715114685"),
|
||||
date: ~D[2025-11-23],
|
||||
opening_price: Decimal.new("0.9998108086782626"),
|
||||
secondary_coin: true
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("0.9999003120252966"),
|
||||
date: ~D[2025-11-24],
|
||||
opening_price: Decimal.new("1.0000636244141254"),
|
||||
secondary_coin: true
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("0.9997550066438543"),
|
||||
date: ~D[2025-11-25],
|
||||
opening_price: Decimal.new("1.0006631013107907"),
|
||||
secondary_coin: true
|
||||
},
|
||||
%{
|
||||
closing_price: Decimal.new("0.9997050546597661"),
|
||||
date: ~D[2025-11-26],
|
||||
opening_price: Decimal.new("0.9998251538809131"),
|
||||
secondary_coin: true
|
||||
}
|
||||
]} ==
|
||||
DIA.fetch_secondary_coin_price_history(previous_days)
|
||||
end
|
||||
end
|
||||
|
||||
describe "market_cap_history_fetching_enabled?" do
|
||||
test "ignored" do
|
||||
assert DIA.market_cap_history_fetching_enabled?() == :ignore
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_market_cap_history" do
|
||||
test "ignored" do
|
||||
assert DIA.fetch_market_cap_history(0) == :ignore
|
||||
end
|
||||
end
|
||||
|
||||
describe "tvl_history_fetching_enabled?" do
|
||||
test "ignored" do
|
||||
assert DIA.tvl_history_fetching_enabled?() == :ignore
|
||||
end
|
||||
end
|
||||
|
||||
describe "fetch_tvl_history" do
|
||||
test "ignored" do
|
||||
assert DIA.fetch_tvl_history(0) == :ignore
|
||||
end
|
||||
end
|
||||
|
||||
# cspell:disable
|
||||
|
||||
defp json_coin(coin_address_hash, fiat_value, symbol, name) do
|
||||
"""
|
||||
{
|
||||
"Symbol": "#{symbol}",
|
||||
"Name": "#{name}",
|
||||
"Address": "#{coin_address_hash}",
|
||||
"Blockchain": "Ethereum",
|
||||
"Price": #{fiat_value},
|
||||
"PriceYesterday": 2935.2004802640054,
|
||||
"VolumeYesterdayUSD": 6577658642.868258,
|
||||
"Time": "2025-11-27T08:09:59Z",
|
||||
"Source": "diadata.org",
|
||||
"Signature": "0x5b080262b6ee4f5303251ef65dc81b61dc6e5ceb6e2ff0d9ca8a6c9175c0bef472bbadfe4a3a339134ebc094b4336fb6ad0e50bac3622968bb03819789b6dbea01"
|
||||
}
|
||||
"""
|
||||
end
|
||||
|
||||
defp json_tokens_list do
|
||||
"""
|
||||
[
|
||||
{
|
||||
"Asset": {
|
||||
"Symbol": "ETH",
|
||||
"Name": "Ether",
|
||||
"Address": "0x0000000000000000000000000000000000000000",
|
||||
"Decimals": 18,
|
||||
"Blockchain": "Ethereum"
|
||||
},
|
||||
"Volume": 6577658642.868258,
|
||||
"VolumeUSD": 0,
|
||||
"Index": 0
|
||||
},
|
||||
{
|
||||
"Asset": {
|
||||
"Symbol": "USDC",
|
||||
"Name": "USD Coin",
|
||||
"Address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
|
||||
"Decimals": 6,
|
||||
"Blockchain": "Ethereum"
|
||||
},
|
||||
"Volume": 2756384237.21397,
|
||||
"VolumeUSD": 0,
|
||||
"Index": 0
|
||||
},
|
||||
{
|
||||
"Asset": {
|
||||
"Symbol": "USDT",
|
||||
"Name": "Tether USD",
|
||||
"Address": "0xdAC17F958D2ee523a2206206994597C13D831ec7",
|
||||
"Decimals": 6,
|
||||
"Blockchain": "Ethereum"
|
||||
},
|
||||
"Volume": 504424765.759824,
|
||||
"VolumeUSD": 0,
|
||||
"Index": 0
|
||||
}
|
||||
]
|
||||
"""
|
||||
end
|
||||
|
||||
# cspell:enable
|
||||
end
|
||||
@@ -0,0 +1,54 @@
|
||||
defmodule Explorer.BenchmarkCase do
|
||||
@moduledoc """
|
||||
This module defines the benchmark case to be used by benchmarks.
|
||||
|
||||
This module provides common setup and utilities for benchmarking,
|
||||
similar to DataCase but optimized for benchmarking needs, including:
|
||||
|
||||
- Database sandbox management
|
||||
- Factory imports
|
||||
- Common helpers
|
||||
|
||||
## Example
|
||||
|
||||
```elixir
|
||||
defmodule Explorer.MyBenchmark do
|
||||
use Explorer.BenchmarkCase
|
||||
|
||||
def run do
|
||||
Benchee.run(...)
|
||||
end
|
||||
end
|
||||
|
||||
# Run the benchmark
|
||||
Explorer.MyBenchmark.run()
|
||||
```
|
||||
"""
|
||||
|
||||
defmacro __using__(_opts) do
|
||||
caller_file = __CALLER__.file
|
||||
path = caller_file |> String.replace(Path.extname(caller_file), ".benchee")
|
||||
|
||||
quote do
|
||||
import Explorer.Factory
|
||||
|
||||
@path unquote(path)
|
||||
|
||||
@doc """
|
||||
Resets the database for consistent benchmarks
|
||||
"""
|
||||
def reset_db do
|
||||
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo, ownership_timeout: :infinity)
|
||||
end
|
||||
|
||||
@doc """
|
||||
Setup common benchmark environment
|
||||
"""
|
||||
def benchmark_setup do
|
||||
{:ok, _} = Application.ensure_all_started(:explorer)
|
||||
|
||||
:ok
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -105,7 +105,7 @@ defmodule Indexer.Memory.Monitor do
|
||||
default_limit = 1 <<< 30
|
||||
|
||||
percentage =
|
||||
case Application.get_env(:explorer, :mode) do
|
||||
case Explorer.mode() do
|
||||
:indexer -> 100
|
||||
_ -> Application.get_env(:indexer, :system_memory_percentage)
|
||||
end
|
||||
|
||||
@@ -17,7 +17,25 @@ defmodule NFTMediaHandler.DispatcherInterface do
|
||||
"""
|
||||
@impl true
|
||||
def init(_) do
|
||||
{:ok, %{used_nodes: [], unused_nodes: []}}
|
||||
{:ok, nil, {:continue, 1}}
|
||||
end
|
||||
|
||||
@impl true
|
||||
def handle_continue(attempt, _state) do
|
||||
attempt |> Kernel.**(3) |> :timer.seconds() |> :timer.sleep()
|
||||
|
||||
get_indexer_nodes()
|
||||
|> case do
|
||||
[] ->
|
||||
if attempt < 5 do
|
||||
{:noreply, nil, {:continue, attempt + 1}}
|
||||
else
|
||||
raise "No indexer nodes discovered after #{attempt} attempts"
|
||||
end
|
||||
|
||||
nodes ->
|
||||
{:noreply, %{used_nodes: [], unused_nodes: nodes}}
|
||||
end
|
||||
end
|
||||
|
||||
@doc """
|
||||
@@ -29,8 +47,7 @@ defmodule NFTMediaHandler.DispatcherInterface do
|
||||
{used, unused, node_to_call} =
|
||||
case unused_nodes do
|
||||
[] ->
|
||||
Node.list()
|
||||
|> Enum.filter(&Helper.indexer_node?/1)
|
||||
get_indexer_nodes()
|
||||
|> case do
|
||||
[] ->
|
||||
raise "No indexer nodes discovered"
|
||||
@@ -88,4 +105,9 @@ defmodule NFTMediaHandler.DispatcherInterface do
|
||||
defp remote_call(args, function, _node, false) do
|
||||
apply(Indexer.NFTMediaHandler.Queue, function, args)
|
||||
end
|
||||
|
||||
defp get_indexer_nodes do
|
||||
Node.list()
|
||||
|> Enum.filter(&Helper.indexer_node?/1)
|
||||
end
|
||||
end
|
||||
|
||||
@@ -275,6 +275,7 @@ defmodule ConfigHelper do
|
||||
| Source.CryptoCompare
|
||||
| Source.CryptoRank
|
||||
| Source.DefiLlama
|
||||
| Source.DIA
|
||||
| Source.Mobula
|
||||
| nil
|
||||
def market_source(env_var) do
|
||||
@@ -284,6 +285,7 @@ defmodule ConfigHelper do
|
||||
"crypto_compare" => Source.CryptoCompare,
|
||||
"crypto_rank" => Source.CryptoRank,
|
||||
"defillama" => Source.DefiLlama,
|
||||
"dia" => Source.DIA,
|
||||
"mobula" => Source.Mobula,
|
||||
"" => nil,
|
||||
nil => nil
|
||||
|
||||
+9
-1
@@ -401,7 +401,7 @@ config :explorer, Explorer.Chain.Cache.Counters.AverageBlockTime,
|
||||
num_of_blocks: ConfigHelper.parse_integer_env_var("CACHE_AVERAGE_BLOCK_TIME_WINDOW", 100)
|
||||
|
||||
config :explorer, Explorer.Market.MarketHistoryCache,
|
||||
cache_period: ConfigHelper.parse_time_env_var("CACHE_MARKET_HISTORY_PERIOD", "6h")
|
||||
cache_period: ConfigHelper.parse_time_env_var("CACHE_MARKET_HISTORY_PERIOD", "1h")
|
||||
|
||||
config :explorer, Explorer.Chain.Cache.Counters.AddressTransactionsCount,
|
||||
cache_period: ConfigHelper.parse_time_env_var("CACHE_ADDRESS_TRANSACTIONS_COUNTER_PERIOD", "1h")
|
||||
@@ -427,6 +427,8 @@ config :explorer, Explorer.Chain.Cache.Counters.NewPendingTransactionsCount,
|
||||
cache_period: ConfigHelper.parse_time_env_var("CACHE_FRESH_PENDING_TRANSACTIONS_COUNTER_PERIOD", "5m"),
|
||||
enable_consolidation: true
|
||||
|
||||
config :explorer, Explorer.Market, enabled: !disable_exchange_rates?
|
||||
|
||||
config :explorer, Explorer.Market.Source,
|
||||
native_coin_source:
|
||||
ConfigHelper.market_source("MARKET_NATIVE_COIN_SOURCE") || ConfigHelper.market_source("EXCHANGE_RATES_SOURCE"),
|
||||
@@ -508,6 +510,12 @@ config :explorer, Explorer.Market.Source.Mobula,
|
||||
secondary_coin_id:
|
||||
System.get_env("MARKET_MOBULA_SECONDARY_COIN_ID") || System.get_env("EXCHANGE_RATES_MOBULA_SECONDARY_COIN_ID")
|
||||
|
||||
config :explorer, Explorer.Market.Source.DIA,
|
||||
blockchain: System.get_env("MARKET_DIA_BLOCKCHAIN"),
|
||||
base_url: System.get_env("MARKET_DIA_BASE_URL", "https://api.diadata.org/v1"),
|
||||
coin_address_hash: System.get_env("MARKET_DIA_COIN_ADDRESS_HASH"),
|
||||
secondary_coin_address_hash: System.get_env("MARKET_DIA_SECONDARY_COIN_ADDRESS_HASH")
|
||||
|
||||
config :explorer, Explorer.Market.Fetcher.Coin,
|
||||
store: :ets,
|
||||
enabled: !disable_exchange_rates? && ConfigHelper.parse_bool_env_var("MARKET_COIN_FETCHER_ENABLED", "true"),
|
||||
|
||||
@@ -82,6 +82,10 @@ DISABLE_MARKET=true
|
||||
# MARKET_MOBULA_API_KEY=
|
||||
# MARKET_MOBULA_COIN_ID=
|
||||
# MARKET_MOBULA_SECONDARY_COIN_ID=
|
||||
# MARKET_DIA_BLOCKCHAIN=
|
||||
# MARKET_DIA_BASE_URL=
|
||||
# MARKET_DIA_COIN_ADDRESS_HASH=
|
||||
# MARKET_DIA_SECONDARY_COIN_ADDRESS_HASH=
|
||||
# MARKET_COIN_FETCHER_ENABLED=
|
||||
# MARKET_COIN_CACHE_PERIOD=
|
||||
# MARKET_TOKENS_FETCHER_ENABLED=
|
||||
|
||||
Reference in New Issue
Block a user