Merge pull request #612 from poanetwork/frg-use-indexed-token-balances-in-dropdown

Use indexed token balances in the address page balances dropdown
pull/630/head
Felipe Renan 6 years ago committed by GitHub
commit 867ed7235e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 8
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_balance_controller.ex
  2. 12
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  3. 10
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  4. 14
      apps/block_scout_web/lib/block_scout_web/views/address_token_balance_view.ex
  5. 63
      apps/block_scout_web/test/block_scout_web/views/address_token_balance_view_test.exs
  6. 7
      apps/explorer/lib/explorer/chain.ex
  7. 22
      apps/explorer/lib/explorer/chain/address/token_balance.ex
  8. 47
      apps/explorer/lib/explorer/token/balance_reader.ex
  9. 2
      apps/explorer/priv/repo/migrations/20180817021704_create_address_token_balances.exs
  10. 68
      apps/explorer/test/explorer/chain_test.exs
  11. 65
      apps/explorer/test/explorer/token/balance_reader_test.exs
  12. 2
      apps/explorer/test/support/factory.ex
  13. 3
      apps/indexer/test/indexer/token_balance_fetcher_test.exs

@ -2,20 +2,16 @@ defmodule BlockScoutWeb.AddressTokenBalanceController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Token.BalanceReader
def index(conn, %{"address_id" => address_hash_string}) do def index(conn, %{"address_id" => address_hash_string}) do
with true <- ajax?(conn), with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do
token_balances = token_balances = Chain.fetch_last_token_balances(address_hash)
address_hash
|> Chain.fetch_tokens_from_address_hash()
|> BalanceReader.fetch_token_balances_without_error(address_hash_string)
conn conn
|> put_status(200) |> put_status(200)
|> put_layout(false) |> put_layout(false)
|> render("_token_balances.html", tokens: token_balances) |> render("_token_balances.html", token_balances: token_balances)
else else
_ -> _ ->
not_found(conn) not_found(conn)

@ -1,4 +1,4 @@
<%= if Enum.any?(@tokens) do %> <%= if Enum.any?(@token_balances) do %>
<a href="#" <a href="#"
class="text-white" class="text-white"
data-dropdown-toggle data-dropdown-toggle
@ -10,26 +10,26 @@
style="text-decoration: none;"> style="text-decoration: none;">
<i class="fas fa-chevron-down mr-2"></i> <i class="fas fa-chevron-down mr-2"></i>
<h5 data-tokens-count class="d-inline"><%= tokens_count_title(@tokens) %></h5> <h5 data-tokens-count class="d-inline"><%= tokens_count_title(@token_balances) %></h5>
</a> </a>
<% end %> <% end %>
<div class="dropdown-menu dropdown-menu-right token-balance-dropdown p-0" aria-labelledby="dropdown-tokens"> <div class="dropdown-menu dropdown-menu-right token-balance-dropdown p-0" aria-labelledby="dropdown-tokens">
<div data-dropdown-items class="dropdown-items"> <div data-dropdown-items class="dropdown-items">
<%= if Enum.any?(@tokens, & &1.type == "ERC-721") do %> <%= if Enum.any?(@token_balances, & &1.token.type == "ERC-721") do %>
<%= render( <%= render(
"_tokens.html", "_tokens.html",
conn: @conn, conn: @conn,
tokens: filter_by_type(@tokens, "ERC-721"), token_balances: filter_by_type(@token_balances, "ERC-721"),
type: "ERC-721" type: "ERC-721"
) %> ) %>
<% end %> <% end %>
<%= if Enum.any?(@tokens, & &1.type == "ERC-20") do %> <%= if Enum.any?(@token_balances, & &1.token.type == "ERC-20") do %>
<%= render( <%= render(
"_tokens.html", "_tokens.html",
conn: @conn, conn: @conn,
tokens: filter_by_type(@tokens, "ERC-20"), token_balances: filter_by_type(@token_balances, "ERC-20"),
type: "ERC-20" type: "ERC-20"
) %> ) %>
<% end %> <% end %>

@ -1,15 +1,15 @@
<h6 class="dropdown-header"> <h6 class="dropdown-header">
<%= @type %> (<%= Enum.count(@tokens)%>) <%= @type %> (<%= Enum.count(@token_balances)%>)
</h6> </h6>
<%= for token <- sort_by_name(@tokens) do %> <%= for token_balance <- sort_by_name(@token_balances) do %>
<div class="border-bottom"> <div class="border-bottom">
<%= link( <%= link(
to: token_path(@conn, :show, :en, token.contract_address_hash), to: token_path(@conn, :show, :en, token_balance.token.contract_address_hash),
class: "dropdown-item" class: "dropdown-item"
) do %> ) do %>
<p class="mb-0"><%= token_name(token) %></p> <p class="mb-0"><%= token_name(token_balance.token) %></p>
<p class="mb-0"> <p class="mb-0">
<%= format_according_to_decimals(token.balance, token.decimals) %> <%= token.symbol %> <%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_balance.token.symbol %>
</p> </p>
<% end %> <% end %>
</div> </div>

@ -1,20 +1,20 @@
defmodule BlockScoutWeb.AddressTokenBalanceView do defmodule BlockScoutWeb.AddressTokenBalanceView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
def tokens_count_title(tokens) do def tokens_count_title(token_balances) do
ngettext("%{count} token", "%{count} tokens", Enum.count(tokens)) ngettext("%{count} token", "%{count} tokens", Enum.count(token_balances))
end end
def filter_by_type(tokens, type) do def filter_by_type(token_balances, type) do
Enum.filter(tokens, &(&1.type == type)) Enum.filter(token_balances, &(&1.token.type == type))
end end
@doc """ @doc """
Sorts the given list of tokens in alphabetically order considering nil values in the bottom of Sorts the given list of tokens in alphabetically order considering nil values in the bottom of
the list. the list.
""" """
def sort_by_name(tokens) do def sort_by_name(token_balances) do
{unnamed, named} = Enum.split_with(tokens, &is_nil(&1.name)) {unnamed, named} = Enum.split_with(token_balances, &is_nil(&1.token.name))
Enum.sort_by(named, &String.downcase(&1.name)) ++ unnamed Enum.sort_by(named, &String.downcase(&1.token.name)) ++ unnamed
end end
end end

@ -3,28 +3,69 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
alias BlockScoutWeb.AddressTokenBalanceView alias BlockScoutWeb.AddressTokenBalanceView
describe "tokens_count_title/1" do
test "returns the title pluralized" do
token_balances = [
build(:token_balance),
build(:token_balance)
]
assert AddressTokenBalanceView.tokens_count_title(token_balances) == "2 tokens"
end
end
describe "filter_by_type/2" do
test "filter tokens by the given type" do
token_balance_a = build(:token_balance, token: build(:token, type: "ERC-20"))
token_balance_b = build(:token_balance, token: build(:token, type: "ERC-721"))
token_balances = [token_balance_a, token_balance_b]
assert AddressTokenBalanceView.filter_by_type(token_balances, "ERC-20") == [token_balance_a]
end
end
describe "sort_by_name/1" do describe "sort_by_name/1" do
test "sorts the given tokens by its name" do test "sorts the given tokens by its name" do
token_a = build(:token, name: "token name") token_balance_a = build(:token_balance, token: build(:token, name: "token name"))
token_b = build(:token, name: "token") token_balance_b = build(:token_balance, token: build(:token, name: "token"))
token_c = build(:token, name: "atoken") token_balance_c = build(:token_balance, token: build(:token, name: "atoken"))
token_balances = [
token_balance_a,
token_balance_b,
token_balance_c
]
expected = [token_balance_c, token_balance_b, token_balance_a]
assert AddressTokenBalanceView.sort_by_name([token_a, token_b, token_c]) == [token_c, token_b, token_a] assert AddressTokenBalanceView.sort_by_name(token_balances) == expected
end end
test "considers nil values in the bottom of the list" do test "considers nil values in the bottom of the list" do
token_a = build(:token, name: nil) token_balance_a = build(:token_balance, token: build(:token, name: nil))
token_b = build(:token, name: "token name") token_balance_b = build(:token_balance, token: build(:token, name: "token name"))
token_c = build(:token, name: "token") token_balance_c = build(:token_balance, token: build(:token, name: "token"))
assert AddressTokenBalanceView.sort_by_name([token_a, token_b, token_c]) == [token_c, token_b, token_a] token_balances = [
token_balance_a,
token_balance_b,
token_balance_c
]
expected = [token_balance_c, token_balance_b, token_balance_a]
assert AddressTokenBalanceView.sort_by_name(token_balances) == expected
end end
test "considers capitalization" do test "considers capitalization" do
token_a = build(:token, name: "Token") token_balance_a = build(:token_balance, token: build(:token, name: "Token"))
token_b = build(:token, name: "atoken") token_balance_b = build(:token_balance, token: build(:token, name: "atoken"))
token_balances = [token_balance_a, token_balance_b]
expected = [token_balance_b, token_balance_a]
assert AddressTokenBalanceView.sort_by_name([token_a, token_b]) == [token_b, token_a] assert AddressTokenBalanceView.sort_by_name(token_balances) == expected
end end
end end
end end

@ -1681,4 +1681,11 @@ defmodule Explorer.Chain do
{:error, changeset} {:error, changeset}
end end
end end
@spec fetch_last_token_balances(Hash.Address.t()) :: []
def fetch_last_token_balances(address_hash) do
address_hash
|> TokenBalance.last_token_balances()
|> Repo.all()
end
end end

@ -5,9 +5,10 @@ defmodule Explorer.Chain.Address.TokenBalance do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query, only: [from: 2]
alias Explorer.Chain.Address.TokenBalance alias Explorer.Chain.Address.TokenBalance
alias Explorer.Chain.{Address, Block, Hash, Token, Wei} alias Explorer.Chain.{Address, Block, Hash, Token}
@typedoc """ @typedoc """
* `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner.
@ -25,11 +26,11 @@ defmodule Explorer.Chain.Address.TokenBalance do
block_number: Block.block_number(), block_number: Block.block_number(),
inserted_at: DateTime.t(), inserted_at: DateTime.t(),
updated_at: DateTime.t(), updated_at: DateTime.t(),
value: Wei.t() | nil value: Decimal.t() | nil
} }
schema "address_token_balances" do schema "address_token_balances" do
field(:value, Wei) field(:value, :decimal)
field(:block_number, :integer) field(:block_number, :integer)
field(:value_fetched_at, :utc_datetime) field(:value_fetched_at, :utc_datetime)
@ -59,4 +60,19 @@ defmodule Explorer.Chain.Address.TokenBalance do
|> foreign_key_constraint(:token_contract_address_hash) |> foreign_key_constraint(:token_contract_address_hash)
|> unique_constraint(:block_number, name: :token_balances_address_hash_block_number_index) |> unique_constraint(:block_number, name: :token_balances_address_hash_block_number_index)
end end
@doc """
Builds an `Ecto.Query` to fetch the last token balances.
The last token balances from an Address is the last block indexed.
"""
def last_token_balances(address_hash) do
from(
tb in TokenBalance,
where: tb.address_hash == ^address_hash and tb.value > 0,
distinct: :token_contract_address_hash,
order_by: [desc: :block_number],
preload: :token
)
end
end end

@ -27,53 +27,6 @@ defmodule Explorer.Token.BalanceReader do
} }
] ]
@doc """
Fetches the token balances that were fetched without error and have balances more than 0.
TODO: Remove this function once AddressTokenBalanceController is fetching balances from database.
"""
def fetch_token_balances_without_error(tokens, address_hash_string) do
tokens
|> fetch_token_balances_from_blockchain(address_hash_string)
|> Stream.filter(&token_without_error?/1)
|> Stream.map(&format_result/1)
|> Enum.filter(&tokens_with_no_zero_balance?/1)
end
defp token_without_error?({:ok, _token}), do: true
defp token_without_error?({:error, _token}), do: false
defp format_result({:ok, token}), do: token
defp tokens_with_no_zero_balance?(%{balance: balance}), do: balance != 0
@doc """
Fetches the token balances given the tokens and the address hash as string.
This function is going to perform one request async for each token inside a list of tokens in
order to fetch the balance.
"""
@spec fetch_token_balances_from_blockchain([], String.t()) :: []
def fetch_token_balances_from_blockchain(tokens, address_hash_string) do
tokens
|> Task.async_stream(&fetch_from_blockchain(&1, address_hash_string))
|> Enum.map(&format_blockchain_result_from_tasks/1)
end
defp fetch_from_blockchain(%{contract_address_hash: address_hash} = token, address_hash_string) do
address_hash
|> Reader.query_unverified_contract(@balance_function_abi, %{"balanceOf" => [address_hash_string]})
|> format_blockchain_result(token)
end
defp format_blockchain_result(%{"balanceOf" => {:ok, [balance]}}, token) do
{:ok, Map.put(token, :balance, balance)}
end
defp format_blockchain_result(%{"balanceOf" => {:error, error}}, token) do
{:error, Map.put(token, :balance, error)}
end
defp format_blockchain_result_from_tasks({:ok, blockchain_result}), do: blockchain_result
@spec get_balance_of(String.t(), String.t(), non_neg_integer()) :: {atom(), non_neg_integer() | String.t()} @spec get_balance_of(String.t(), String.t(), non_neg_integer()) :: {atom(), non_neg_integer() | String.t()}
def get_balance_of(token_contract_address_hash, address_hash, block_number) do def get_balance_of(token_contract_address_hash, address_hash, block_number) do
result = result =

@ -12,7 +12,7 @@ defmodule Explorer.Repo.Migrations.CreateAddressTokenBalances do
null: false null: false
) )
add(:value, :numeric, precision: 100, default: fragment("NULL"), null: true) add(:value, :decimal, null: true)
add(:value_fetched_at, :utc_datetime, default: fragment("NULL"), null: true) add(:value_fetched_at, :utc_datetime, default: fragment("NULL"), null: true)
timestamps(null: false, type: :utc_datetime) timestamps(null: false, type: :utc_datetime)

@ -1804,4 +1804,72 @@ defmodule Explorer.ChainTest do
Chain.update_token(token, update_params) Chain.update_token(token, update_params)
refute Repo.get_by(Address.Name, address_hash: token.contract_address_hash) refute Repo.get_by(Address.Name, address_hash: token.contract_address_hash)
end end
describe "fetch_last_token_balances/1" do
test "returns the token balances given the address hash" do
address = insert(:address)
token_balance = insert(:token_balance, address: address)
insert(:token_balance, address: build(:address))
token_balances =
address.hash
|> Chain.fetch_last_token_balances()
|> Enum.map(& &1.address_hash)
assert token_balances == [token_balance.address_hash]
end
test "returns the value from the last block" do
address = insert(:address)
token_a = insert(:token, contract_address: build(:contract_address))
token_b = insert(:token, contract_address: build(:contract_address))
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: token_a.contract_address_hash,
value: 5000
)
token_balance_a =
insert(
:token_balance,
address: address,
block_number: 1001,
token_contract_address_hash: token_a.contract_address_hash,
value: 4000
)
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: token_b.contract_address_hash,
value: 3000
)
token_balance_b =
insert(
:token_balance,
address: address,
block_number: 1001,
token_contract_address_hash: token_b.contract_address_hash,
value: 2000
)
token_balances = Chain.fetch_last_token_balances(address.hash)
assert Enum.count(token_balances) == 2
assert Enum.map(token_balances, & &1.value) == [token_balance_a.value, token_balance_b.value]
end
test "returns an empty list when there are no token balances" do
address = insert(:address)
insert(:token_balance, address: build(:address))
assert Chain.fetch_last_token_balances(address.hash) == []
end
end
end end

@ -12,71 +12,6 @@ defmodule Explorer.Token.BalanceReaderTest do
setup :verify_on_exit! setup :verify_on_exit!
setup :set_mox_global setup :set_mox_global
describe "fetch_token_balances_from_blockchain/2" do
test "fetches balances of tokens given the address hash" do
address = insert(:address)
token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
get_balance_from_blockchain()
result =
[token]
|> BalanceReader.fetch_token_balances_from_blockchain(address_hash_string)
|> List.first()
assert result == {:ok, Map.put(token, :balance, 1_000_000_000_000_000_000_000_000)}
end
test "does not ignore calls that were returned with error" do
address = insert(:address)
token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
get_balance_from_blockchain_with_error()
result =
[token]
|> BalanceReader.fetch_token_balances_from_blockchain(address_hash_string)
|> List.first()
assert result == {:error, Map.put(token, :balance, "(-32015) VM execution error.")}
end
end
describe "fetch_token_balances_without_error/2" do
test "filters token balances that were fetched without error" do
address = insert(:address)
token_a = insert(:token, contract_address: build(:contract_address))
token_b = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
get_balance_from_blockchain()
get_balance_from_blockchain_with_error()
results =
[token_a, token_b]
|> BalanceReader.fetch_token_balances_without_error(address_hash_string)
assert Enum.count(results) == 1
assert List.first(results) == Map.put(token_a, :balance, 1_000_000_000_000_000_000_000_000)
end
test "does not considers balances equal 0" do
address = insert(:address)
token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
get_balance_from_blockchain_with_balance_zero()
results =
[token]
|> BalanceReader.fetch_token_balances_without_error(address_hash_string)
assert Enum.count(results) == 0
end
end
describe "get_balance_of/3" do describe "get_balance_of/3" do
setup do setup do
address = insert(:address) address = insert(:address)

@ -444,7 +444,7 @@ defmodule Explorer.Factory do
def token_balance_factory() do def token_balance_factory() do
%TokenBalance{ %TokenBalance{
address: build(:address), address: build(:address),
token: build(:token), token_contract_address_hash: insert(:token).contract_address_hash,
block_number: block_number(), block_number: block_number(),
value: Enum.random(1..100_000), value: Enum.random(1..100_000),
value_fetched_at: DateTime.utc_now() value_fetched_at: DateTime.utc_now()

@ -45,8 +45,7 @@ defmodule Indexer.TokenBalanceFetcherTest do
TokenBalance TokenBalance
|> Explorer.Repo.get_by(address_hash: token_balance.address_hash) |> Explorer.Repo.get_by(address_hash: token_balance.address_hash)
{:ok, wei_value} = Explorer.Chain.Wei.cast(1_000_000_000_000_000_000_000_000) assert token_balance_updated.value == Decimal.new(1_000_000_000_000_000_000_000_000)
assert token_balance_updated.value == wei_value
assert token_balance_updated.value_fetched_at != nil assert token_balance_updated.value_fetched_at != nil
end end
end end

Loading…
Cancel
Save