parent
4c9a1b5193
commit
a8b185a865
@ -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 |
@ -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> |
@ -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 |
Loading…
Reference in new issue