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
alias Explorer.Chain
alias Explorer.Token.BalanceReader
def index(conn, %{"address_id" => address_hash_string}) do
with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string) do
token_balances =
address_hash
|> Chain.fetch_tokens_from_address_hash()
|> BalanceReader.fetch_token_balances_without_error(address_hash_string)
token_balances = Chain.fetch_last_token_balances(address_hash)
conn
|> put_status(200)
|> put_layout(false)
|> render("_token_balances.html", tokens: token_balances)
|> render("_token_balances.html", token_balances: token_balances)
else
_ ->
not_found(conn)

@ -1,4 +1,4 @@
<%= if Enum.any?(@tokens) do %>
<%= if Enum.any?(@token_balances) do %>
<a href="#"
class="text-white"
data-dropdown-toggle
@ -10,26 +10,26 @@
style="text-decoration: none;">
<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>
<% end %>
<div class="dropdown-menu dropdown-menu-right token-balance-dropdown p-0" aria-labelledby="dropdown-tokens">
<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(
"_tokens.html",
conn: @conn,
tokens: filter_by_type(@tokens, "ERC-721"),
token_balances: filter_by_type(@token_balances, "ERC-721"),
type: "ERC-721"
) %>
<% end %>
<%= if Enum.any?(@tokens, & &1.type == "ERC-20") do %>
<%= if Enum.any?(@token_balances, & &1.token.type == "ERC-20") do %>
<%= render(
"_tokens.html",
conn: @conn,
tokens: filter_by_type(@tokens, "ERC-20"),
token_balances: filter_by_type(@token_balances, "ERC-20"),
type: "ERC-20"
) %>
<% end %>

@ -1,15 +1,15 @@
<h6 class="dropdown-header">
<%= @type %> (<%= Enum.count(@tokens)%>)
<%= @type %> (<%= Enum.count(@token_balances)%>)
</h6>
<%= for token <- sort_by_name(@tokens) do %>
<%= for token_balance <- sort_by_name(@token_balances) do %>
<div class="border-bottom">
<%= 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"
) do %>
<p class="mb-0"><%= token_name(token) %></p>
<p class="mb-0"><%= token_name(token_balance.token) %></p>
<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>
<% end %>
</div>

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

@ -3,28 +3,69 @@ defmodule BlockScoutWeb.AddressTokenBalanceViewTest do
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
test "sorts the given tokens by its name" do
token_a = build(:token, name: "token name")
token_b = build(:token, name: "token")
token_c = build(:token, name: "atoken")
token_balance_a = build(:token_balance, token: build(:token, name: "token name"))
token_balance_b = build(:token_balance, token: build(:token, name: "token"))
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
test "considers nil values in the bottom of the list" do
token_a = build(:token, name: nil)
token_b = build(:token, name: "token name")
token_c = build(:token, name: "token")
token_balance_a = build(:token_balance, token: build(:token, name: nil))
token_balance_b = build(:token_balance, token: build(:token, name: "token name"))
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
test "considers capitalization" do
token_a = build(:token, name: "Token")
token_b = build(:token, name: "atoken")
token_balance_a = build(:token_balance, token: build(:token, name: "Token"))
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

@ -1681,4 +1681,11 @@ defmodule Explorer.Chain do
{:error, changeset}
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

@ -5,9 +5,10 @@ defmodule Explorer.Chain.Address.TokenBalance do
use Ecto.Schema
import Ecto.Changeset
import Ecto.Query, only: [from: 2]
alias Explorer.Chain.Address.TokenBalance
alias Explorer.Chain.{Address, Block, Hash, Token, Wei}
alias Explorer.Chain.{Address, Block, Hash, Token}
@typedoc """
* `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(),
inserted_at: DateTime.t(),
updated_at: DateTime.t(),
value: Wei.t() | nil
value: Decimal.t() | nil
}
schema "address_token_balances" do
field(:value, Wei)
field(:value, :decimal)
field(:block_number, :integer)
field(:value_fetched_at, :utc_datetime)
@ -59,4 +60,19 @@ defmodule Explorer.Chain.Address.TokenBalance do
|> foreign_key_constraint(:token_contract_address_hash)
|> unique_constraint(:block_number, name: :token_balances_address_hash_block_number_index)
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

@ -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()}
def get_balance_of(token_contract_address_hash, address_hash, block_number) do
result =

@ -12,7 +12,7 @@ defmodule Explorer.Repo.Migrations.CreateAddressTokenBalances do
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)
timestamps(null: false, type: :utc_datetime)

@ -1804,4 +1804,72 @@ defmodule Explorer.ChainTest do
Chain.update_token(token, update_params)
refute Repo.get_by(Address.Name, address_hash: token.contract_address_hash)
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

@ -12,71 +12,6 @@ defmodule Explorer.Token.BalanceReaderTest do
setup :verify_on_exit!
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
setup do
address = insert(:address)

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

@ -45,8 +45,7 @@ defmodule Indexer.TokenBalanceFetcherTest do
TokenBalance
|> 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 == wei_value
assert token_balance_updated.value == Decimal.new(1_000_000_000_000_000_000_000_000)
assert token_balance_updated.value_fetched_at != nil
end
end

Loading…
Cancel
Save