Add Token Transfers's tab in the Address' page.

pull/764/head
Felipe Renan 6 years ago
parent 4c9a1b5193
commit a8b185a865
  1. 47
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex
  2. 19
      apps/block_scout_web/lib/block_scout_web/router.ex
  3. 6
      apps/block_scout_web/lib/block_scout_web/templates/address_token/_tokens.html.eex
  4. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_token/index.html.eex
  5. 148
      apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex
  6. 3
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  7. 5
      apps/block_scout_web/lib/block_scout_web/views/address_token_transfer_view.ex
  8. 114
      apps/block_scout_web/test/block_scout_web/controllers/address_token_transfer_controller_test.exs
  9. 10
      apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex
  10. 38
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  11. 6
      apps/explorer/lib/explorer/chain/transaction.ex

@ -0,0 +1,47 @@
defmodule BlockScoutWeb.AddressTokenTransferController 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, "address_token_id" => token_hash_string} = params
) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token_hash} <- Chain.string_to_address_hash(token_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash),
{:ok, token} <- Chain.token_from_address_hash(token_hash) do
transactions =
Chain.address_to_transactions_with_token_tranfers(
address_hash,
token_hash,
paging_options(params)
)
{transactions_paginated, next_page} = split_list_by_page(transactions)
render(
conn,
"index.html",
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
next_page_params: next_page_params(next_page, transactions_paginated, params),
token: token,
transaction_count: transaction_count(address),
transactions: transactions_paginated
)
else
:error ->
unprocessable_entity(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -58,7 +58,10 @@ defmodule BlockScoutWeb.Router do
resources("/logs", TransactionLogController, only: [:index], as: :log)
resources("/token_transfers", TransactionTokenTransferController, only: [:index], as: :token_transfer)
resources("/token_transfers", TransactionTokenTransferController,
only: [:index],
as: :token_transfer
)
end
resources("/accounts", AddressController, only: [:index])
@ -94,12 +97,14 @@ defmodule BlockScoutWeb.Router do
as: :read_contract
)
resources(
"/tokens",
AddressTokenController,
only: [:index],
as: :token
)
resources("/tokens", AddressTokenController, only: [:index], as: :token) do
resources(
"/token_transfers",
AddressTokenTransferController,
only: [:index],
as: :transfers
)
end
resources(
"/token_balances",

@ -1,7 +1,11 @@
<div class="tile tile-type-token">
<div class="row justify-content align-items-center">
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0">
<%= link(to: token_path(@conn, :show, @token.contract_address_hash), class: "tile-title-lg") do %>
<%= link(
to: address_token_transfers_path(@conn, :index, @address.hash, @token.contract_address_hash),
class: "tile-title-lg",
"data-test": "token_transfers_#{@token.contract_address_hash}"
) do %>
<%= token_name(@token) %>
<% end %>
<span><%= @token.type %> - <%= number_of_transfers(@token) %></span>

@ -109,7 +109,7 @@
<h2 class="card-title"><%= gettext "Tokens" %></h2>
<%= if Enum.any?(@tokens) do %>
<%= for token <- @tokens do %>
<%= render "_tokens.html", conn: @conn, token: token %>
<%= render "_tokens.html", conn: @conn, token: token, address: @address %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">

@ -0,0 +1,148 @@
<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-lg-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-lg-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">
<%= gettext("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 active",
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)
) %>
<%= 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 %>
<%= if AddressView.smart_contract_with_read_only_functions?(@address) do %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
class: "dropdown-item"
)%>
<% end %>
</div>
</li>
</ul>
</div>
<div class="card-body">
<h2 class="card-title">
<span class="text-muted"><%= gettext "Tokens" %></span> / <%= token_name(@token) %>
</h2>
<%= if Enum.any?(@transactions) do %>
<span data-selector="transactions-list">
<%= for transaction <- @transactions do %>
<%= render(
BlockScoutWeb.TransactionView,
"_tile.html",
transaction: transaction,
current_address: @address
) %>
<% end %>
</span>
<% else %>
<div class="tile tile-muted text-center">
<span><%= gettext "There are no token transfers for this address." %></span>
</div>
<% end %>
<%= if @next_page_params do %>
<%= link(
gettext("Next"),
class: "button button-secondary button-sm float-right mt-3",
to: address_token_transfers_path(
@conn,
:index,
@address.hash,
@token.contract_address_hash,
@next_page_params
)
) %>
<% end %>
</div>
</div>
</section>
</section>

@ -19,7 +19,8 @@
<%= link(
gettext("Tokens"),
class: "nav-link",
to: address_token_path(@conn, :index, @address.hash)
to: address_token_path(@conn, :index, @address.hash),
"data-test": "tokens_tab_link"
) %>
</li>
<li class="nav-item">

@ -0,0 +1,5 @@
defmodule BlockScoutWeb.AddressTokenTransferView do
use BlockScoutWeb, :view
alias BlockScoutWeb.AddressView
end

@ -0,0 +1,114 @@
defmodule BlockScoutWeb.AddressTokenTransferControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.Router.Helpers, only: [address_token_transfers_path: 4]
alias Explorer.Chain.{Address, Token}
describe "GET index/2" do
test "with invalid address hash", %{conn: conn} do
token_hash = "0xc8982771dd50285389c352c175ada74d074427c7"
conn = get(conn, address_token_transfers_path(conn, :index, "invalid_address", token_hash))
assert html_response(conn, 422)
end
test "with invalid token hash", %{conn: conn} do
address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, "invalid_address"))
assert html_response(conn, 422)
end
test "with an address that doesn't exist in our database", %{conn: conn} do
address_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
%Token{contract_address_hash: token_hash} = insert(:token)
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, token_hash))
assert html_response(conn, 404)
end
test "with an token that doesn't exist in our database", %{conn: conn} do
%Address{hash: address_hash} = insert(:address)
token_hash = "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, token_hash))
assert html_response(conn, 404)
end
test "without token transfers for a token", %{conn: conn} do
%Address{hash: address_hash} = insert(:address)
%Token{contract_address_hash: token_hash} = insert(:token)
conn = get(conn, address_token_transfers_path(conn, :index, address_hash, token_hash))
assert html_response(conn, 200)
assert conn.assigns.transactions == []
end
test "returns the transactions that have token transfers for the given address and token", %{conn: conn} do
address = insert(:address)
token = insert(:token)
transaction =
:transaction
|> insert()
|> with_block()
insert(
:token_transfer,
to_address: address,
transaction: transaction,
token_contract_address: token.contract_address
)
conn = get(conn, address_token_transfers_path(conn, :index, address.hash, token.contract_address_hash))
transaction_hashes = Enum.map(conn.assigns.transactions, & &1.hash)
assert html_response(conn, 200)
assert transaction_hashes == [transaction.hash]
end
test "returns next page of results based on last seen transactions", %{conn: conn} do
address = insert(:address)
token = insert(:token)
second_page_transactions =
1..50
|> Enum.map(fn index ->
block = insert(:block, number: 1000 - index)
transaction =
:transaction
|> insert()
|> with_block(block)
insert(
:token_transfer,
to_address: address,
transaction: transaction,
token_contract_address: token.contract_address
)
transaction
end)
|> Enum.map(& &1.hash)
transaction =
:transaction
|> insert()
|> with_block(insert(:block, number: 1002))
conn =
get(conn, address_token_transfers_path(conn, :index, address.hash, token.contract_address_hash), %{
"block_number" => Integer.to_string(transaction.block_number),
"index" => Integer.to_string(transaction.index)
})
actual_transactions = Enum.map(conn.assigns.transactions, & &1.hash)
assert second_page_transactions == actual_transactions
end
end
end

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.AddressPage do
use Wallaby.DSL
import Wallaby.Query, only: [css: 1, css: 2]
alias Explorer.Chain.{Address, InternalTransaction, Hash, Transaction}
alias Explorer.Chain.{Address, InternalTransaction, Hash, Transaction, Token}
def apply_filter(session, direction) do
session
@ -27,6 +27,14 @@ defmodule BlockScoutWeb.AddressPage do
click(session, css("[data-test='internal_transactions_tab_link']"))
end
def click_tokens(session) do
click(session, css("[data-test='tokens_tab_link']"))
end
def click_token_transfers(session, %Token{contract_address_hash: contract_address_hash}) do
click(session, css("[data-test='token_transfers_#{contract_address_hash}']"))
end
def contract_creation(%InternalTransaction{created_contract_address_hash: hash}) do
css("[data-address-hash='#{hash}']", text: to_string(hash))
end

@ -422,4 +422,42 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
|> assert_has(AddressPage.token_transfers(transaction, count: 3))
end
end
describe "viewing token transfers from a specific token" do
test "list token transfers related to the address", %{
addresses: addresses,
block: block,
session: session
} do
lincoln = addresses.lincoln
taft = addresses.taft
contract_address = insert(:contract_address)
token = insert(:token, contract_address: contract_address)
transaction =
:transaction
|> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block)
insert(
:token_transfer,
from_address: lincoln,
to_address: taft,
transaction: transaction,
token_contract_address: contract_address
)
insert(:token_balance, address: lincoln, token_contract_address_hash: contract_address.hash)
session
|> AddressPage.visit_page(lincoln)
|> AddressPage.click_tokens()
|> AddressPage.click_token_transfers(token)
|> assert_has(AddressPage.token_transfers(transaction, count: 1))
|> assert_has(AddressPage.token_transfer(transaction, lincoln, count: 1))
|> assert_has(AddressPage.token_transfer(transaction, taft, count: 1))
|> refute_has(AddressPage.token_transfers_expansion(transaction))
end
end
end

@ -513,7 +513,11 @@ defmodule Explorer.Chain.Transaction do
def transactions_with_token_transfers(address_hash, token_hash) do
query = transactions_with_token_transfers_query(address_hash, token_hash)
from(t in subquery(query), order_by: [desc: t.block_number, desc: t.index])
from(
t in subquery(query),
order_by: [desc: t.block_number, desc: t.index],
preload: [:from_address, :to_address, :created_contract_address, :block]
)
end
defp transactions_with_token_transfers_query(address_hash, token_hash) do

Loading…
Cancel
Save