Wireup top_addresses page

pull/744/head
Stamates 6 years ago
parent b616f5c0aa
commit c060b86527
  1. 11
      apps/block_scout_web/lib/block_scout_web/chain.ex
  2. 23
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  3. 100
      apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex
  4. 5
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  5. 44
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  6. 58
      apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs
  7. 20
      apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex
  8. 24
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  9. 91
      apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
  10. 40
      apps/explorer/lib/explorer/chain.ex
  11. 49
      apps/explorer/test/explorer/chain_test.exs

@ -16,12 +16,12 @@ defmodule BlockScoutWeb.Chain do
Address,
Address.TokenBalance,
Block,
Hash,
InternalTransaction,
Log,
Token,
TokenTransfer,
Transaction
Transaction,
Wei
}
alias Explorer.PagingOptions
@ -155,6 +155,11 @@ defmodule BlockScoutWeb.Chain do
end
end
defp paging_params(%Address{fetched_coin_balance: value, hash: hash}) do
integer_value = value |> Wei.to(:wei) |> Decimal.to_integer()
%{"address_hash" => to_string(hash), "value" => integer_value}
end
defp paging_params(%Block{number: number}) do
%{"block_number" => number}
end
@ -195,7 +200,7 @@ defmodule BlockScoutWeb.Chain do
end
defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do
%{"address_hash" => Hash.to_string(address_hash), "value" => Decimal.to_integer(value)}
%{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)}
end
defp transaction_from_param(param) do

@ -3,27 +3,22 @@ defmodule BlockScoutWeb.AddressController do
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias Explorer.Chain
alias Explorer.{Chain, Market}
alias Explorer.Chain.Address
alias Explorer.ExchangeRates.Token
def index(conn, params) do
full_options =
Keyword.merge(
[
necessity_by_association: %{
transactions: :optional
}
],
paging_options(params)
)
full_options = paging_options(params)
addresses_plus_one = []
# addresses_plus_one = Chain.list_top_addresses(full_options)
addresses_plus_one = Chain.list_top_addresses(full_options)
{addresses, next_page} = split_list_by_page(addresses_plus_one)
render(conn, "index.html", addresses: addresses, next_page_params: next_page_params(next_page, addresses, params))
render(conn, "index.html",
addresses: addresses,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
next_page_params: next_page_params(next_page, addresses, params)
)
end
def show(conn, %{"id" => id}) do

@ -1,63 +1,65 @@
<section class="container">
<div class="card">
<div class="card-body">
<h1><%= gettext "Top Addresses" %></h1>
<span data-selector="top-addresses-list">
<h1><%= gettext "Top Addresses" %></h1>
<div class="tile">
<div class="row">
<!-- rank -->
<div class="col-2 col-md-1 d-flex justify-content-center align-items-center">
<!-- incremented number by order in the list -->
<span> 1 </span>
</div>
<span data-selector="top-addresses-list">
<div class="col-10 col-md-11">
<div class="row">
<!-- address and txn count -->
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0">
<!-- address with link to address page -->
<a href="#" class="tile-title text-truncate">
0x281055afc982d96fab65b3a49cac8b878184cb16
</a>
<!-- number of txns for this address -->
<span> 516 Transactions </span>
</div>
<div class="tile">
<%= for {address, index} <- Enum.with_index(@addresses, 1) do %>
<div class="card">
<div class="card-body">
<div class="row">
<!-- rank -->
<div class="col-2 col-md-1 d-flex justify-content-center align-items-center">
<!-- incremented number by order in the list -->
<!-- <span><%= index %></span> -->
</div>
<div class="col-10 col-md-11">
<div class="row">
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0">
<%= address |> BlockScoutWeb.AddressView.address_partial_selector(nil, nil) |> BlockScoutWeb.AddressView.render_partial() %>
<!-- number of txns for this address -->
<span>
<span data-test="transaction_count"><%= transaction_count(address) %></span>
<%= gettext "Transactions" %>
</span>
</div>
<!-- balance and percentage -->
<div class="col-md-5 d-flex flex-column text-md-right mt-3 mt-md-0">
<!-- percentage of coins from total supply -->
<span class="tile-title"> 1.50954711% </span>
<div class="d-flex flex-column flex-lg-row justify-content-md-end">
<!-- address coin balance -->
<span class="mr-0 mr-lg-2"> 1,538,423.05662936 POA </span>
<!-- USD value of the balance -->
<span> 82,261.53 USD </span>
<!-- balance and percentage -->
<div class="col-md-5 d-flex flex-column text-md-right mt-3 mt-md-0">
<!-- percentage of coins from total supply -->
<span class="tile-title"><%= balance_percentage(address) %></span>
<div class="d-flex flex-column flex-lg-row justify-content-md-end">
<!-- address coin balance -->
<span class="mr-0 mr-lg-2" data-test="address_balance"><%= balance(address) %></span>
<!-- USD value of the balance -->
<span
data-wei-value="<%= if address.fetched_coin_balance, do: address.fetched_coin_balance.value %>"
data-usd-exchange-rate="<%= @exchange_rate.usd_value %>">
</span>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</span>
<!-- paging -->
<!-- <%= if @next_page_params do %>
<%= link(
gettext("Older"),
class: "button button-secondary button-sm float-right mt-3",
to: block_path(
@conn,
:index,
@next_page_params
)
) %>
<% end %> -->
<% end %>
</div>
</div>
</span>
<!-- paging -->
<%= if @next_page_params do %>
<%= link(
gettext("Next"),
class: "button button-secondary button-sm float-right mt-3",
to: address_path(
@conn,
:index,
@next_page_params
)
) %>
<% end %>
</section>

@ -18,6 +18,11 @@
<%= gettext("Transactions") %>
<% end %>
</li>
<li class="nav-item">
<%= link to: address_path(@conn, :index), class: "nav-link topnav-nav-link" do %>
<%= gettext("Top Addresses") %>
<% end %>
</li>
<li class="nav-item">
<%= link to: api_docs_path(@conn, :index), class: "nav-link topnav-nav-link" do %>
<%= gettext("API") %>

@ -1,12 +1,17 @@
defmodule BlockScoutWeb.AddressView do
use BlockScoutWeb, :view
alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction}
alias Explorer.Chain
alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei}
@dialyzer :no_match
def address_partial_selector(struct_to_render_from, direction, current_address, truncate \\ false)
def address_partial_selector(%Address{} = address, _, current_address, truncate) do
matching_address_check(current_address, address.hash, contract?(address), truncate)
end
def address_partial_selector(
%InternalTransaction{to_address_hash: nil, created_contract_address_hash: nil},
:to,
@ -84,6 +89,15 @@ defmodule BlockScoutWeb.AddressView do
format_wei_value(balance, :ether)
end
def balance_percentage(%Address{fetched_coin_balance: balance}) do
balance
|> Wei.to(:ether)
|> Decimal.div(Decimal.new(Chain.total_supply()))
|> Decimal.mult(100)
|> Decimal.to_string()
|> Kernel.<>("%")
end
def balance_block_number(%Address{fetched_coin_balance_block_number: nil}), do: ""
def balance_block_number(%Address{fetched_coin_balance_block_number: fetched_coin_balance_block_number}) do
@ -100,6 +114,18 @@ defmodule BlockScoutWeb.AddressView do
to_string(hash)
end
@doc """
Returns the primary name of an address if available.
"""
def primary_name(%Address{names: [_ | _] = address_names}) do
case Enum.find(address_names, &(&1.primary == true)) do
nil -> nil
%Address.Name{name: name} -> name
end
end
def primary_name(%Address{names: _}), do: nil
def qr_code(%Address{hash: hash}) do
hash
|> to_string()
@ -136,6 +162,10 @@ defmodule BlockScoutWeb.AddressView do
def token_title(%Token{name: name, symbol: symbol}), do: "#{name} (#{symbol})"
def transaction_count(%Address{} = address) do
Chain.address_to_transaction_count(address)
end
def trimmed_hash(%Hash{} = hash) do
string_hash = to_string(hash)
"#{String.slice(string_hash, 0..5)}#{String.slice(string_hash, -6..-1)}"
@ -160,16 +190,4 @@ defmodule BlockScoutWeb.AddressView do
truncate: truncate
}
end
@doc """
Returns the primary name of an address if available.
"""
def primary_name(%Address{names: [_ | _] = address_names}) do
case Enum.find(address_names, &(&1.primary == true)) do
nil -> nil
%Address.Name{name: name} -> name
end
end
def primary_name(%Address{names: _}), do: nil
end

@ -1,6 +1,64 @@
defmodule BlockScoutWeb.AddressControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.{Address, Wei}
describe "GET index/2" do
test "returns top addresses", %{conn: conn} do
address_hashes =
4..1
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash)
conn = get(conn, address_path(conn, :index))
assert conn.assigns.addresses |> Enum.map(& &1.hash) == address_hashes
end
test "returns next page of results based on last seen address", %{conn: conn} do
second_page_address_hashes =
50..1
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash)
%Address{fetched_coin_balance: value, hash: address_hash} = insert(:address, fetched_coin_balance: 51)
conn =
get(conn, address_path(conn, :index), %{
"address_hash" => to_string(address_hash),
"value" => value |> Wei.to(:wei) |> Decimal.to_integer()
})
actual_address_hashes =
conn.assigns.addresses
|> Enum.map(& &1.hash)
assert second_page_address_hashes == actual_address_hashes
end
test "next_page_params exist if not on last page", %{conn: conn} do
%Address{fetched_coin_balance: value, hash: address_hash} =
60..1
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.fetch!(49)
conn = get(conn, address_path(conn, :index))
assert %{
"address_hash" => to_string(address_hash),
"value" => value |> Wei.to(:wei) |> Decimal.to_integer()
} == conn.assigns.next_page_params
end
test "next_page_params are empty if on last page", %{conn: conn} do
insert(:address)
conn = get(conn, address_path(conn, :index))
refute conn.assigns.next_page_params
end
end
describe "GET show/3" do
test "redirects to address/:address_id/transactions", %{conn: conn} do
insert(:address, hash: "0x5aAeb6053F3E94C9b9A09f33669435E7Ef1BeAed")

@ -15,6 +15,10 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-test='address_balance']")
end
def address(%Address{hash: hash}) do
css("[data-address-hash='#{hash}']", text: to_string(hash))
end
def contract_creator do
css("[data-test='address_contract_creator']")
end
@ -75,12 +79,6 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-transaction-hash='#{transaction_hash}'] [data-test='transaction_status']")
end
def visit_page(session, %Address{hash: address_hash}), do: visit_page(session, address_hash)
def visit_page(session, address_hash) do
visit(session, "/address/#{address_hash}")
end
def token_transfer(%Transaction{hash: transaction_hash}, %Address{hash: address_hash}, count: count) do
css(
"[data-transaction-hash='#{transaction_hash}'] [data-test='token_transfer'] [data-address-hash='#{address_hash}']",
@ -95,4 +93,14 @@ defmodule BlockScoutWeb.AddressPage do
def token_transfers_expansion(%Transaction{hash: transaction_hash}) do
css("[data-transaction-hash='#{transaction_hash}'] [data-test='token_transfers_expansion']")
end
def visit_page(session, %Address{hash: address_hash}), do: visit_page(session, address_hash)
def visit_page(session, address_hash) do
visit(session, "/address/#{address_hash}")
end
def visit_page(session) do
visit(session, "/top_addresses")
end
end

@ -1,15 +1,13 @@
defmodule BlockScoutWeb.ViewingAddressesTest do
use BlockScoutWeb.FeatureCase, async: true
alias Explorer.Chain.Wei
alias BlockScoutWeb.{AddressPage, AddressView, Notifier}
setup do
block = insert(:block)
{:ok, balance} = Wei.cast(5)
lincoln = insert(:address, fetched_coin_balance: balance)
taft = insert(:address)
lincoln = insert(:address, fetched_coin_balance: 5)
taft = insert(:address, fetched_coin_balance: 5)
from_taft =
:transaction
@ -29,6 +27,24 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
}}
end
describe "viewing top addresses" do
setup do
addresses = Enum.map(150..101, &insert(:address, fetched_coin_balance: &1))
{:ok, %{addresses: addresses}}
end
test "lists top addresses", %{session: session, addresses: addresses} do
[first_address | _] = addresses
[last_address | _] = Enum.reverse(addresses)
session
|> AddressPage.visit_page()
|> assert_has(AddressPage.address(first_address))
|> assert_has(AddressPage.address(last_address))
end
end
test "viewing address overview information", %{session: session} do
address = insert(:address, fetched_coin_balance: 500)

@ -115,6 +115,21 @@ defmodule BlockScoutWeb.AddressViewTest do
end
end
describe "balance_block_number/1" do
test "gives empty string with no fetched balance block number present" do
assert AddressView.balance_block_number(%Address{}) == ""
end
test "gives block number when fetched balance block number is non-nil" do
assert AddressView.balance_block_number(%Address{fetched_coin_balance_block_number: 1_000_000}) == "1000000"
end
end
test "balance_percentage/1" do
address = insert(:address, fetched_coin_balance: 2_524_608_000_000_000_000_000_000)
assert "1.00%" = AddressView.balance_percentage(address)
end
describe "contract?/1" do
test "with a smart contract" do
{:ok, code} = Data.cast("0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef")
@ -132,6 +147,39 @@ defmodule BlockScoutWeb.AddressViewTest do
end
end
describe "hash/1" do
test "gives a string version of an address's hash" do
address = %Address{
hash: %Hash{
byte_count: 20,
bytes: <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>>
}
}
assert AddressView.hash(address) == "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
end
end
describe "primary_name/1" do
test "gives an address's primary name when present" do
address = insert(:address)
address_name = insert(:address_name, address: address, primary: true, name: "POA Foundation Wallet")
insert(:address_name, address: address, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address, :names)
assert AddressView.primary_name(preloaded_address) == address_name.name
end
test "returns nil when no primary available" do
address_name = insert(:address_name, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address_name.address, :names)
refute AddressView.primary_name(preloaded_address)
end
end
describe "qr_code/1" do
test "it returns an encoded value" do
address = build(:address)
@ -241,47 +289,4 @@ defmodule BlockScoutWeb.AddressViewTest do
assert AddressView.token_title(token) == "super token money (ST$)"
end
end
describe "hash/1" do
test "gives a string version of an address's hash" do
address = %Address{
hash: %Hash{
byte_count: 20,
bytes: <<139, 243, 141, 71, 100, 146, 144, 100, 242, 212, 211, 165, 101, 32, 167, 106, 179, 223, 65, 91>>
}
}
assert AddressView.hash(address) == "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
end
end
describe "balance_block_number/1" do
test "gives empty string with no fetched balance block number present" do
assert AddressView.balance_block_number(%Address{}) == ""
end
test "gives block number when fetched balance block number is non-nil" do
assert AddressView.balance_block_number(%Address{fetched_coin_balance_block_number: 1_000_000}) == "1000000"
end
end
describe "primary_name/1" do
test "gives an address's primary name when present" do
address = insert(:address)
address_name = insert(:address_name, address: address, primary: true, name: "POA Foundation Wallet")
insert(:address_name, address: address, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address, :names)
assert AddressView.primary_name(preloaded_address) == address_name.name
end
test "returns nil when no primary available" do
address_name = insert(:address_name, name: "POA Wallet")
preloaded_address = Explorer.Repo.preload(address_name.address, :names)
refute AddressView.primary_name(preloaded_address)
end
end
end

@ -740,7 +740,7 @@ defmodule Explorer.Chain do
`:required`, and the `t:Explorer.Chain.Block.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Block.t/0` will not be included in the page `entries`.
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
`:key` (a tuple of the lowest/oldest `{block_number}`) and. Results will be the internal
`:key` (a tuple of the lowest/oldest `{block_number}`). Results will be the internal
transactions older than the `block_number` that are passed.
"""
@ -757,6 +757,34 @@ defmodule Explorer.Chain do
|> Repo.all()
end
@doc """
Lists `t:Explorer.Chain.Address.t/0`'s' in descending order based on coin balance.
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Address.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Address.t/0` will not be included in the page `entries`.
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size`
and`:key` (`{value, address_hash}` of the last seen address). Results will be the addresses with lesser
`fetched_coin_balance` (or equal `fetched_coin_balance` and greater alphabetical 'hash') than the last seen address.
"""
@spec list_top_addresses([paging_options | necessity_by_association_option]) :: [Address.t()]
def list_top_addresses(options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
{:ok, zero_wei} = Wei.cast(Decimal.new(0))
Address
|> join_associations(necessity_by_association)
|> page_addresses(paging_options)
|> limit(^paging_options.page_size)
|> order_by(desc: :fetched_coin_balance, asc: :hash)
|> where([address], address.fetched_coin_balance > ^zero_wei)
|> Repo.all()
end
@doc """
Returns a stream of unfetched `t:Explorer.Chain.Address.CoinBalance.t/0`.
@ -1533,6 +1561,16 @@ defmodule Explorer.Chain do
end)
end
defp page_addresses(query, %PagingOptions{key: nil}), do: query
defp page_addresses(query, %PagingOptions{key: {value, hash}}) do
where(
query,
[address],
address.fetched_coin_balance < ^value or (address.fetched_coin_balance == ^value and address.hash > ^hash)
)
end
defp page_blocks(query, %PagingOptions{key: nil}), do: query
defp page_blocks(query, %PagingOptions{key: {block_number}}) do

@ -979,6 +979,55 @@ defmodule Explorer.ChainTest do
end
end
describe "list_top_addresses/2" do
test "without addresses with balance > 0" do
insert(:address, fetched_coin_balance: 0)
assert [] = Chain.list_top_addresses()
end
test "with top addresses in order" do
address_hashes =
4..1
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash)
assert address_hashes == Enum.map(Chain.list_top_addresses(), & &1.hash)
end
test "with top addresses in order with matching value" do
test_hashes =
4..0
|> Enum.map(&Explorer.Chain.Hash.cast(Explorer.Chain.Hash.Address, &1))
|> Enum.map(&elem(&1, 1))
tail =
4..1
|> Enum.map(&insert(:address, fetched_coin_balance: &1, hash: Enum.fetch!(test_hashes, &1 - 1)))
|> Enum.map(& &1.hash)
first_result_hash =
:address
|> insert(fetched_coin_balance: 4, hash: Enum.fetch!(test_hashes, 4))
|> Map.fetch!(:hash)
assert [first_result_hash | tail] == Enum.map(Chain.list_top_addresses(), & &1.hash)
end
test "with addresses can be paginated" do
second_page_address_hashes =
50..1
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash)
%Address{fetched_coin_balance: value, hash: address_hash} = insert(:address, fetched_coin_balance: 51)
assert second_page_address_hashes ==
[paging_options: %PagingOptions{key: {value, address_hash}, page_size: 50}]
|> Chain.list_top_addresses()
|> Enum.map(& &1.hash)
end
end
describe "number_to_block/1" do
test "without block" do
assert {:error, :not_found} = Chain.number_to_block(-1)

Loading…
Cancel
Save