Skip to content

Commit

Permalink
Merge pull request #3470 from poanetwork/vb-address-tokens-sum
Browse files Browse the repository at this point in the history
Display sum of tokens' USD value at tokens holder's address page
  • Loading branch information
vbaranov committed Nov 23, 2020
2 parents 335c424 + ced4ddd commit 52563b5
Show file tree
Hide file tree
Showing 17 changed files with 345 additions and 66 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
_ ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down
Original file line number Diff line number Diff line change
@@ -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))
Expand Down Expand Up @@ -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

Expand All @@ -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
4 changes: 2 additions & 2 deletions apps/block_scout_web/priv/gettext/default.pot
Original file line number Diff line number Diff line change
@@ -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] ""
Expand Down Expand Up @@ -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 ""

Expand Down
4 changes: 2 additions & 2 deletions apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
Original file line number Diff line number Diff line change
@@ -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] ""
Expand Down Expand Up @@ -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 ""

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
22 changes: 22 additions & 0 deletions apps/explorer/config/config.exs
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
2 changes: 2 additions & 0 deletions apps/explorer/lib/explorer/application.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down
25 changes: 25 additions & 0 deletions apps/explorer/lib/explorer/chain.ex
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ defmodule Explorer.Chain do
Address.TokenBalance,
Block,
BridgedToken,
CurrencyHelpers,
Data,
DecompiledSmartContract,
Hash,
Expand Down Expand Up @@ -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
Expand Down
132 changes: 132 additions & 0 deletions apps/explorer/lib/explorer/chain/cache/token_exchange_rate.ex
Original file line number Diff line number Diff line change
@@ -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
12 changes: 12 additions & 0 deletions apps/explorer/lib/explorer/chain/currency_helpers.ex
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit 52563b5

Please sign in to comment.