Merge pull request #3470 from poanetwork/vb-address-tokens-sum

Display sum of tokens' USD value at tokens holder's address page
pull/3479/head
Victor Baranov 4 years ago committed by GitHub
commit 52563b5a1c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 4
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex
  3. 6
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex
  4. 3
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  5. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  6. 18
      apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
  7. 4
      apps/block_scout_web/priv/gettext/default.pot
  8. 4
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  9. 7
      apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
  10. 22
      apps/explorer/config/config.exs
  11. 2
      apps/explorer/lib/explorer/application.ex
  12. 25
      apps/explorer/lib/explorer/chain.ex
  13. 132
      apps/explorer/lib/explorer/chain/cache/token_exchange_rate.ex
  14. 12
      apps/explorer/lib/explorer/chain/currency_helpers.ex
  15. 16
      apps/explorer/lib/explorer/chain/supply/token_bridge.ex
  16. 112
      apps/explorer/lib/explorer/counters/address_tokens_usd_sum.ex
  17. 41
      apps/explorer/lib/explorer/counters/bridge.ex

@ -1,6 +1,7 @@
## Current
### Features
- [#3470](https://github.com/poanetwork/blockscout/pull/3470) - Display sum of tokens' USD value at tokens holder's address page
- [#3462](https://github.com/poanetwork/blockscout/pull/3462) - Display price for bridged tokens
### Fixes

@ -17,13 +17,13 @@ defmodule BlockScoutWeb.AddressTokenBalanceController do
conn
|> put_status(200)
|> put_layout(false)
|> render("_token_balances.html", token_balances: token_balances)
|> render("_token_balances.html", address_hash: address_hash, token_balances: token_balances)
_ ->
conn
|> put_status(200)
|> put_layout(false)
|> render("_token_balances.html", token_balances: [])
|> render("_token_balances.html", address_hash: address_hash, token_balances: [])
end
else
_ ->

@ -32,7 +32,11 @@ defmodule BlockScoutWeb.Tokens.HolderController do
token_balances_json =
Enum.map(token_balances_paginated, fn token_balance ->
View.render_to_string(HolderView, "_token_balances.html", token_balance: token_balance, token: token)
View.render_to_string(HolderView, "_token_balances.html",
address_hash: address_hash,
token_balance: token_balance,
token: token
)
end)
json(conn, %{items: token_balances_json, next_page_path: next_page_path})

@ -11,6 +11,9 @@
<i class="fas fa-chevron-down mr-2"></i>
<span data-tokens-count><%= tokens_count_title(@token_balances) %></span>
<%= if @token_balances && Decimal.cmp(address_tokens_usd_sum_cache(@address_hash, @token_balances), Decimal.new(0)) == :gt do %>
(<span data-usd-value=<%= address_tokens_usd_sum_cache(@address_hash, @token_balances) %> ></span>)
<% end %>
</a>
<% else %>
<span data-tokens-count><%= tokens_count_title(@token_balances) %></span>

@ -18,7 +18,7 @@
<p class="mb-0 col-md-6"><%= token_name(token_balance.token) %></p>
<%= if token_balance.token.usd_value do %>
<p class="mb-0 col-md-6 text-right">
<span data-selector="token-balance-usd" data-usd-value="<%= balance_in_usd(token_balance) %>"></span>
<span data-selector="token-balance-usd" data-usd-value="<%= Chain.balance_in_usd(token_balance) %>"></span>
</p>
<% end %>
</div>

@ -1,7 +1,8 @@
defmodule BlockScoutWeb.AddressTokenBalanceView do
use BlockScoutWeb, :view
alias BlockScoutWeb.CurrencyHelpers
alias Explorer.Chain
alias Explorer.Counters.AddressTokenUsdSum
def tokens_count_title(token_balances) do
ngettext("%{count} token", "%{count} tokens", Enum.count(token_balances))
@ -57,7 +58,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
defp sort_2_tokens_by_value_desc_and_name(token_balance1, token_balance2, usd_value1, usd_value2, sort_by_name)
when not is_nil(usd_value1) and not is_nil(usd_value2) do
case Decimal.cmp(balance_in_usd(token_balance1), balance_in_usd(token_balance2)) do
case Decimal.cmp(Chain.balance_in_usd(token_balance1), Chain.balance_in_usd(token_balance2)) do
:gt ->
true
@ -84,16 +85,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceView do
sort_by_name
end
@doc """
Return the balance in usd corresponding to this token. Return nil if the usd_value of the token is not present.
"""
def balance_in_usd(%{token: %{usd_value: nil}}) do
nil
end
def balance_in_usd(token_balance) do
tokens = CurrencyHelpers.divide_decimals(token_balance.value, token_balance.token.decimals)
price = token_balance.token.usd_value
Decimal.mult(tokens, price)
def address_tokens_usd_sum_cache(address, token_balances) do
AddressTokenUsdSum.fetch(address, token_balances)
end
end

@ -1,5 +1,5 @@
#, elixir-format
#: lib/block_scout_web/views/address_token_balance_view.ex:7
#: lib/block_scout_web/views/address_token_balance_view.ex:8
msgid "%{count} token"
msgid_plural "%{count} tokens"
msgstr[0] ""
@ -1262,7 +1262,7 @@ msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:30
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:33
msgid "Search tokens"
msgstr ""

@ -1,5 +1,5 @@
#, elixir-format
#: lib/block_scout_web/views/address_token_balance_view.ex:7
#: lib/block_scout_web/views/address_token_balance_view.ex:8
msgid "%{count} token"
msgid_plural "%{count} tokens"
msgstr[0] ""
@ -1262,7 +1262,7 @@ msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:30
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:33
msgid "Search tokens"
msgstr ""

@ -2,6 +2,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.AddressTokenBalanceView
alias Explorer.Chain
describe "tokens_count_title/1" do
test "returns the title pluralized" do
@ -146,7 +147,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
token_balance = build(:token_balance, value: Decimal.new(10), token: token)
result = AddressTokenBalanceView.balance_in_usd(token_balance)
result = Chain.balance_in_usd(token_balance)
assert Decimal.cmp(result, 30) == :eq
end
@ -159,7 +160,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
token_balance = build(:token_balance, value: 10, token: token)
assert AddressTokenBalanceView.balance_in_usd(token_balance) == nil
assert Chain.balance_in_usd(token_balance) == nil
end
test "consider decimals when computing value" do
@ -170,7 +171,7 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
token_balance = build(:token_balance, value: Decimal.new(10), token: token)
result = AddressTokenBalanceView.balance_in_usd(token_balance)
result = Chain.balance_in_usd(token_balance)
assert Decimal.cmp(result, Decimal.from_float(0.3)) == :eq
end

@ -105,6 +105,28 @@ config :explorer, Explorer.Counters.AddressTransactionsGasUsageCounter,
enable_consolidation: true,
period: address_transactions_gas_usage_counter_cache_period
address_tokens_usd_sum_cache_period =
case Integer.parse(System.get_env("ADDRESS_TOKENS_USD_SUM_CACHE_PERIOD", "")) do
{secs, ""} -> :timer.seconds(secs)
_ -> :timer.hours(1)
end
config :explorer, Explorer.Counters.AddressTokenUsdSum,
enabled: true,
enable_consolidation: true,
period: address_tokens_usd_sum_cache_period
token_exchange_rate_cache_period =
case Integer.parse(System.get_env("TOKEN_EXCHANGE_RATE_CACHE_PERIOD", "")) do
{secs, ""} -> :timer.seconds(secs)
_ -> :timer.hours(1)
end
config :explorer, Explorer.Chain.Cache.TokenExchangeRate,
enabled: true,
enable_consolidation: true,
period: token_exchange_rate_cache_period
token_holders_counter_cache_period =
case Integer.parse(System.get_env("TOKEN_HOLDERS_COUNTER_CACHE_PERIOD", "")) do
{secs, ""} -> :timer.seconds(secs)

@ -76,12 +76,14 @@ defmodule Explorer.Application do
configure(Explorer.ChainSpec.GenesisData),
configure(Explorer.KnownTokens),
configure(Explorer.Market.History.Cataloger),
configure(Explorer.Chain.Cache.TokenExchangeRate),
configure(Explorer.Chain.Transaction.History.Historian),
configure(Explorer.Chain.Events.Listener),
configure(Explorer.Counters.AddressesWithBalanceCounter),
configure(Explorer.Counters.AddressesCounter),
configure(Explorer.Counters.AddressTransactionsCounter),
configure(Explorer.Counters.AddressTransactionsGasUsageCounter),
configure(Explorer.Counters.AddressTokenUsdSum),
configure(Explorer.Counters.TokenHoldersCounter),
configure(Explorer.Counters.TokenTransfersCounter),
configure(Explorer.Counters.AverageBlockTime),

@ -44,6 +44,7 @@ defmodule Explorer.Chain do
Address.TokenBalance,
Block,
BridgedToken,
CurrencyHelpers,
Data,
DecompiledSmartContract,
Hash,
@ -2012,6 +2013,30 @@ defmodule Explorer.Chain do
end
end
@doc """
Return the balance in usd corresponding to this token. Return nil if the usd_value of the token is not present.
"""
def balance_in_usd(%{token: %{usd_value: nil}}) do
nil
end
def balance_in_usd(token_balance) do
tokens = CurrencyHelpers.divide_decimals(token_balance.value, token_balance.token.decimals)
price = token_balance.token.usd_value
Decimal.mult(tokens, price)
end
def address_tokens_usd_sum(token_balances) do
token_balances
|> Enum.reduce(Decimal.new(0), fn token_balance, acc ->
if token_balance.value && token_balance.token.usd_value do
Decimal.add(acc, balance_in_usd(token_balance))
else
acc
end
end)
end
defp contract?(%{contract_code: nil}), do: false
defp contract?(%{contract_code: _}), do: true

@ -0,0 +1,132 @@
defmodule Explorer.Chain.Cache.TokenExchangeRate do
@moduledoc """
Caches Token USD exchange_rate.
"""
use GenServer
alias Explorer.ExchangeRates.Source
@cache_name :token_exchange_rate
@last_update_key "last_update"
@cache_period Application.get_env(:explorer, __MODULE__)[:period]
@ets_opts [
:set,
:named_table,
:public,
read_concurrency: true
]
config = Application.get_env(:explorer, Explorer.Chain.Cache.TokenExchangeRate)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(_args) do
create_cache_table()
{:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}}
end
@impl true
def handle_continue(:ok, %{consolidate?: true} = state) do
{:noreply, state}
end
@impl true
def handle_continue(:ok, state) do
{:noreply, state}
end
@impl true
def handle_info(:consolidate, state) do
{:noreply, state}
end
def cache_key(symbol) do
"token_symbol_exchange_rate_#{symbol}"
end
def fetch(symbol) do
if cache_expired?(symbol) || value_is_empty?(symbol) do
Task.start_link(fn ->
update_cache(symbol)
end)
end
fetch_from_cache(cache_key(symbol))
end
def cache_name, do: @cache_name
defp cache_expired?(symbol) do
updated_at = fetch_from_cache("#{cache_key(symbol)}_#{@last_update_key}")
cond do
is_nil(updated_at) -> true
current_time() - updated_at > @cache_period -> true
true -> false
end
end
defp value_is_empty?(symbol) do
value = fetch_from_cache(cache_key(symbol))
is_nil(value) || value == 0
end
defp update_cache(symbol) do
put_into_cache("#{cache_key(symbol)}_#{@last_update_key}", current_time())
exchange_rate = fetch_token_exchange_rate(symbol)
put_into_cache(cache_key(symbol), exchange_rate)
end
def fetch_token_exchange_rate(symbol) do
case Source.fetch_exchange_rates_for_token(symbol) do
{:ok, [rates]} ->
rates.usd_value
_ ->
nil
end
end
defp fetch_from_cache(key) do
case :ets.lookup(@cache_name, key) do
[{_, value}] ->
value
[] ->
0
end
end
def put_into_cache(key, value) do
if cache_table_exists?() do
:ets.insert(@cache_name, {key, value})
end
end
defp current_time do
utc_now = DateTime.utc_now()
DateTime.to_unix(utc_now, :millisecond)
end
def cache_table_exists? do
:ets.whereis(@cache_name) !== :undefined
end
def create_cache_table do
unless cache_table_exists?() do
:ets.new(@cache_name, @ets_opts)
end
end
def enable_consolidation?, do: @enable_consolidation
end

@ -0,0 +1,12 @@
defmodule Explorer.Chain.CurrencyHelpers do
@moduledoc """
Helper functions for interacting with `t:BlockScoutWeb.ExchangeRates.USD.t/0` values.
"""
@spec divide_decimals(Decimal.t(), Decimal.t()) :: Decimal.t()
def divide_decimals(%{sign: sign, coef: coef, exp: exp}, decimals) do
sign
|> Decimal.new(coef, exp - Decimal.to_integer(decimals))
|> Decimal.normalize()
end
end

@ -11,6 +11,7 @@ defmodule Explorer.Chain.Supply.TokenBridge do
]
alias Explorer.Chain.{BridgedToken, Token, Wei}
alias Explorer.Chain.Cache.TokenExchangeRate, as: TokenExchangeRateCache
alias Explorer.Counters.Bridge
alias Explorer.ExchangeRates.Source
alias Explorer.Repo
@ -198,13 +199,7 @@ defmodule Explorer.Chain.Supply.TokenBridge do
def get_current_price_for_bridged_token(symbol) do
bridged_token_symbol_for_price_fetching = bridged_token_symbol_mapping_to_get_price(symbol)
case Source.fetch_exchange_rates_for_token(bridged_token_symbol_for_price_fetching) do
{:ok, [rates]} ->
rates.usd_value
_ ->
nil
end
TokenExchangeRateCache.fetch(bridged_token_symbol_for_price_fetching)
end
def get_bridged_mainnet_tokens_list do
@ -224,7 +219,12 @@ defmodule Explorer.Chain.Supply.TokenBridge do
bridged_mainnet_tokens_with_supply =
bridged_mainnet_tokens_list
|> Enum.map(fn {bridged_token_hash, bridged_token_symbol} ->
bridged_token_price = Bridge.fetch_token_price(bridged_token_symbol)
bridged_token_price =
if TokenExchangeRateCache.fetch(bridged_token_symbol) > 0 do
TokenExchangeRateCache.fetch(bridged_token_symbol)
else
TokenExchangeRateCache.fetch_token_exchange_rate(bridged_token_symbol)
end
query =
from(t in Token,

@ -0,0 +1,112 @@
defmodule Explorer.Counters.AddressTokenUsdSum do
@moduledoc """
Caches Address tokens USD value.
"""
use GenServer
alias Explorer.Chain
@cache_name :address_tokens_usd_value
@last_update_key "last_update"
@cache_period Application.get_env(:explorer, __MODULE__)[:period]
@ets_opts [
:set,
:named_table,
:public,
read_concurrency: true
]
config = Application.get_env(:explorer, Explorer.Counters.AddressTokenUsdSum)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(_args) do
create_cache_table()
{:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}}
end
@impl true
def handle_continue(:ok, %{consolidate?: true} = state) do
{:noreply, state}
end
@impl true
def handle_continue(:ok, state) do
{:noreply, state}
end
@impl true
def handle_info(:consolidate, state) do
{:noreply, state}
end
def fetch(address_hash, token_balances) do
if cache_expired?(address_hash) do
Task.start_link(fn ->
update_cache(address_hash, token_balances)
end)
end
address_hash_string = get_address_hash_string(address_hash)
fetch_from_cache("hash_#{address_hash_string}")
end
def cache_name, do: @cache_name
defp cache_expired?(address_hash) do
address_hash_string = get_address_hash_string(address_hash)
updated_at = fetch_from_cache("hash_#{address_hash_string}_#{@last_update_key}")
cond do
is_nil(updated_at) -> true
current_time() - updated_at > @cache_period -> true
true -> false
end
end
defp update_cache(address_hash, token_balances) do
address_hash_string = get_address_hash_string(address_hash)
put_into_cache("hash_#{address_hash_string}_#{@last_update_key}", current_time())
new_data = Chain.address_tokens_usd_sum(token_balances)
put_into_cache("hash_#{address_hash_string}", new_data)
end
defp fetch_from_cache(key) do
case :ets.lookup(@cache_name, key) do
[{_, value}] ->
value
[] ->
0
end
end
defp put_into_cache(key, value) do
:ets.insert(@cache_name, {key, value})
end
defp get_address_hash_string(address_hash) do
Base.encode16(address_hash.bytes, case: :lower)
end
defp current_time do
utc_now = DateTime.utc_now()
DateTime.to_unix(utc_now, :millisecond)
end
def create_cache_table do
if :ets.whereis(@cache_name) == :undefined do
:ets.new(@cache_name, @ets_opts)
end
end
def enable_consolidation?, do: @enable_consolidation
end

@ -1,15 +1,15 @@
defmodule Explorer.Counters.Bridge do
@moduledoc """
Caches the prices of bridged tokens.
Caches the total supply of TokenBridge and OmniBridge.
It loads the count asynchronously and in a time interval of 30 minutes.
"""
use GenServer
alias Explorer.Chain.Cache.TokenExchangeRate
alias Explorer.Chain.Supply.TokenBridge
@prices_table :omni_bridge_bridged_tokens_prices
@bridges_table :bridges_market_cap
@current_total_supply_from_token_bridge_cache_key "current_total_supply_from_token_bridge"
@ -22,10 +22,6 @@ defmodule Explorer.Counters.Bridge do
read_concurrency: true
]
def price_cache_key(symbol) do
"token_symbol_price_#{symbol}"
end
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
@ -51,16 +47,6 @@ defmodule Explorer.Counters.Bridge do
{:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}}
end
def prices_table_exists? do
:ets.whereis(@prices_table) !== :undefined
end
def create_prices_table do
unless prices_table_exists?() do
:ets.new(@prices_table, @ets_opts)
end
end
def bridges_table_exists? do
:ets.whereis(@bridges_table) !== :undefined
end
@ -72,7 +58,7 @@ defmodule Explorer.Counters.Bridge do
end
def create_tables do
create_prices_table()
TokenExchangeRate.create_cache_table()
create_bridges_table()
end
@ -84,8 +70,8 @@ defmodule Explorer.Counters.Bridge do
Inserts new bridged token price into the `:ets` table.
"""
def insert_price({key, info}) do
if prices_table_exists?() do
:ets.insert(@prices_table, {key, info})
if TokenExchangeRate.cache_table_exists?() do
TokenExchangeRate.put_into_cache(key, info)
end
end
@ -110,20 +96,6 @@ defmodule Explorer.Counters.Bridge do
{:noreply, state}
end
@doc """
Fetches the info for a specific item from the `:ets` table.
"""
def fetch_token_price(symbol) do
if prices_table_exists?() do
do_fetch_token_price(:ets.lookup(@prices_table, price_cache_key(symbol)))
else
0
end
end
defp do_fetch_token_price([{_, result}]), do: result
defp do_fetch_token_price([]), do: 0
def fetch_token_bridge_total_supply do
if bridges_table_exists?() do
do_fetch_token_bridge_total_supply(:ets.lookup(@bridges_table, @current_total_supply_from_token_bridge_cache_key))
@ -191,7 +163,8 @@ defmodule Explorer.Counters.Bridge do
bridged_mainnet_tokens_list
|> Enum.each(fn {_bridged_token_hash, bridged_token_symbol} ->
bridged_token_price = TokenBridge.get_current_price_for_bridged_token(bridged_token_symbol)
insert_price({price_cache_key(bridged_token_symbol), bridged_token_price})
cache_key = TokenExchangeRate.cache_key(bridged_token_symbol)
TokenExchangeRate.put_into_cache(cache_key, bridged_token_price)
end)
update_total_supply_from_token_bridge_cache()

Loading…
Cancel
Save