@ -1,3 +1,3 @@ |
||||
$primary: #1b1b3a; |
||||
$secondary: #40ed9e; |
||||
$tertiary: #40ed9e; |
||||
$primary: #1b1b39; |
||||
$secondary: #4beba0; |
||||
$tertiary: #4beba0; |
||||
|
@ -1,3 +1,3 @@ |
||||
$primary: #12455b; |
||||
$secondary: #4786cb; |
||||
$primary: #16465b; |
||||
$secondary: #5ab3ff; |
||||
$tertiary: #77a4c5; |
||||
|
@ -1,3 +1,3 @@ |
||||
$primary: #1BACA4; |
||||
$secondary: #6435c9; |
||||
$tertiary: #6435c9; |
||||
$primary: #28aca4; |
||||
$secondary: #89edda; |
||||
$tertiary: $purple; |
||||
|
@ -1,3 +1,3 @@ |
||||
$primary: $indigo; |
||||
$secondary: $teal; |
||||
$secondary: #7dd79f; |
||||
$tertiary: $purple; |
||||
|
@ -1,3 +1,3 @@ |
||||
$primary: #24a7fb; |
||||
$secondary: #f4c500; |
||||
$primary: #2fa8f8; |
||||
$secondary: #a2daff; |
||||
$tertiary: #006aa7; |
||||
|
@ -1,3 +1,3 @@ |
||||
$primary: #539387; |
||||
$secondary: #77a4c5; |
||||
$primary: #559387; |
||||
$secondary: #add7cf; |
||||
$tertiary: #38533d; |
||||
|
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 593 B |
After Width: | Height: | Size: 945 B |
After Width: | Height: | Size: 643 B |
After Width: | Height: | Size: 354 B |
After Width: | Height: | Size: 269 B |
After Width: | Height: | Size: 368 B |
@ -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> |
After Width: | Height: | Size: 410 B |
After Width: | Height: | Size: 593 B |
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 945 B |
After Width: | Height: | Size: 643 B |
After Width: | Height: | Size: 354 B |
After Width: | Height: | Size: 269 B |
Before Width: | Height: | Size: 494 B After Width: | Height: | Size: 368 B |
@ -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 |