- Add the route and the controller; - Add the overview and the token list html; - Sort tokens by type and then number of transfers; - Support pagination.pull/606/head
parent
46998b5afb
commit
1942248670
@ -0,0 +1,33 @@ |
||||
defmodule BlockScoutWeb.AddressTokenController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Explorer.{Chain, Market} |
||||
alias Explorer.ExchangeRates.Token |
||||
|
||||
import BlockScoutWeb.AddressController, only: [transaction_count: 1] |
||||
import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1] |
||||
|
||||
def index(conn, %{"address_id" => address_hash_string} = params) do |
||||
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), |
||||
{:ok, address} <- Chain.hash_to_address(address_hash) do |
||||
tokens_plus_one = Chain.tokens_with_number_of_transfers_from_address(address_hash, paging_options(params)) |
||||
{tokens, next_page} = split_list_by_page(tokens_plus_one) |
||||
|
||||
render( |
||||
conn, |
||||
"index.html", |
||||
address: address, |
||||
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), |
||||
transaction_count: transaction_count(address), |
||||
next_page_params: next_page_params(next_page, tokens, params), |
||||
tokens: tokens |
||||
) |
||||
else |
||||
:error -> |
||||
unprocessable_entity(conn) |
||||
|
||||
{:error, :not_found} -> |
||||
not_found(conn) |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,10 @@ |
||||
<div class="tile tile-type-token"> |
||||
<div class="row justify-content"> |
||||
<div class="col-md-12 d-flex flex-column tile-label"> |
||||
<%= link(to: token_path(@conn, :show, @token.contract_address_hash), class: "tile-title-lg") do %> |
||||
<%= token_name(@token) %> |
||||
<% end %> |
||||
<span><%= @token.type %> - <%= number_of_transfers(@token) %></span> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,132 @@ |
||||
<section class="container"> |
||||
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %> |
||||
|
||||
<section> |
||||
<div class="card"> |
||||
<div class="card-header"> |
||||
<!-- DESKTOP TAB NAV --> |
||||
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex"> |
||||
<li class="nav-item"> |
||||
<%= link( |
||||
gettext("Transactions"), |
||||
class: "nav-link", |
||||
to: address_transaction_path(@conn, :index, @address.hash) |
||||
) %> |
||||
</li> |
||||
|
||||
<li class="nav-item"> |
||||
<%= link( |
||||
gettext("Tokens"), |
||||
class: "nav-link active", |
||||
to: address_token_path(@conn, :index, @address.hash) |
||||
) %> |
||||
</li> |
||||
|
||||
<li class="nav-item"> <%= link( |
||||
gettext("Internal Transactions"), |
||||
class: "nav-link", |
||||
"data-test": "internal_transactions_tab_link", |
||||
to: address_internal_transaction_path(@conn, :index, @address.hash) |
||||
) %> |
||||
</li> |
||||
|
||||
<%= if AddressView.contract?(@address) do %> |
||||
<li class="nav-item"> |
||||
<%= link( |
||||
to: address_contract_path(@conn, :index, @address.hash), |
||||
class: "nav-link") do %> |
||||
<%= gettext("Code") %> |
||||
|
||||
<%= if AddressView.smart_contract_verified?(@address) do %> |
||||
<i class="far fa-check-circle"></i> |
||||
<% end %> |
||||
<% end %> |
||||
</li> |
||||
<% end %> |
||||
|
||||
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %> |
||||
<li class="nav-item"> |
||||
<%= link( |
||||
gettext("Read Contract"), |
||||
to: address_read_contract_path(@conn, :index, @address.hash), |
||||
class: "nav-link")%> |
||||
</li> |
||||
<% end %> |
||||
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %> |
||||
<li class="nav-item"> |
||||
<%= link( |
||||
gettext("Read Contract"), |
||||
to: address_read_contract_path(@conn, :index, @address.hash), |
||||
class: "nav-link")%> |
||||
</li> |
||||
<% end %> |
||||
</ul> |
||||
|
||||
<!-- MOBILE DROPDOWN NAV --> |
||||
<ul class="nav nav-tabs card-header-tabs d-md-none"> |
||||
<li class="nav-item dropdown flex-fill text-center"> |
||||
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Tokens</a> |
||||
|
||||
<div class="dropdown-menu"> |
||||
<%= link( |
||||
gettext("Transactions"), |
||||
class: "dropdown-item", |
||||
to: address_transaction_path(@conn, :index, @address.hash) |
||||
) %> |
||||
<%= link( |
||||
gettext("Tokens"), |
||||
class: "dropdown-item", |
||||
to: address_token_path(@conn, :index, @address.hash) |
||||
) %> |
||||
<%= link( |
||||
gettext("Internal Transactions"), |
||||
class: "dropdown-item", |
||||
"data-test": "internal_transactions_tab_link", |
||||
to: address_internal_transaction_path(@conn, :index, @address.hash) |
||||
) %> |
||||
<%= if AddressView.contract?(@address) do %> |
||||
<%= link( |
||||
to: address_contract_path(@conn, :index, @address.hash), |
||||
class: "dropdown-item") do %> |
||||
<%= gettext("Code") %> |
||||
|
||||
<%= if AddressView.smart_contract_verified?(@address) do %> |
||||
<i class="far fa-check-circle"></i> |
||||
<% end %> |
||||
<% end %> |
||||
<% end %> |
||||
</div> |
||||
</li> |
||||
</ul> |
||||
</div> |
||||
|
||||
<div class="card-body"> |
||||
<h2 class="card-title"><%= gettext "Tokens" %></h2> |
||||
<%= if Enum.any?(@tokens) do %> |
||||
<%= for token <- @tokens do %> |
||||
<%= render "_tokens.html", conn: @conn, token: token %> |
||||
<% end %> |
||||
<% else %> |
||||
<div class="tile tile-muted text-center"> |
||||
<span><%= gettext "There are no tokens for this address." %></span> |
||||
</div> |
||||
<% end %> |
||||
|
||||
<div> |
||||
<%= if @next_page_params do %> |
||||
<%= link( |
||||
gettext("Next"), |
||||
class: "button button-secondary button-sm float-right", |
||||
to: address_token_path( |
||||
@conn, |
||||
:index, |
||||
@address, |
||||
@next_page_params |
||||
) |
||||
) %> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</section> |
||||
</section> |
@ -0,0 +1,9 @@ |
||||
defmodule BlockScoutWeb.AddressTokenView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias BlockScoutWeb.AddressView |
||||
|
||||
def number_of_transfers(token) do |
||||
ngettext("%{count} transfer", "%{count} transfers", token.number_of_transfers) |
||||
end |
||||
end |
@ -0,0 +1,110 @@ |
||||
defmodule BlockScoutWeb.AddressTokenControllerTest do |
||||
use BlockScoutWeb.ConnCase |
||||
|
||||
import BlockScoutWeb.Router.Helpers, only: [address_token_path: 3] |
||||
|
||||
alias Explorer.Chain.{Token} |
||||
|
||||
describe "GET index/2" do |
||||
test "with invalid address hash", %{conn: conn} do |
||||
conn = get(conn, address_token_path(conn, :index, "invalid_address")) |
||||
|
||||
assert html_response(conn, 422) |
||||
end |
||||
|
||||
test "with valid address hash without address", %{conn: conn} do |
||||
conn = get(conn, address_token_path(conn, :index, "0x8bf38d4764929064f2d4d3a56520a76ab3df415b")) |
||||
|
||||
assert html_response(conn, 404) |
||||
end |
||||
|
||||
test "returns tokens for the address", %{conn: conn} do |
||||
address = insert(:address) |
||||
|
||||
token1 = |
||||
:token |
||||
|> insert(name: "token1") |
||||
|
||||
token2 = |
||||
:token |
||||
|> insert(name: "token2") |
||||
|
||||
insert( |
||||
:token_transfer, |
||||
token_contract_address: token1.contract_address, |
||||
from_address: address, |
||||
to_address: build(:address) |
||||
) |
||||
|
||||
insert( |
||||
:token_transfer, |
||||
token_contract_address: token2.contract_address, |
||||
from_address: build(:address), |
||||
to_address: address |
||||
) |
||||
|
||||
conn = get(conn, address_token_path(conn, :index, address)) |
||||
|
||||
actual_token_hashes = |
||||
conn.assigns.tokens |
||||
|> Enum.map(& &1.contract_address_hash) |
||||
|
||||
assert html_response(conn, 200) |
||||
assert Enum.member?(actual_token_hashes, token1.contract_address_hash) |
||||
assert Enum.member?(actual_token_hashes, token2.contract_address_hash) |
||||
end |
||||
|
||||
test "returns next page of results based on last seen token", %{conn: conn} do |
||||
address = insert(:address) |
||||
|
||||
second_page_tokens = |
||||
1..50 |
||||
|> Enum.reduce([], fn i, acc -> |
||||
token = insert(:token, name: "A Token#{i}", type: "ERC-20") |
||||
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address) |
||||
acc ++ [token.name] |
||||
end) |
||||
|> Enum.sort() |
||||
|
||||
token = insert(:token, name: "Another Token", type: "ERC-721") |
||||
insert(:token_transfer, token: token, from_address: address) |
||||
%Token{name: name, type: type} = token |
||||
|
||||
conn = |
||||
get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash), %{ |
||||
"name" => name, |
||||
"type" => type |
||||
}) |
||||
|
||||
actual_tokens = |
||||
conn.assigns.tokens |
||||
|> Enum.map(& &1.name) |
||||
|> Enum.sort() |
||||
|
||||
assert second_page_tokens == actual_tokens |
||||
end |
||||
|
||||
test "next_page_params exists if not on last page", %{conn: conn} do |
||||
address = insert(:address) |
||||
|
||||
Enum.each(1..51, fn i -> |
||||
token = insert(:token, name: "A Token#{i}", type: "ERC-20") |
||||
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address) |
||||
end) |
||||
|
||||
conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash)) |
||||
|
||||
assert conn.assigns.next_page_params |
||||
end |
||||
|
||||
test "next_page_params are empty if on last page", %{conn: conn} do |
||||
address = insert(:address) |
||||
token = insert(:token) |
||||
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address) |
||||
|
||||
conn = get(conn, address_token_path(BlockScoutWeb.Endpoint, :index, address.hash)) |
||||
|
||||
refute conn.assigns.next_page_params |
||||
end |
||||
end |
||||
end |
@ -0,0 +1,25 @@ |
||||
defmodule BlockScoutWeb.AddressTokenViewTest do |
||||
use BlockScoutWeb.ConnCase, async: true |
||||
|
||||
alias BlockScoutWeb.AddressTokenView |
||||
|
||||
describe "number_of_transfers/1" do |
||||
test "returns the singular form when there is only one transfer" do |
||||
token = %{number_of_transfers: 1} |
||||
|
||||
assert AddressTokenView.number_of_transfers(token) == "1 transfer" |
||||
end |
||||
|
||||
test "returns the plural form when there is more than one transfer" do |
||||
token = %{number_of_transfers: 2} |
||||
|
||||
assert AddressTokenView.number_of_transfers(token) == "2 transfers" |
||||
end |
||||
|
||||
test "returns the plural form when there are 0 transfers" do |
||||
token = %{number_of_transfers: 0} |
||||
|
||||
assert AddressTokenView.number_of_transfers(token) == "0 transfers" |
||||
end |
||||
end |
||||
end |
Loading…
Reference in new issue