Skip to content

Commit

Permalink
Exchange rates CoinMarketCap source module
Browse files Browse the repository at this point in the history
  • Loading branch information
vbaranov committed May 26, 2022
1 parent 9e64d36 commit cfde2df
Show file tree
Hide file tree
Showing 11 changed files with 224 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .dialyzer-ignore
Original file line number Diff line number Diff line change
Expand Up @@ -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/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
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
21 changes: 15 additions & 6 deletions apps/explorer/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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") ||
Expand Down Expand Up @@ -142,7 +141,21 @@ 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"),
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.CoinMarketCap
end

config :explorer, Explorer.ExchangeRates.Source, source: exchange_rates_source

config :explorer, Explorer.KnownTokens, enabled: System.get_env("DISABLE_KNOWN_TOKENS") != "true", store: :ets

Expand Down Expand Up @@ -221,10 +234,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"))
Expand Down
4 changes: 3 additions & 1 deletion apps/explorer/lib/explorer/chain/supply/token_bridge.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
13 changes: 8 additions & 5 deletions apps/explorer/lib/explorer/exchange_rates/source.ex
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,16 @@ defmodule Explorer.ExchangeRates.Source do

@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)
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)
end

defp fetch_exchange_rates_request(_source, source_url) when is_nil(source_url), do: {:error, "Source URL is nil"}
Expand Down Expand Up @@ -82,7 +84,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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -82,7 +82,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do

@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
Expand Down
180 changes: 180 additions & 0 deletions apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
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: []

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

@impl Source
def source_url do
coin = Explorer.coin()
symbol = if coin, do: String.upcase(Explorer.coin()), else: nil

if symbol, do: "#{base_url()}/cryptocurrency/quotes/latest?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: "#{base_url()}/cryptocurrency/quotes/latest?symbol=#{symbol}&CMC_PRO_API_KEY=#{api_key()}",
else: nil
end
end

defp base_url do
config(:base_url) || "https://pro-api.coinmarketcap.com/v2"
end

defp get_btc_price(currency \\ "usd") do
url = "#{base_url()}/cryptocurrency/quotes/latest?symbol=BTC&CMC_PRO_API_KEY=#{api_key()}"

case Source.http_request(url) 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
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ defmodule Explorer.ExchangeRates.Source.TokenBridge do

@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
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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}
Expand Down
5 changes: 3 additions & 2 deletions docker-compose/envs/common-blockscout.env
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@ BLOCKSCOUT_PROTOCOL=
PORT=4000
# COIN=
# COIN_NAME=
# COINGECKO_COIN_ID=
# METADATA_CONTRACT=
# VALIDATORS_CONTRACT=
# KEYS_MANAGER_CONTRACT=
Expand All @@ -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
Expand Down
13 changes: 8 additions & 5 deletions docker/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)'
Expand Down

0 comments on commit cfde2df

Please sign in to comment.