@ -0,0 +1,6 @@ |
||||
// These styles extend the default Bootstrap styles |
||||
|
||||
.dropdown-menu { |
||||
width: 100%; |
||||
box-shadow: $box-shadow; |
||||
} |
@ -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 |
@ -1,14 +1,23 @@ |
||||
defmodule BlockScoutWeb.AddressController do |
||||
use BlockScoutWeb, :controller |
||||
|
||||
alias Explorer.Chain |
||||
alias Explorer.{Chain, Market} |
||||
alias Explorer.Chain.Address |
||||
alias Explorer.ExchangeRates.Token |
||||
|
||||
def index(conn, _params) do |
||||
render(conn, "index.html", |
||||
addresses: Chain.list_top_addresses(), |
||||
address_estimated_count: Chain.address_estimated_count(), |
||||
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() |
||||
) |
||||
end |
||||
|
||||
def show(conn, %{"id" => id}) do |
||||
redirect(conn, to: address_transaction_path(conn, :index, id)) |
||||
end |
||||
|
||||
def transaction_count(%Address{} = address) do |
||||
Chain.address_to_transaction_count(address) |
||||
Chain.address_to_transactions_estimated_count(address) |
||||
end |
||||
end |
||||
|
@ -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,56 @@ |
||||
<section class="container"> |
||||
<div class="card"> |
||||
<div class="card-body"> |
||||
<h1><%= gettext "Addresses" %></h1> |
||||
<p> |
||||
<%= gettext "Showing 250 addresses of" %> |
||||
<%= Cldr.Number.to_string!(@address_estimated_count, format: "#,###") %> |
||||
<%= gettext "total addresses with a balance" %> |
||||
</p> |
||||
|
||||
<span data-selector="top-addresses-list"> |
||||
<%= for {address, index} <- Enum.with_index(@addresses, 1) do %> |
||||
<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><%= 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"> |
||||
<!-- address coin balance --> |
||||
<span class="tile-title" data-test="address_balance"><%= balance(address) %></span> |
||||
<div class="d-flex flex-column flex-md-row justify-content-md-end"> |
||||
<!-- 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> |
||||
<!-- percentage of coins from total supply --> |
||||
<span class="ml-0 ml-md-2">(<%= balance_percentage(address) %>)</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
<% end %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
|
||||
|
||||
</section> |
@ -1,33 +0,0 @@ |
||||
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-id="<%= @internal_transaction.id %>"> |
||||
<div class="row"> |
||||
<div class="col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center tile-label mb-1 mb-md-0 pl-md-4"> |
||||
<%= gettext("Internal Transaction") %> |
||||
</div> |
||||
<div class="col-md-8 col-lg-8 d-flex flex-column text-nowrap pr-2 pr-sm-2 pr-md-0"> |
||||
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %> |
||||
<span class="text-nowrap"> |
||||
<%= if @address.hash == @internal_transaction.from_address_hash do %> |
||||
<%= render BlockScoutWeb.AddressView, "_responsive_hash.html", address: @internal_transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.from_address) %> |
||||
<% else %> |
||||
<%= render BlockScoutWeb.AddressView, "_link.html", address: @internal_transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.from_address) %> |
||||
<% end %> |
||||
→ |
||||
<%= if @address.hash == BlockScoutWeb.InternalTransactionView.to_address_hash(@internal_transaction) do %> |
||||
<%= render BlockScoutWeb.AddressView, "_responsive_hash.html", address: BlockScoutWeb.InternalTransactionView.to_address(@internal_transaction), contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.to_address) %> |
||||
<% else %> |
||||
<%= render BlockScoutWeb.AddressView, "_link.html", address: BlockScoutWeb.InternalTransactionView.to_address(@internal_transaction), contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.to_address) %> |
||||
<% end %> |
||||
</span> |
||||
<span class="tile-title text-truncate mt-3 mt-md-0"> |
||||
<%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %> <%= gettext "Ether" %> |
||||
</span> |
||||
</div> |
||||
<div class="col-md-2 d-flex flex-row flex-md-column justify-content-start align-items-end mt-3 mt-md-0"> |
||||
<%= if @address.hash == @internal_transaction.from_address_hash do %> |
||||
<span class="badge badge-danger tile-badge"><%= gettext "OUT" %></span> |
||||
<% else %> |
||||
<span class="badge badge-success tile-badge"><%= gettext "IN" %></span> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -1,10 +1,19 @@ |
||||
<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 %> |
||||
<div class="row justify-content align-items-center"> |
||||
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0"> |
||||
<%= 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> |
||||
</div> |
||||
<div class="col-md-5 d-flex flex-column text-md-right mt-3 mt-md-0"> |
||||
<span class="tile-title-lg text-md-right align-bottom"> |
||||
<%= format_according_to_decimals(@token.balance, @token.decimals) %> <%= @token.symbol %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</div> |
||||
|
@ -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,37 @@ |
||||
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-id="<%= @internal_transaction.id %>"> |
||||
<div class="row"> |
||||
<div class="col-md-2 d-flex flex-row flex-md-column align-items-left justify-content-start justify-content-lg-center mb-1 mb-md-0 pl-md-4"> |
||||
<%= gettext("Internal Transaction") %> |
||||
</div> |
||||
<div class="col-md-7 col-lg-8 d-flex flex-column text-nowrap pr-2 pr-sm-2 pr-md-0"> |
||||
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %> |
||||
<span class="text-nowrap"> |
||||
<%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %> |
||||
→ |
||||
<%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %> |
||||
</span> |
||||
<span class="tile-title text-truncate mt-3 mt-md-0"> |
||||
<%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %> |
||||
<%= gettext "Ether" %> |
||||
</span> |
||||
</div> |
||||
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-start text-md-right mt-3 mt-md-0"> |
||||
<span class="mr-2 mr-md-0 order-1"> |
||||
<%= link( |
||||
gettext("Block #%{number}", number: to_string(@internal_transaction.transaction.block_number)), |
||||
to: block_path(BlockScoutWeb.Endpoint, :show, @internal_transaction.transaction.block) |
||||
) %> |
||||
</span> |
||||
<span class="mr-2 mr-md-0 order-2" data-from-now="<%= @internal_transaction.transaction.block.timestamp %>"></span> |
||||
<%= if assigns[:current_address] do %> |
||||
<span class="mr-2 mr-md-0 order-0 order-md-3"> |
||||
<%= if assigns[:current_address].hash == @internal_transaction.from_address_hash do %> |
||||
<span class="badge badge-danger tile-badge"><%= gettext "OUT" %></span> |
||||
<% else %> |
||||
<span class="badge badge-success tile-badge"><%= gettext "IN" %></span> |
||||
<% end %> |
||||
</span> |
||||
<% end %> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -1,20 +0,0 @@ |
||||
<div class="tile tile-type-internal-transaction fade-in" data-test="internal_transaction" data-internal-transaction-id="<%= @internal_transaction.id %>"> |
||||
<div class="row"> |
||||
<div class="col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center tile-label"> |
||||
<%= gettext("Internal Transaction") %> |
||||
</div> |
||||
<div class="col-md-9 col-lg-10 d-flex flex-column text-nowrap"> |
||||
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %> |
||||
<span class="text-nowrap"> |
||||
<%= render BlockScoutWeb.AddressView, "_link.html", address: @internal_transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.from_address) %> |
||||
→ |
||||
<%= render BlockScoutWeb.AddressView, "_link.html", address: BlockScoutWeb.InternalTransactionView.to_address(@internal_transaction), contract: BlockScoutWeb.AddressView.contract?(@internal_transaction.to_address) %> |
||||
</span> |
||||
|
||||
<span class="tile-title text-truncate"> |
||||
<%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %> |
||||
<%= gettext "Ether" %> |
||||
</span> |
||||
</div> |
||||
</div> |
||||
</div> |
@ -0,0 +1,5 @@ |
||||
defmodule BlockScoutWeb.AddressTokenTransferView do |
||||
use BlockScoutWeb, :view |
||||
|
||||
alias BlockScoutWeb.AddressView |
||||
end |
@ -1,17 +1,3 @@ |
||||
defmodule BlockScoutWeb.InternalTransactionView do |
||||
use BlockScoutWeb, :view |
||||
@dialyzer :no_match |
||||
|
||||
alias Explorer.Chain.{Address, InternalTransaction} |
||||
|
||||
def create?(%InternalTransaction{type: :create}), do: true |
||||
def create?(_), do: false |
||||
|
||||
# This is the address to be shown in the to field |
||||
def to_address_hash(%InternalTransaction{to_address_hash: nil, created_contract_address_hash: hash}), do: hash |
||||
|
||||
def to_address_hash(%InternalTransaction{to_address_hash: hash}), do: hash |
||||
|
||||
def to_address(%InternalTransaction{to_address: nil, created_contract_address: %Address{} = address}), do: address |
||||
def to_address(%InternalTransaction{to_address: %Address{} = address}), do: address |
||||
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 |
@ -1,62 +0,0 @@ |
||||
defmodule BlockScoutWeb.InternalTransactionViewTest do |
||||
use BlockScoutWeb.ConnCase, async: true |
||||
|
||||
alias BlockScoutWeb.InternalTransactionView |
||||
|
||||
describe "create?/1" do |
||||
test "with internal transaction of type create returns true" do |
||||
internal_transaction = build(:internal_transaction_create) |
||||
|
||||
assert InternalTransactionView.create?(internal_transaction) |
||||
end |
||||
|
||||
test "with non-create type internal transaction returns false" do |
||||
internal_transaction = build(:internal_transaction) |
||||
|
||||
refute InternalTransactionView.create?(internal_transaction) |
||||
end |
||||
end |
||||
|
||||
describe "to_address_hash/1" do |
||||
setup do |
||||
transaction = insert(:transaction) |
||||
{:ok, transaction: transaction} |
||||
end |
||||
|
||||
test "with a contract address", %{transaction: transaction} do |
||||
internal_transaction = insert(:internal_transaction_create, transaction: transaction, index: 1) |
||||
|
||||
assert InternalTransactionView.to_address_hash(internal_transaction) == |
||||
internal_transaction.created_contract_address_hash |
||||
end |
||||
|
||||
test "without a contract address", %{transaction: transaction} do |
||||
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 1) |
||||
|
||||
assert InternalTransactionView.to_address_hash(internal_transaction) == internal_transaction.to_address_hash |
||||
end |
||||
end |
||||
|
||||
describe "to_address/1" do |
||||
setup do |
||||
transaction = insert(:transaction) |
||||
{:ok, transaction: transaction} |
||||
end |
||||
|
||||
test "with a contract address", %{transaction: transaction} do |
||||
internal_transaction = insert(:internal_transaction_create, transaction: transaction, index: 1) |
||||
preloaded_internal_transaction = Explorer.Repo.preload(internal_transaction, :to_address) |
||||
|
||||
assert InternalTransactionView.to_address(preloaded_internal_transaction) == |
||||
preloaded_internal_transaction.created_contract_address |
||||
end |
||||
|
||||
test "without a contract address", %{transaction: transaction} do |
||||
internal_transaction = insert(:internal_transaction, transaction: transaction, index: 1) |
||||
preloaded_internal_transaction = Explorer.Repo.preload(internal_transaction, :created_contract_address) |
||||
|
||||
assert InternalTransactionView.to_address(preloaded_internal_transaction) == |
||||
preloaded_internal_transaction.to_address |
||||
end |
||||
end |
||||
end |
@ -1,8 +0,0 @@ |
||||
defmodule BlockScoutWeb.FakeAdapter do |
||||
alias Explorer.Chain.Address |
||||
alias Explorer.Repo |
||||
|
||||
def address_estimated_count do |
||||
Repo.aggregate(Address, :count, :hash) |
||||
end |
||||
end |
@ -0,0 +1,106 @@ |
||||
defmodule Explorer.Chain.Address.Token do |
||||
@moduledoc """ |
||||
A projection that represents the relation between a Token and a specific Address. |
||||
|
||||
This representation is expressed by the following attributes: |
||||
|
||||
- contract_address_hash - Address of a Token's contract. |
||||
- name - Token's name. |
||||
- symbol - Token's symbol. |
||||
- type - Token's type. |
||||
- decimals - Token's decimals. |
||||
- balance - how much tokens (TokenBalance) the Address has from the Token. |
||||
- transfer_count - a count of how many TokenTransfers of the Token the Address was involved. |
||||
""" |
||||
@enforce_keys [:contract_address_hash, :inserted_at, :name, :symbol, :balance, :decimals, :type, :transfers_count] |
||||
defstruct @enforce_keys |
||||
|
||||
import Ecto.Query |
||||
alias Explorer.{PagingOptions, Chain} |
||||
|
||||
alias Explorer.Chain.{Hash, Address, Address.TokenBalance} |
||||
|
||||
@default_paging_options %PagingOptions{page_size: 50} |
||||
@typep paging_options :: {:paging_options, PagingOptions.t()} |
||||
|
||||
@doc """ |
||||
It builds a paginated query of Address.Tokens that have a balance higher than zero ordered by type and name. |
||||
""" |
||||
@spec list_address_tokens_with_balance(Hash.t(), [paging_options()]) :: %Ecto.Query{} |
||||
def list_address_tokens_with_balance(address_hash, options \\ []) do |
||||
paging_options = Keyword.get(options, :paging_options, @default_paging_options) |
||||
|
||||
Chain.Token |
||||
|> Chain.Token.join_with_transfers() |
||||
|> join_with_last_balance(address_hash) |
||||
|> order_filter_and_group(address_hash) |
||||
|> page_tokens(paging_options) |
||||
|> limit(^paging_options.page_size) |
||||
end |
||||
|
||||
defp order_filter_and_group(query, address_hash) do |
||||
from( |
||||
[token, transfer, balance] in query, |
||||
order_by: fragment("? DESC, LOWER(?) ASC NULLS LAST", token.type, token.name), |
||||
where: |
||||
(transfer.to_address_hash == ^address_hash or transfer.from_address_hash == ^address_hash) and balance.value > 0, |
||||
group_by: [token.name, token.symbol, balance.value, token.type, token.contract_address_hash], |
||||
select: %Address.Token{ |
||||
contract_address_hash: token.contract_address_hash, |
||||
inserted_at: max(token.inserted_at), |
||||
name: token.name, |
||||
symbol: token.symbol, |
||||
balance: balance.value, |
||||
decimals: max(token.decimals), |
||||
type: token.type, |
||||
transfers_count: count(token.contract_address_hash) |
||||
} |
||||
) |
||||
end |
||||
|
||||
defp join_with_last_balance(queryable, address_hash) do |
||||
last_balance_query = |
||||
from( |
||||
tb in TokenBalance, |
||||
where: tb.address_hash == ^address_hash, |
||||
distinct: :token_contract_address_hash, |
||||
order_by: [desc: :block_number], |
||||
select: %{value: tb.value, token_contract_address_hash: tb.token_contract_address_hash} |
||||
) |
||||
|
||||
from( |
||||
t in queryable, |
||||
join: tb in subquery(last_balance_query), |
||||
on: tb.token_contract_address_hash == t.contract_address_hash |
||||
) |
||||
end |
||||
|
||||
@doc """ |
||||
Builds the pagination according to the given key within `PagingOptions`. |
||||
|
||||
* it just returns the given query when the key is nil. |
||||
* it composes another where clause considering the `type`, `name` and `inserted_at`. |
||||
|
||||
""" |
||||
def page_tokens(query, %PagingOptions{key: nil}), do: query |
||||
|
||||
def page_tokens(query, %PagingOptions{key: {nil, type, inserted_at}}) do |
||||
where( |
||||
query, |
||||
[token], |
||||
token.type < ^type or (token.type == ^type and is_nil(token.name) and token.inserted_at < ^inserted_at) |
||||
) |
||||
end |
||||
|
||||
def page_tokens(query, %PagingOptions{key: {name, type, inserted_at}}) do |
||||
upper_name = String.downcase(name) |
||||
|
||||
where( |
||||
query, |
||||
[token], |
||||
token.type < ^type or |
||||
(token.type == ^type and (fragment("LOWER(?)", token.name) > ^upper_name or is_nil(token.name))) or |
||||
(token.type == ^type and fragment("LOWER(?)", token.name) == ^upper_name and token.inserted_at < ^inserted_at) |
||||
) |
||||
end |
||||
end |