Exchange rates CoinMarketCap source module/CoinGecko API key support

pull/5613/head
Viktor Baranov 3 years ago
parent 9e64d3685b
commit cc6a03508f
  1. 4
      .dialyzer-ignore
  2. 1
      CHANGELOG.md
  3. 22
      apps/explorer/config/config.exs
  4. 4
      apps/explorer/lib/explorer/chain/supply/token_bridge.ex
  5. 28
      apps/explorer/lib/explorer/exchange_rates/source.ex
  6. 93
      apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
  7. 189
      apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex
  8. 23
      apps/explorer/lib/explorer/exchange_rates/source/token_bridge.ex
  9. 2
      apps/explorer/lib/explorer/known_tokens/source.ex
  10. 2
      apps/explorer/test/explorer/exchange_rates/source/token_bridge_test.exs
  11. 3
      apps/explorer/test/support/fakes/no_op_source.ex
  12. 3
      apps/explorer/test/support/fakes/one_coin_source.ex
  13. 5
      docker-compose/envs/common-blockscout.env
  14. 13
      docker/Makefile

@ -20,8 +20,8 @@ lib/block_scout_web/views/layout_view.ex:145: The call 'Elixir.Poison.Parser':'p
lib/block_scout_web/views/layout_view.ex:237: The call 'Elixir.Poison.Parser':'parse!'
lib/explorer/smart_contract/reader.ex:435
lib/indexer/fetcher/token_total_supply_on_demand.ex:16
lib/explorer/exchange_rates/source.ex:110
lib/explorer/exchange_rates/source.ex:113
lib/explorer/exchange_rates/source.ex:116
lib/explorer/exchange_rates/source.ex:119
lib/explorer/smart_contract/solidity/verifier.ex:223
lib/block_scout_web/templates/address_contract/index.html.eex:158
lib/block_scout_web/templates/address_contract/index.html.eex:195

@ -1,6 +1,7 @@
## Current
### Features
- [#5613](https://github.com/blockscout/blockscout/pull/5613) - Exchange rates CoinMarketCap source module
- [#5588](https://github.com/blockscout/blockscout/pull/5588) - Add broadcasting of coin balance
- [#5479](https://github.com/blockscout/blockscout/pull/5479) - Remake of solidity verifier module; Verification UX improvements
- [#5540](https://github.com/blockscout/blockscout/pull/5540) - Tx page: scroll to selected tab's data

@ -12,7 +12,6 @@ disable_webapp = System.get_env("DISABLE_WEBAPP")
config :explorer,
ecto_repos: [Explorer.Repo],
coin: System.get_env("COIN") || "POA",
coingecko_coin_id: System.get_env("COINGECKO_COIN_ID"),
token_functions_reader_max_retries: 3,
allowed_evm_versions:
System.get_env("ALLOWED_EVM_VERSIONS") ||
@ -142,7 +141,22 @@ config :explorer, Explorer.Counters.Bridge,
update_interval_in_seconds: bridge_market_cap_update_interval || 30 * 60,
disable_lp_tokens_in_market_cap: System.get_env("DISABLE_LP_TOKENS_IN_MARKET_CAP") == "true"
config :explorer, Explorer.ExchangeRates, enabled: System.get_env("DISABLE_EXCHANGE_RATES") != "true", store: :ets
config :explorer, Explorer.ExchangeRates,
enabled: System.get_env("DISABLE_EXCHANGE_RATES") != "true",
store: :ets,
coingecko_coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"),
coingecko_api_key: System.get_env("EXCHANGE_RATES_COINGECKO_API_KEY"),
coinmarketcap_api_key: System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY")
exchange_rates_source =
cond do
System.get_env("EXCHANGE_RATES_SOURCE") == "token_bridge" -> Explorer.ExchangeRates.Source.TokenBridge
System.get_env("EXCHANGE_RATES_SOURCE") == "coin_gecko" -> Explorer.ExchangeRates.Source.CoinGecko
System.get_env("EXCHANGE_RATES_SOURCE") == "coin_market_cap" -> Explorer.ExchangeRates.Source.CoinMarketCap
true -> Explorer.ExchangeRates.Source.CoinGecko
end
config :explorer, Explorer.ExchangeRates.Source, source: exchange_rates_source
config :explorer, Explorer.KnownTokens, enabled: System.get_env("DISABLE_KNOWN_TOKENS") != "true", store: :ets
@ -221,10 +235,6 @@ case System.get_env("SUPPLY_MODULE") do
:ok
end
if System.get_env("SOURCE_MODULE") == "TokenBridge" do
config :explorer, Explorer.ExchangeRates.Source, source: Explorer.ExchangeRates.Source.TokenBridge
end
config :explorer,
solc_bin_api_url: "https://solc-bin.ethereum.org",
checksum_function: System.get_env("CHECKSUM_FUNCTION") && String.to_atom(System.get_env("CHECKSUM_FUNCTION"))

@ -92,8 +92,10 @@ defmodule Explorer.Chain.Supply.TokenBridge do
def total_market_cap_from_omni_bridge, do: Bridge.fetch_omni_bridge_market_cap()
def total_chain_supply do
source = Application.get_env(:explorer, Source)[:source]
usd_value =
case Source.fetch_exchange_rates(Source.CoinGecko) do
case Source.fetch_exchange_rates(source) do
{:ok, [rates]} ->
rates.usd_value

@ -11,25 +11,28 @@ defmodule Explorer.ExchangeRates.Source do
@spec fetch_exchange_rates(module) :: {:ok, [Token.t()]} | {:error, any}
def fetch_exchange_rates(source \\ exchange_rates_source()) do
source_url = source.source_url()
fetch_exchange_rates_request(source, source_url)
fetch_exchange_rates_request(source, source_url, source.headers())
end
@spec fetch_exchange_rates_for_token(String.t()) :: {:ok, [Token.t()]} | {:error, any}
def fetch_exchange_rates_for_token(symbol) do
source_url = Source.CoinGecko.source_url(symbol)
fetch_exchange_rates_request(Source.CoinGecko, source_url)
source = Application.get_env(:explorer, Source)[:source]
source_url = source.source_url(symbol)
fetch_exchange_rates_request(source, source_url, source.headers())
end
@spec fetch_exchange_rates_for_token_address(String.t()) :: {:ok, [Token.t()]} | {:error, any}
def fetch_exchange_rates_for_token_address(address_hash) do
source_url = Source.CoinGecko.source_url(address_hash)
fetch_exchange_rates_request(Source.CoinGecko, source_url)
source = Application.get_env(:explorer, Source)[:source]
source_url = source.source_url(address_hash)
fetch_exchange_rates_request(source, source_url, source.headers())
end
defp fetch_exchange_rates_request(_source, source_url) when is_nil(source_url), do: {:error, "Source URL is nil"}
defp fetch_exchange_rates_request(_source, source_url, _headers) when is_nil(source_url),
do: {:error, "Source URL is nil"}
defp fetch_exchange_rates_request(source, source_url) do
case http_request(source_url) do
defp fetch_exchange_rates_request(source, source_url, headers) do
case http_request(source_url, headers) do
{:ok, result} = resp ->
if is_map(result) do
result_formatted =
@ -58,6 +61,8 @@ defmodule Explorer.ExchangeRates.Source do
@callback source_url(String.t()) :: String.t() | :ignore
@callback headers() :: [any]
def headers do
[{"Content-Type", "application/json"}]
end
@ -82,7 +87,8 @@ defmodule Explorer.ExchangeRates.Source do
@spec exchange_rates_source() :: module()
defp exchange_rates_source do
config(:source) || Explorer.ExchangeRates.Source.CoinGecko
source = Application.get_env(:explorer, Source)[:source]
config(:source) || source
end
@spec config(atom()) :: term
@ -90,8 +96,8 @@ defmodule Explorer.ExchangeRates.Source do
Application.get_env(:explorer, __MODULE__, [])[key]
end
def http_request(source_url) do
case HTTPoison.get(source_url, headers()) do
def http_request(source_url, additional_headers) do
case HTTPoison.get(source_url, headers() ++ additional_headers) do
{:ok, %Response{body: body, status_code: 200}} ->
parse_http_success_response(body)

@ -3,7 +3,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
Adapter for fetching exchange rates from https://coingecko.com
"""
alias Explorer.Chain
alias Explorer.{Chain, ExchangeRates}
alias Explorer.ExchangeRates.{Source, Token}
import Source, only: [to_decimal: 1]
@ -44,45 +44,9 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
@impl Source
def format_data(_), do: []
defp get_last_updated(market_data) do
last_updated_data = market_data && market_data["last_updated"]
if last_updated_data do
{:ok, last_updated, 0} = DateTime.from_iso8601(last_updated_data)
last_updated
else
nil
end
end
defp get_current_price(market_data) do
if market_data["current_price"] do
to_decimal(market_data["current_price"]["usd"])
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
@impl Source
def source_url do
explicit_coin_id = Application.get_env(:explorer, :coingecko_coin_id)
explicit_coin_id = Application.get_env(:explorer, ExchangeRates)[:coingecko_coin_id]
{:ok, id} =
if explicit_coin_id do
@ -123,8 +87,13 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
end
end
defp base_url do
config(:base_url) || "https://api.coingecko.com/api/v3"
@impl Source
def headers do
[{"X-Cg-Pro-Api-Key", "#{api_key()}"}]
end
defp api_key do
Application.get_env(:explorer, ExchangeRates)[:coingecko_api_key]
end
def coin_id do
@ -143,7 +112,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
symbol_downcase = String.downcase(symbol)
case Source.http_request(url) do
case Source.http_request(url, headers()) do
{:ok, data} = resp ->
if is_list(data) do
symbol_data =
@ -166,10 +135,50 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
end
end
defp get_last_updated(market_data) do
last_updated_data = market_data && market_data["last_updated"]
if last_updated_data do
{:ok, last_updated, 0} = DateTime.from_iso8601(last_updated_data)
last_updated
else
nil
end
end
defp get_current_price(market_data) do
if market_data["current_price"] do
to_decimal(market_data["current_price"]["usd"])
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 base_url do
config(:base_url) || "https://pro-api.coingecko.com/api/v3"
end
defp get_btc_price(currency \\ "usd") do
url = "#{base_url()}/exchange_rates"
case Source.http_request(url) do
case Source.http_request(url, headers()) do
{:ok, data} = resp ->
if is_map(data) do
current_price = data["rates"][currency]["value"]

@ -0,0 +1,189 @@
defmodule Explorer.ExchangeRates.Source.CoinMarketCap do
@moduledoc """
Adapter for fetching exchange rates from https://coinmarketcap.com/api/
"""
alias Explorer.{Chain, ExchangeRates}
alias Explorer.ExchangeRates.{Source, Token}
import Source, only: [to_decimal: 1]
@behaviour Source
@impl Source
def format_data(%{"data" => _} = json_data) do
market_data = json_data["data"]
token_properties = get_token_properties(market_data)
last_updated = get_last_updated(token_properties)
current_price = get_current_price(token_properties)
id = token_properties && token_properties["id"]
btc_value = get_btc_value(id, token_properties)
circulating_supply_data = get_circulating_supply(token_properties)
total_supply_data = get_total_supply(token_properties)
market_cap_data_usd = get_market_cap_data_usd(token_properties)
total_volume_data_usd = get_total_volume_data_usd(token_properties)
[
%Token{
available_supply: to_decimal(circulating_supply_data),
total_supply: to_decimal(total_supply_data) || to_decimal(circulating_supply_data),
btc_value: btc_value,
id: id,
last_updated: last_updated,
market_cap_usd: to_decimal(market_cap_data_usd),
name: token_properties && token_properties["name"],
symbol: token_properties && String.upcase(token_properties["symbol"]),
usd_value: current_price,
volume_24h_usd: to_decimal(total_volume_data_usd)
}
]
end
@impl Source
def format_data(_), do: []
@impl Source
def source_url do
coin = Explorer.coin()
symbol = if coin, do: String.upcase(Explorer.coin()), else: nil
if symbol, do: "#{api_quotes_latest_url()}?symbol=#{symbol}&CMC_PRO_API_KEY=#{api_key()}", else: nil
end
@impl Source
def source_url(input) do
case Chain.Hash.Address.cast(input) do
{:ok, _} ->
# todo: find symbol by contract address hash
nil
_ ->
symbol = if input, do: input |> String.upcase(), else: nil
if symbol,
do: "#{api_quotes_latest_url()}?symbol=#{symbol}&CMC_PRO_API_KEY=#{api_key()}",
else: nil
end
end
@impl Source
def headers do
[]
end
defp api_key do
Application.get_env(:explorer, ExchangeRates)[:coinmarketcap_api_key]
end
defp get_token_properties(market_data) do
token_values_list =
market_data
|> Map.values()
if Enum.count(token_values_list) > 0 do
token_values = token_values_list |> Enum.at(0)
if Enum.count(token_values) > 0 do
token_values |> Enum.at(0)
else
%{}
end
else
%{}
end
end
defp get_circulating_supply(token_properties) do
token_properties && token_properties["circulating_supply"]
end
defp get_total_supply(token_properties) do
token_properties && token_properties["total_supply"]
end
defp get_market_cap_data_usd(token_properties) do
token_properties && token_properties["quote"] &&
token_properties["quote"]["USD"] &&
token_properties["quote"]["USD"]["market_cap"]
end
defp get_total_volume_data_usd(token_properties) do
token_properties && token_properties["quote"] &&
token_properties["quote"]["USD"] &&
token_properties["quote"]["USD"]["volume_24h"]
end
defp get_last_updated(token_properties) do
last_updated_data = token_properties && token_properties["last_updated"]
if last_updated_data do
{:ok, last_updated, 0} = DateTime.from_iso8601(last_updated_data)
last_updated
else
nil
end
end
defp get_current_price(token_properties) do
if token_properties && token_properties["quote"] && token_properties["quote"]["USD"] &&
token_properties["quote"]["USD"]["price"] do
to_decimal(token_properties["quote"]["USD"]["price"])
else
1
end
end
defp get_btc_value(id, token_properties) do
case get_btc_price() do
{:ok, price} ->
btc_price = to_decimal(price)
current_price = get_current_price(token_properties)
if id != "btc" && current_price && btc_price do
Decimal.div(current_price, btc_price)
else
1
end
_ ->
1
end
end
defp base_url do
config(:base_url) || "https://pro-api.coinmarketcap.com/v2"
end
defp api_quotes_latest_url do
"#{base_url()}/cryptocurrency/quotes/latest"
end
defp get_btc_price(currency \\ "usd") do
url = "#{api_quotes_latest_url()}?symbol=BTC&CMC_PRO_API_KEY=#{api_key()}"
case Source.http_request(url, headers()) do
{:ok, data} = resp ->
if is_map(data) do
current_price = data["rates"][currency]["value"]
{:ok, current_price}
else
resp
end
resp ->
resp
end
end
@spec config(atom()) :: term
defp config(key) do
Application.get_env(:explorer, __MODULE__, [])[key]
end
end

@ -23,6 +23,19 @@ defmodule Explorer.ExchangeRates.Source.TokenBridge do
[token_data]
end
@impl Source
def source_url do
secondary_source().source_url()
end
@impl Source
def source_url(_), do: :ignore
@impl Source
def headers do
[]
end
defp build_struct(original_token) do
%Token{
available_supply: to_decimal(Chain.circulating_supply()),
@ -46,17 +59,9 @@ defmodule Explorer.ExchangeRates.Source.TokenBridge do
|> Decimal.mult(original_token.usd_value)
end
@impl Source
def source_url do
secondary_source().source_url()
end
@impl Source
def source_url(_), do: :ignore
@spec secondary_source() :: module()
defp secondary_source do
config(:secondary_source) || Explorer.ExchangeRates.Source.CoinGecko
config(:secondary_source) || Application.get_env(:explorer, Explorer.ExchangeRates.Source)[:source]
end
@spec config(atom()) :: term

@ -11,7 +11,7 @@ defmodule Explorer.KnownTokens.Source do
"""
@spec fetch_known_tokens() :: {:ok, [Hash.Address.t()]} | {:error, any}
def fetch_known_tokens(source \\ known_tokens_source()) do
Source.http_request(source.source_url())
Source.http_request(source.source_url(), source.headers())
end
@doc """

@ -1,6 +1,7 @@
defmodule Explorer.ExchangeRates.Source.TokenBridgeTest do
use Explorer.DataCase
alias Explorer.ExchangeRates
alias Explorer.ExchangeRates.Source.CoinGecko
alias Explorer.ExchangeRates.Source.TokenBridge
alias Explorer.ExchangeRates.Token
@ -26,6 +27,7 @@ defmodule Explorer.ExchangeRates.Source.TokenBridgeTest do
describe "format_data/1" do
setup do
bypass = Bypass.open()
Application.put_env(:explorer, ExchangeRates, source: "coin_gecko")
Application.put_env(:explorer, CoinGecko, base_url: "http://localhost:#{bypass.port}")
{:ok, bypass: bypass}

@ -13,4 +13,7 @@ defmodule Explorer.ExchangeRates.Source.NoOpSource do
@impl Source
def source_url(_), do: :ignore
@impl Source
def headers, do: []
end

@ -29,4 +29,7 @@ defmodule Explorer.ExchangeRates.Source.OneCoinSource do
@impl Source
def source_url(_), do: :ignore
@impl Source
def headers, do: []
end

@ -20,7 +20,6 @@ BLOCKSCOUT_PROTOCOL=
PORT=4000
# COIN=
# COIN_NAME=
# COINGECKO_COIN_ID=
# METADATA_CONTRACT=
# VALIDATORS_CONTRACT=
# KEYS_MANAGER_CONTRACT=
@ -29,7 +28,9 @@ PORT=4000
EMISSION_FORMAT=DEFAULT
# CHAIN_SPEC_PATH=
# SUPPLY_MODULE=
# SOURCE_MODULE=
# EXCHANGE_RATES_SOURCE=
# EXCHANGE_RATES_COINGECKO_COIN_ID=
# EXCHANGE_RATES_COINMARKETCAP_API_KEY=
POOL_SIZE=40
POOL_SIZE_API=10
ECTO_USE_SSL=false

@ -79,9 +79,6 @@ endif
ifdef SUPPLY_MODULE
BLOCKSCOUT_CONTAINER_PARAMS += -e 'SUPPLY_MODULE=$(SUPPLY_MODULE)'
endif
ifdef SOURCE_MODULE
BLOCKSCOUT_CONTAINER_PARAMS += -e 'SOURCE_MODULE=$(SOURCE_MODULE)'
endif
ifdef POOL_SIZE
BLOCKSCOUT_CONTAINER_PARAMS += -e 'POOL_SIZE=$(POOL_SIZE)'
endif
@ -217,8 +214,14 @@ endif
ifdef CHECKSUM_FUNCTION
BLOCKSCOUT_CONTAINER_PARAMS += -e 'CHECKSUM_FUNCTION=$(CHECKSUM_FUNCTION)'
endif
ifdef COINGECKO_COIN_ID
BLOCKSCOUT_CONTAINER_PARAMS += -e 'COINGECKO_COIN_ID=$(COINGECKO_COIN_ID)'
ifdef EXCHANGE_RATES_SOURCE
BLOCKSCOUT_CONTAINER_PARAMS += -e 'EXCHANGE_RATES_SOURCE=$(EXCHANGE_RATES_SOURCE)'
endif
ifdef EXCHANGE_RATES_COINGECKO_COIN_ID
BLOCKSCOUT_CONTAINER_PARAMS += -e 'EXCHANGE_RATES_COINGECKO_COIN_ID=$(EXCHANGE_RATES_COINGECKO_COIN_ID)'
endif
ifdef EXCHANGE_RATES_COINMARKETCAP_API_KEY
BLOCKSCOUT_CONTAINER_PARAMS += -e 'EXCHANGE_RATES_COINMARKETCAP_API_KEY=$(EXCHANGE_RATES_COINMARKETCAP_API_KEY)'
endif
ifdef DISABLE_EXCHANGE_RATES
BLOCKSCOUT_CONTAINER_PARAMS += -e 'DISABLE_EXCHANGE_RATES=$(DISABLE_EXCHANGE_RATES)'

Loading…
Cancel
Save