Merge branch 'master' into patch-1

pull/701/head
Jimmy Lauzau 6 years ago committed by GitHub
commit 52af3b38b1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 21
      apps/block_scout_web/lib/block_scout_web/chain.ex
  2. 36
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex
  3. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex
  4. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex
  5. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  6. 6
      apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex
  7. 19
      apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex
  8. 94
      apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex
  9. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex
  10. 16
      apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex
  11. 20
      apps/block_scout_web/lib/block_scout_web/templates/tokens/token/show.html.eex
  12. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
  13. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex
  14. 10
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  15. 94
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  16. 50
      apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex
  17. 93
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  18. 105
      apps/block_scout_web/priv/gettext/default.pot
  19. 105
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  20. 92
      apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs
  21. 2
      apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs
  22. 23
      apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex
  23. 45
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  24. 22
      apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs
  25. 124
      apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
  26. 40
      apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs
  27. 51
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  28. 19
      apps/explorer/lib/explorer/chain.ex
  29. 66
      apps/explorer/lib/explorer/chain/address/token_balance.ex
  30. 27
      apps/explorer/lib/explorer/chain/token_transfer.ex
  31. 49
      apps/explorer/test/explorer/chain/token_transfer_test.exs
  32. 291
      apps/explorer/test/explorer/chain_test.exs

@ -12,7 +12,18 @@ defmodule BlockScoutWeb.Chain do
string_to_transaction_hash: 1 string_to_transaction_hash: 1
] ]
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Token, TokenTransfer, Transaction} alias Explorer.Chain.{
Address,
Address.TokenBalance,
Block,
Hash,
InternalTransaction,
Log,
Token,
TokenTransfer,
Transaction
}
alias Explorer.PagingOptions alias Explorer.PagingOptions
@page_size 50 @page_size 50
@ -121,6 +132,10 @@ defmodule BlockScoutWeb.Chain do
def paging_options(%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at}), def paging_options(%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at}),
do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}] do: [paging_options: %{@default_paging_options | key: {name, type, inserted_at}}]
def paging_options(%{"value" => value, "address_hash" => address_hash}) do
[paging_options: %{@default_paging_options | key: {value, address_hash}}]
end
def paging_options(_params), do: [paging_options: @default_paging_options] def paging_options(_params), do: [paging_options: @default_paging_options]
def param_to_block_number(formatted_number) when is_binary(formatted_number) do def param_to_block_number(formatted_number) when is_binary(formatted_number) do
@ -179,6 +194,10 @@ defmodule BlockScoutWeb.Chain do
%{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime} %{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime}
end end
defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do
%{"address_hash" => Hash.to_string(address_hash), "value" => Decimal.to_integer(value)}
end
defp transaction_from_param(param) do defp transaction_from_param(param) do
with {:ok, hash} <- string_to_transaction_hash(param) do with {:ok, hash} <- string_to_transaction_hash(param) do
hash_to_transaction(hash) hash_to_transaction(hash)

@ -0,0 +1,36 @@
defmodule BlockScoutWeb.Tokens.HolderController do
use BlockScoutWeb, :controller
alias Explorer.Chain
import BlockScoutWeb.Chain,
only: [
split_list_by_page: 1,
paging_options: 1,
next_page_params: 3
]
def index(conn, %{"token_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash),
token_balances <- Chain.fetch_token_holders_from_token_hash(address_hash, paging_options(params)) do
{token_balances_paginated, next_page} = split_list_by_page(token_balances)
render(
conn,
"index.html",
token: token,
token_balances: token_balances_paginated,
total_token_holders: Chain.count_token_holders_from_token_hash(address_hash),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
next_page_params: next_page_params(next_page, token_balances_paginated, params)
)
else
:error ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end
end

@ -11,7 +11,7 @@ defmodule BlockScoutWeb.Tokens.ReadContractController do
"index.html", "index.html",
token: token, token: token,
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash), total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
total_address_in_token_transfers: Chain.count_addresses_in_token_transfers_from_token_hash(address_hash) total_token_holders: Chain.count_token_holders_from_token_hash(address_hash)
) )
else else
:error -> :error ->

@ -17,7 +17,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do
transfers: token_transfers_paginated, transfers: token_transfers_paginated,
token: token, token: token,
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash), total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
total_address_in_token_transfers: Chain.count_addresses_in_token_transfers_from_token_hash(address_hash), total_token_holders: Chain.count_token_holders_from_token_hash(address_hash),
next_page_params: next_page_params(next_page, token_transfers_paginated, params) next_page_params: next_page_params(next_page, token_transfers_paginated, params)
) )
else else

@ -113,6 +113,13 @@ defmodule BlockScoutWeb.Router do
only: [:index], only: [:index],
as: :read_contract as: :read_contract
) )
resources(
"/token_holders",
Tokens.HolderController,
only: [:index],
as: :holder
)
end end
resources( resources(

@ -1,5 +1,3 @@
<%= if @address_hash do %> <%= link to: address_path(BlockScoutWeb.Endpoint, :show, @address_hash), "data-test": "address_hash_link" do %>
<%= link to: address_path(BlockScoutWeb.Endpoint, :show, @address_hash), "data-test": "address_hash_link" do %> <%= render BlockScoutWeb.AddressView, "_responsive_hash.html", address_hash: @address_hash, contract: @contract, truncate: assigns[:truncate] %>
<%= render BlockScoutWeb.AddressView, "_responsive_hash.html", address_hash: @address_hash, contract: @contract, truncate: assigns[:truncate] %>
<% end %>
<% end %> <% end %>

@ -0,0 +1,19 @@
<div class="tile tile-type-token fade-in mb-10" data-test="token_holders">
<div class="row">
<div class="col-md-7 col-lg-8 d-flex flex-column">
<span>
<%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @token_balance.address_hash, contract: BlockScoutWeb.AddressView.contract?(@token_balance.address) %>
</span>
<span>
<span class="text-dark">
<%= format_token_balance_value(@token_balance.value, @token) %> <%= @token.symbol %>
</span>
<%= if @token.total_supply > 0 do %>
(<%= total_supply_percentage(@token_balance.value, @token.total_supply) %>)
<% end %>
</span>
</div>
</div>
</div>

@ -0,0 +1,94 @@
<section class="container">
<%= render(
OverviewView,
"_details.html",
token: @token,
total_token_transfers: @total_token_transfers,
total_token_holders: @total_token_holders
) %>
<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("Token Transfers"),
class: "nav-link",
to: token_path(@conn, :show, @token.contract_address_hash)
) %>
</li>
<%= if TokenView.smart_contract_with_read_only_functions?(@token) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
to: token_read_contract_path(@conn, :index, @conn.params["id"]),
class: "nav-link")%>
</li>
<% end %>
<li class="nav-item">
<%= link(
gettext("Token Holders"),
class: "nav-link active",
"data-test": "token_holders_tab",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</li>
</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"><%= gettext("Token Holders") %></a>
<div class="dropdown-menu">
<%= link(
gettext("Token Transfers"),
class: "dropdown-item",
to: token_path(@conn, :show, @token.contract_address_hash)
) %>
<%= if TokenView.smart_contract_with_read_only_functions?(@token) do %>
<%= link(
gettext("Read Contract"),
to: "#",
class: "dropdown-item")%>
<% end %>
<%= link(
gettext("Token Holders"),
class: "dropdown-item",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</div>
</li>
</ul>
</div>
<!-- Token Holders -->
<div class="card-body">
<h2 class="card-title"><%= gettext "Token Holders" %></h2>
<%= if Enum.any?(@token_balances) do %>
<%= for token_balance <- @token_balances do %>
<%= render "_token_balances.html", token: @token, token_balance: token_balance %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">
<span data-selector="empty-transactions-list">
<%= gettext "There are no holders for this Token." %>
</span>
</div>
<% end %>
<%= if @next_page_params do %>
<%= link(
gettext("Next Page"),
class: "button button-secondary button-small float-right mt-4",
to: token_holder_path(@conn, :index, @token.contract_address_hash, @next_page_params)
) %>
<% end %>
</div>
</div>
</section>
</section>

@ -27,7 +27,7 @@
<div class="d-flex flex-row justify-content-start text-muted"> <div class="d-flex flex-row justify-content-start text-muted">
<span class="mr-4"> <%= @token.type %> </span> <span class="mr-4"> <%= @token.type %> </span>
<span class="mr-4"><%= @total_address_in_token_transfers %> <%= gettext "addresses" %></span> <span class="mr-4"><%= @total_token_holders %> <%= gettext "addresses" %></span>
<span class="mr-4"><%= @total_token_transfers %> <%= gettext "Transfers" %></span> <span class="mr-4"><%= @total_token_transfers %> <%= gettext "Transfers" %></span>
<%= if decimals?(@token) do %> <%= if decimals?(@token) do %>
<span class="mr-4"><%= @token.decimals %> <%= gettext "decimals" %></span> <span class="mr-4"><%= @token.decimals %> <%= gettext "decimals" %></span>

@ -4,7 +4,7 @@
"_details.html", "_details.html",
token: @token, token: @token,
total_token_transfers: @total_token_transfers, total_token_transfers: @total_token_transfers,
total_address_in_token_transfers: @total_address_in_token_transfers total_token_holders: @total_token_holders
) %> ) %>
<section> <section>
@ -26,6 +26,15 @@
to: token_read_contract_path(@conn, :index, @conn.params["token_id"]), to: token_read_contract_path(@conn, :index, @conn.params["token_id"]),
class: "nav-link active")%> class: "nav-link active")%>
</li> </li>
<li class="nav-item">
<%= link(
gettext("Token Holders"),
class: "nav-link active",
"data-test": "token_holders_tab",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</li>
</ul> </ul>
<!-- MOBILE DROPDOWN NAV --> <!-- MOBILE DROPDOWN NAV -->
@ -42,6 +51,11 @@
gettext("Read Contract"), gettext("Read Contract"),
to: "#", to: "#",
class: "nav-link")%> class: "nav-link")%>
<%= link(
gettext("Token Holders"),
class: "dropdown-item",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</div> </div>
</li> </li>
</ul> </ul>

@ -4,7 +4,7 @@
"_details.html", "_details.html",
token: @token, token: @token,
total_token_transfers: @total_token_transfers, total_token_transfers: @total_token_transfers,
total_address_in_token_transfers: @total_address_in_token_transfers total_token_holders: @total_token_holders
) %> ) %>
<section> <section>
@ -28,6 +28,15 @@
class: "nav-link")%> class: "nav-link")%>
</li> </li>
<% end %> <% end %>
<li class="nav-item"i>
<%= link(
gettext("Token Holders"),
class: "nav-link",
"data-test": "token_holders_tab",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</li>
</ul> </ul>
<!-- MOBILE DROPDOWN NAV --> <!-- MOBILE DROPDOWN NAV -->
@ -37,15 +46,20 @@
<div class="dropdown-menu"> <div class="dropdown-menu">
<%= link( <%= link(
gettext("Token Transfers"), gettext("Token Transfers"),
class: "nav-link active", class: "dropdown-item",
to: token_path(@conn, :show, @token.contract_address_hash) to: token_path(@conn, :show, @token.contract_address_hash)
) %> ) %>
<%= if smart_contract_with_read_only_functions?(@token) do %> <%= if smart_contract_with_read_only_functions?(@token) do %>
<%= link( <%= link(
gettext("Read Contract"), gettext("Read Contract"),
to: "#", to: "#",
class: "nav-link")%> class: "dropdown-item")%>
<% end %> <% end %>
<%= link(
gettext("Token Holders"),
class: "dropdown-item",
to: token_holder_path(@conn, :index, @token.contract_address_hash)
) %>
</div> </div>
</li> </li>
</ul> </ul>

@ -11,13 +11,9 @@
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0"> <div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<%= render "_link.html", transaction_hash: @transaction.hash %> <%= render "_link.html", transaction_hash: @transaction.hash %>
<span class="text-nowrap"> <span class="text-nowrap">
<%= BlockScoutWeb.AddressView.display_address_hash(assigns[:current_address], @transaction.from_address) %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %>
&rarr; &rarr;
<%= if assigns[:current_address] && assigns[:current_address].hash == to_address_hash(@transaction) do %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %>
<%= render BlockScoutWeb.AddressView, "_responsive_hash.html", address_hash: to_address_hash(@transaction), contract: BlockScoutWeb.AddressView.contract?(@transaction.to_address) %>
<% else %>
<%= render BlockScoutWeb.AddressView, "_link.html", address_hash: to_address_hash(@transaction), contract: BlockScoutWeb.AddressView.contract?(@transaction.to_address) %>
<% end %>
</span> </span>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0"> <span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="tile-title"> <span class="tile-title">

@ -11,9 +11,9 @@
</span> </span>
<% end %> <% end %>
<% end %> <% end %>
<%= BlockScoutWeb.AddressView.display_address_hash(@address, @token_transfer.from_address, true) %> <%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:from, @address, true) |> BlockScoutWeb.AddressView.render_partial() %>
&rarr; &rarr;
<%= BlockScoutWeb.AddressView.display_address_hash(@address, @token_transfer.to_address, true) %> <%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:to, @address, true) |> BlockScoutWeb.AddressView.render_partial() %>
</span> </span>
<span class="col-12 col-md-7 ml-3 ml-sm-0"> <span class="col-12 col-md-7 ml-3 ml-sm-0">
<%= token_transfer_amount(@token_transfer) %> <%= token_transfer_amount(@token_transfer) %>

@ -14,14 +14,10 @@
</div> </div>
<h1 class="card-title"><%= gettext "Transaction Details" %> </h1> <h1 class="card-title"><%= gettext "Transaction Details" %> </h1>
<h3 data-test="transaction_detail_hash"><%= @transaction %> </h3> <h3 data-test="transaction_detail_hash"><%= @transaction %> </h3>
<span class="d-block mb-2"> <span class="d-block mb-2 text-muted">
<%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @transaction.from_address_hash, contract: BlockScoutWeb.AddressView.contract?(@transaction.from_address) %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, nil) |> BlockScoutWeb.AddressView.render_partial() %>
<span class="text-muted"> &rarr; </span> <span class="text-muted"> &rarr; </span>
<%= if @transaction.to_address_hash do %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, nil) |> BlockScoutWeb.AddressView.render_partial() %>
<%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @transaction.to_address_hash, contract: BlockScoutWeb.AddressView.contract?(@transaction.to_address) %>
<% else %>
<%= gettext("Contract Address Pending") %>
<% end %>
</span> </span>
<div class="d-flex flex-row justify-content-start text-muted"> <div class="d-flex flex-row justify-content-start text-muted">
<span class="mr-4 text-<%= BlockScoutWeb.TransactionView.type_suffix(@transaction) %>"><%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %></span> <span class="mr-4 text-<%= BlockScoutWeb.TransactionView.type_suffix(@transaction) %>"><%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %></span>

@ -1,10 +1,46 @@
defmodule BlockScoutWeb.AddressView do defmodule BlockScoutWeb.AddressView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias Explorer.Chain.{Address, Hash, SmartContract} alias Explorer.Chain.{Address, Hash, SmartContract, TokenTransfer, Transaction}
@dialyzer :no_match @dialyzer :no_match
def address_partial_selector(struct_to_render_from, direction, current_address, truncate \\ false)
def address_partial_selector(%TokenTransfer{to_address: address}, :to, current_address, truncate) do
matching_address_check(current_address, address.hash, contract?(address), truncate)
end
def address_partial_selector(%TokenTransfer{from_address: address}, :from, current_address, truncate) do
matching_address_check(current_address, address.hash, contract?(address), truncate)
end
def address_partial_selector(
%Transaction{to_address_hash: nil, created_contract_address_hash: nil},
:to,
_current_address,
_truncate
) do
gettext("Contract Address Pending")
end
def address_partial_selector(
%Transaction{to_address_hash: nil, created_contract_address_hash: hash},
:to,
current_address,
truncate
) do
matching_address_check(current_address, hash, true, truncate)
end
def address_partial_selector(%Transaction{to_address: address}, :to, current_address, truncate) do
matching_address_check(current_address, address.hash, contract?(address), truncate)
end
def address_partial_selector(%Transaction{from_address: address}, :from, current_address, truncate) do
matching_address_check(current_address, address.hash, contract?(address), truncate)
end
def address_title(%Address{} = address) do def address_title(%Address{} = address) do
if contract?(address) do if contract?(address) do
gettext("Contract Address") gettext("Contract Address")
@ -45,16 +81,20 @@ defmodule BlockScoutWeb.AddressView do
|> Base.encode64() |> Base.encode64()
end end
def smart_contract_verified?(%Address{smart_contract: %SmartContract{}}), do: true def render_partial(%{partial: partial, address_hash: hash, contract: contract?, truncate: truncate}) do
render(
partial,
address_hash: hash,
contract: contract?,
truncate: truncate
)
end
def smart_contract_verified?(%Address{smart_contract: nil}), do: false def render_partial(text), do: text
def trimmed_hash(%Hash{} = hash) do def smart_contract_verified?(%Address{smart_contract: %SmartContract{}}), do: true
string_hash = to_string(hash)
"#{String.slice(string_hash, 0..5)}#{String.slice(string_hash, -6..-1)}"
end
def trimmed_hash(_), do: "" def smart_contract_verified?(%Address{smart_contract: nil}), do: false
def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do
Enum.any?(address.smart_contract.abi, & &1["constant"]) Enum.any?(address.smart_contract.abi, & &1["constant"])
@ -62,32 +102,28 @@ defmodule BlockScoutWeb.AddressView do
def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false
def display_address_hash(current_address, target_address, truncate \\ false) def trimmed_hash(%Hash{} = hash) do
string_hash = to_string(hash)
def display_address_hash(nil, target_address, truncate) do "#{String.slice(string_hash, 0..5)}#{String.slice(string_hash, -6..-1)}"
render(
"_link.html",
address_hash: target_address.hash,
contract: contract?(target_address),
truncate: truncate
)
end end
def display_address_hash(current_address, target_address, truncate) do def trimmed_hash(_), do: ""
if current_address.hash == target_address.hash do
render( defp matching_address_check(current_address, hash, contract?, truncate) do
"_responsive_hash.html", if current_address && current_address.hash == hash do
address_hash: current_address.hash, %{
contract: contract?(current_address), partial: "_responsive_hash.html",
address_hash: hash,
contract: contract?,
truncate: truncate truncate: truncate
) }
else else
render( %{
"_link.html", partial: "_link.html",
address_hash: target_address.hash, address_hash: hash,
contract: contract?(target_address), contract: contract?,
truncate: truncate truncate: truncate
) }
end end
end end
end end

@ -0,0 +1,50 @@
defmodule BlockScoutWeb.Tokens.HolderView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Tokens.{OverviewView, TokenView}
alias Explorer.Chain.{Token}
@doc """
Calculates the percentage of the value from the given total supply.
## Examples
iex> value = Decimal.new(200)
iex> total_supply = Decimal.new(1000)
iex> BlockScoutWeb.Tokens.HolderView.total_supply_percentage(value, total_supply)
"20.0000%"
"""
def total_supply_percentage(value, total_supply) do
result =
value
|> Decimal.div(total_supply)
|> Decimal.mult(100)
|> Decimal.round(4)
|> Decimal.to_string()
result <> "%"
end
@doc """
Formats the token balance value according to the Token's type.
## Examples
iex> token = build(:token, type: "ERC-20", decimals: 2)
iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(100000, token)
"1,000"
iex> token = build(:token, type: "ERC-721")
iex> BlockScoutWeb.Tokens.HolderView.format_token_balance_value(1, token)
1
"""
def format_token_balance_value(value, %Token{type: "ERC-20", decimals: decimals}) do
format_according_to_decimals(value, decimals)
end
def format_token_balance_value(value, _token) do
value
end
end

@ -10,6 +10,8 @@ defmodule BlockScoutWeb.TransactionView do
defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction] defguardp is_transaction_type(mod) when mod in [InternalTransaction, Transaction]
defdelegate formatted_timestamp(block), to: BlockView
def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do
case block do case block do
nil -> 0 nil -> 0
@ -17,22 +19,19 @@ defmodule BlockScoutWeb.TransactionView do
end end
end end
def from_or_to_address?(_token_transfer, nil), do: false def contract_creation?(%Transaction{to_address: nil}), do: true
def from_or_to_address?(%{from_address_hash: from_hash, to_address_hash: to_hash}, %Address{hash: hash}) do
from_hash == hash || to_hash == hash
end
# This is the address to be shown in the to field
def to_address_hash(%Transaction{to_address_hash: nil, created_contract_address_hash: address_hash}), do: address_hash
def to_address_hash(%Transaction{to_address: %Address{hash: address_hash}}), do: address_hash def contract_creation?(_), do: false
def fee(%Transaction{} = transaction) do def fee(%Transaction{} = transaction) do
{_, value} = Chain.fee(transaction, :wei) {_, value} = Chain.fee(transaction, :wei)
value value
end end
def format_gas_limit(gas) do
Number.to_string!(gas)
end
def formatted_fee(%Transaction{} = transaction, opts) do def formatted_fee(%Transaction{} = transaction, opts) do
transaction transaction
|> Chain.fee(:wei) |> Chain.fee(:wei)
@ -43,34 +42,6 @@ defmodule BlockScoutWeb.TransactionView do
end end
end end
def gas_used(%Transaction{gas_used: nil}), do: gettext("Pending")
def gas_used(%Transaction{gas_used: gas_used}) do
Number.to_string!(gas_used)
end
def involves_contract?(%Transaction{from_address: from_address, to_address: to_address}) do
AddressView.contract?(from_address) || AddressView.contract?(to_address)
end
def involves_token_transfers?(%Transaction{token_transfers: []}), do: false
def involves_token_transfers?(%Transaction{token_transfers: transfers}) when is_list(transfers), do: true
def contract_creation?(%Transaction{to_address: nil}), do: true
def contract_creation?(_), do: false
def qr_code(%Transaction{hash: hash}) do
hash
|> to_string()
|> QRCode.to_png()
|> Base.encode64()
end
def format_gas_limit(gas) do
Number.to_string!(gas)
end
def formatted_status(transaction) do def formatted_status(transaction) do
transaction transaction
|> Chain.transaction_to_status() |> Chain.transaction_to_status()
@ -82,7 +53,11 @@ defmodule BlockScoutWeb.TransactionView do
end end
end end
defdelegate formatted_timestamp(block), to: BlockView def from_or_to_address?(_token_transfer, nil), do: false
def from_or_to_address?(%{from_address_hash: from_hash, to_address_hash: to_hash}, %Address{hash: hash}) do
from_hash == hash || to_hash == hash
end
def gas(%type{gas: gas}) when is_transaction_type(type) do def gas(%type{gas: gas}) when is_transaction_type(type) do
Cldr.Number.to_string!(gas) Cldr.Number.to_string!(gas)
@ -95,22 +70,39 @@ defmodule BlockScoutWeb.TransactionView do
format_wei_value(gas_price, unit) format_wei_value(gas_price, unit)
end end
def gas_used(%Transaction{gas_used: nil}), do: gettext("Pending")
def gas_used(%Transaction{gas_used: gas_used}) do
Number.to_string!(gas_used)
end
def hash(%Transaction{hash: hash}) do def hash(%Transaction{hash: hash}) do
to_string(hash) to_string(hash)
end end
def involves_contract?(%Transaction{from_address: from_address, to_address: to_address}) do
AddressView.contract?(from_address) || AddressView.contract?(to_address)
end
def involves_token_transfers?(%Transaction{token_transfers: []}), do: false
def involves_token_transfers?(%Transaction{token_transfers: transfers}) when is_list(transfers), do: true
def qr_code(%Transaction{hash: hash}) do
hash
|> to_string()
|> QRCode.to_png()
|> Base.encode64()
end
def status(transaction) do def status(transaction) do
Chain.transaction_to_status(transaction) Chain.transaction_to_status(transaction)
end end
def type_suffix(%Transaction{} = transaction) do # This is the address to be shown in the to field
cond do def to_address_hash(%Transaction{to_address_hash: nil, created_contract_address_hash: address_hash}),
involves_token_transfers?(transaction) -> "token-transfer" do: address_hash
contract_creation?(transaction) -> "contract-creation"
involves_contract?(transaction) -> "contract-call" def to_address_hash(%Transaction{to_address: %Address{hash: address_hash}}), do: address_hash
true -> "transaction"
end
end
def transaction_display_type(%Transaction{} = transaction) do def transaction_display_type(%Transaction{} = transaction) do
cond do cond do
@ -121,6 +113,15 @@ defmodule BlockScoutWeb.TransactionView do
end end
end end
def type_suffix(%Transaction{} = transaction) do
cond do
involves_token_transfers?(transaction) -> "token-transfer"
contract_creation?(transaction) -> "contract-creation"
involves_contract?(transaction) -> "contract-call"
true -> "transaction"
end
end
@doc """ @doc """
Converts a transaction's Wei value to Ether and returns a formatted display value. Converts a transaction's Wei value to Ether and returns a formatted display value.

@ -52,7 +52,7 @@ msgstr ""
msgid "Transactions" msgid "Transactions"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:91 #: lib/block_scout_web/templates/transaction/overview.html.eex:87
msgid "Value" msgid "Value"
msgstr "" msgstr ""
@ -76,7 +76,7 @@ msgid "Miner"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:59 #: lib/block_scout_web/templates/block/overview.html.eex:59
#: lib/block_scout_web/templates/transaction/overview.html.eex:59 #: lib/block_scout_web/templates/transaction/overview.html.eex:55
msgid "Nonce" msgid "Nonce"
msgstr "" msgstr ""
@ -100,7 +100,7 @@ msgstr ""
msgid "Total Difficulty" msgid "Total Difficulty"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:38 #: lib/block_scout_web/templates/transaction/overview.html.eex:34
msgid "Block Number" msgid "Block Number"
msgstr "" msgstr ""
@ -112,7 +112,7 @@ msgstr ""
msgid "Cumulative Gas Used" msgid "Cumulative Gas Used"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:102 #: lib/block_scout_web/templates/transaction/overview.html.eex:98
msgid "Gas" msgid "Gas"
msgstr "" msgstr ""
@ -120,7 +120,7 @@ msgstr ""
msgid "Gas Price" msgid "Gas Price"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:72 #: lib/block_scout_web/templates/transaction/overview.html.eex:68
msgid "Input" msgid "Input"
msgstr "" msgstr ""
@ -132,7 +132,7 @@ msgstr ""
msgid "%{count} transactions in this block" msgid "%{count} transactions in this block"
msgstr "" msgstr ""
#: lib/block_scout_web/views/address_view.ex:12 #: lib/block_scout_web/views/address_view.ex:48
msgid "Address" msgid "Address"
msgstr "" msgstr ""
@ -148,7 +148,7 @@ msgstr ""
msgid "Overview" msgid "Overview"
msgstr "" msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:81 #: lib/block_scout_web/views/transaction_view.ex:52
msgid "Success" msgid "Success"
msgstr "" msgstr ""
@ -199,9 +199,9 @@ msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:35 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/index.html.eex:16 #: lib/block_scout_web/templates/transaction/index.html.eex:16
#: lib/block_scout_web/templates/transaction/index.html.eex:35 #: lib/block_scout_web/templates/transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/overview.html.eex:47 #: lib/block_scout_web/templates/transaction/overview.html.eex:43
#: lib/block_scout_web/views/transaction_view.ex:46 #: lib/block_scout_web/views/transaction_view.ex:51
#: lib/block_scout_web/views/transaction_view.ex:80 #: lib/block_scout_web/views/transaction_view.ex:73
msgid "Pending" msgid "Pending"
msgstr "" msgstr ""
@ -265,15 +265,15 @@ msgstr ""
msgid "TPM" msgid "TPM"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:86
msgid "Next Page" msgid "Next Page"
msgstr "" msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:78 #: lib/block_scout_web/views/transaction_view.ex:49
msgid "Failed" msgid "Failed"
msgstr "" msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:79 #: lib/block_scout_web/views/transaction_view.ex:50
msgid "Out of Gas" msgid "Out of Gas"
msgstr "" msgstr ""
@ -292,8 +292,8 @@ msgstr ""
#: #:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:22 #: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:22
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:68 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:68
#: lib/block_scout_web/templates/transaction/_tile.html.eex:24 #: lib/block_scout_web/templates/transaction/_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/overview.html.eex:91 #: lib/block_scout_web/templates/transaction/overview.html.eex:87
#: lib/block_scout_web/templates/transaction_internal_transaction/_internal_transaction.html.eex:16 #: lib/block_scout_web/templates/transaction_internal_transaction/_internal_transaction.html.eex:16
#: lib/block_scout_web/views/wei_helpers.ex:72 #: lib/block_scout_web/views/wei_helpers.ex:72
msgid "Ether" msgid "Ether"
@ -453,7 +453,7 @@ msgstr ""
msgid "Total Gas Used" msgid "Total Gas Used"
msgstr "" msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:120 #: lib/block_scout_web/views/transaction_view.ex:112
msgid "Transaction" msgid "Transaction"
msgstr "" msgstr ""
@ -467,8 +467,8 @@ msgid "View All"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
#: lib/block_scout_web/templates/transaction/_tile.html.eex:27 #: lib/block_scout_web/templates/transaction/_tile.html.eex:23
#: lib/block_scout_web/templates/transaction/overview.html.eex:64 #: lib/block_scout_web/templates/transaction/overview.html.eex:60
msgid "TX Fee" msgid "TX Fee"
msgstr "" msgstr ""
@ -476,7 +476,7 @@ msgstr ""
msgid "Contract" msgid "Contract"
msgstr "" msgstr ""
#: lib/block_scout_web/views/address_view.ex:10 #: lib/block_scout_web/views/address_view.ex:46
msgid "Contract Address" msgid "Contract Address"
msgstr "" msgstr ""
@ -493,7 +493,7 @@ msgstr ""
#: lib/block_scout_web/templates/block/index.html.eex:15 #: lib/block_scout_web/templates/block/index.html.eex:15
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:78 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:78
#: lib/block_scout_web/templates/tokens/token/show.html.eex:71 #: lib/block_scout_web/templates/tokens/token/show.html.eex:85
#: lib/block_scout_web/templates/transaction/index.html.eex:66 #: lib/block_scout_web/templates/transaction/index.html.eex:66
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:72 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:72
msgid "Older" msgid "Older"
@ -540,7 +540,7 @@ msgid "Newer"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:118 #: lib/block_scout_web/views/transaction_view.ex:110
msgid "Contract Creation" msgid "Contract Creation"
msgstr "" msgstr ""
@ -634,12 +634,12 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:64 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:64
#: lib/block_scout_web/templates/transaction/overview.html.eex:23 #: lib/block_scout_web/views/address_view.ex:24
msgid "Contract Address Pending" msgid "Contract Address Pending"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:119 #: lib/block_scout_web/views/transaction_view.ex:111
msgid "Contract Call" msgid "Contract Call"
msgstr "" msgstr ""
@ -655,21 +655,21 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:36 #: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:36
#: lib/block_scout_web/templates/transaction/_tile.html.eex:34 #: lib/block_scout_web/templates/transaction/_tile.html.eex:30
msgid "Block #%{number}" msgid "Block #%{number}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:29 #: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:29
#: lib/block_scout_web/templates/transaction/_tile.html.eex:47 #: lib/block_scout_web/templates/transaction/_tile.html.eex:43
msgid "IN" msgid "IN"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:27 #: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:27
#: lib/block_scout_web/templates/transaction/_tile.html.eex:43 #: lib/block_scout_web/templates/transaction/_tile.html.eex:39
msgid "OUT" msgid "OUT"
msgstr "" msgstr ""
@ -685,10 +685,12 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:50 #: lib/block_scout_web/templates/address_token/index.html.eex:50
#: lib/block_scout_web/templates/address_token/index.html.eex:58 #: lib/block_scout_web/templates/address_token/index.html.eex:58
#: lib/block_scout_web/templates/address_transaction/index.html.eex:49 #: lib/block_scout_web/templates/address_transaction/index.html.eex:49
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:26
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:54
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:42 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:51
#: lib/block_scout_web/templates/tokens/token/show.html.eex:26 #: lib/block_scout_web/templates/tokens/token/show.html.eex:26
#: lib/block_scout_web/templates/tokens/token/show.html.eex:45 #: lib/block_scout_web/templates/tokens/token/show.html.eex:54
msgid "Read Contract" msgid "Read Contract"
msgstr "" msgstr ""
@ -714,12 +716,12 @@ msgid "Github"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:52 #: lib/block_scout_web/templates/transaction/overview.html.eex:48
msgid "Block Confirmations" msgid "Block Confirmations"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:114 #: lib/block_scout_web/templates/transaction/overview.html.eex:110
msgid "Limit" msgid "Limit"
msgstr "" msgstr ""
@ -735,14 +737,14 @@ msgid "There are no logs for this transaction."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:107 #: lib/block_scout_web/templates/transaction/overview.html.eex:103
msgid "Used" msgid "Used"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:4 #: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4
#: lib/block_scout_web/views/transaction_view.ex:117 #: lib/block_scout_web/views/transaction_view.ex:109
msgid "Token Transfer" msgid "Token Transfer"
msgstr "" msgstr ""
@ -775,7 +777,7 @@ msgstr ""
msgid "Validated Transactions" msgid "Validated Transactions"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/tokens/token/show.html.eex:64 #: lib/block_scout_web/templates/tokens/token/show.html.eex:78
msgid "There are no transfers for this Token." msgid "There are no transfers for this Token."
msgstr "" msgstr ""
@ -785,13 +787,15 @@ msgid "Token Details"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:48
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:17 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:17
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:34 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:43
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:37 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:46
#: lib/block_scout_web/templates/tokens/token/show.html.eex:17 #: lib/block_scout_web/templates/tokens/token/show.html.eex:17
#: lib/block_scout_web/templates/tokens/token/show.html.eex:36 #: lib/block_scout_web/templates/tokens/token/show.html.eex:45
#: lib/block_scout_web/templates/tokens/token/show.html.eex:39 #: lib/block_scout_web/templates/tokens/token/show.html.eex:48
#: lib/block_scout_web/templates/tokens/token/show.html.eex:55 #: lib/block_scout_web/templates/tokens/token/show.html.eex:69
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:12 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:12
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:42 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:42
#: lib/block_scout_web/templates/transaction_log/index.html.eex:13 #: lib/block_scout_web/templates/transaction_log/index.html.eex:13
@ -830,7 +834,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:52 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:52
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:66
msgid "loading..." msgid "loading..."
msgstr "" msgstr ""
@ -1001,12 +1005,12 @@ msgid "loading....."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/_tile.html.eex:67 #: lib/block_scout_web/templates/transaction/_tile.html.eex:63
msgid "View More Transfers" msgid "View More Transfers"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/_tile.html.eex:68 #: lib/block_scout_web/templates/transaction/_tile.html.eex:64
msgid "View Less Transfers" msgid "View Less Transfers"
msgstr "" msgstr ""
@ -1016,7 +1020,7 @@ msgid "Less than"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:42 #: lib/block_scout_web/views/transaction_view.ex:41
msgid "Max of" msgid "Max of"
msgstr "" msgstr ""
@ -1056,3 +1060,20 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:111 #: lib/block_scout_web/templates/address_token/index.html.eex:111
msgid "There are no tokens for this address." msgid "There are no tokens for this address."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:79
msgid "There are no holders for this Token."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:34
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:45
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:59
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:70
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:32
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:55
#: lib/block_scout_web/templates/tokens/token/show.html.eex:34
#: lib/block_scout_web/templates/tokens/token/show.html.eex:59
msgid "Token Holders"
msgstr ""

@ -64,7 +64,7 @@ msgstr "BlockScout"
msgid "Transactions" msgid "Transactions"
msgstr "Transactions" msgstr "Transactions"
#: lib/block_scout_web/templates/transaction/overview.html.eex:91 #: lib/block_scout_web/templates/transaction/overview.html.eex:87
msgid "Value" msgid "Value"
msgstr "Value" msgstr "Value"
@ -88,7 +88,7 @@ msgid "Miner"
msgstr "Validator" msgstr "Validator"
#: lib/block_scout_web/templates/block/overview.html.eex:59 #: lib/block_scout_web/templates/block/overview.html.eex:59
#: lib/block_scout_web/templates/transaction/overview.html.eex:59 #: lib/block_scout_web/templates/transaction/overview.html.eex:55
msgid "Nonce" msgid "Nonce"
msgstr "Nonce" msgstr "Nonce"
@ -112,7 +112,7 @@ msgstr "Timestamp"
msgid "Total Difficulty" msgid "Total Difficulty"
msgstr "Total Difficulty" msgstr "Total Difficulty"
#: lib/block_scout_web/templates/transaction/overview.html.eex:38 #: lib/block_scout_web/templates/transaction/overview.html.eex:34
msgid "Block Number" msgid "Block Number"
msgstr "Block Height" msgstr "Block Height"
@ -124,7 +124,7 @@ msgstr "Transaction Details"
msgid "Cumulative Gas Used" msgid "Cumulative Gas Used"
msgstr "Cumulative Gas Used" msgstr "Cumulative Gas Used"
#: lib/block_scout_web/templates/transaction/overview.html.eex:102 #: lib/block_scout_web/templates/transaction/overview.html.eex:98
msgid "Gas" msgid "Gas"
msgstr "Gas" msgstr "Gas"
@ -132,7 +132,7 @@ msgstr "Gas"
msgid "Gas Price" msgid "Gas Price"
msgstr "Gas Price" msgstr "Gas Price"
#: lib/block_scout_web/templates/transaction/overview.html.eex:72 #: lib/block_scout_web/templates/transaction/overview.html.eex:68
msgid "Input" msgid "Input"
msgstr "Input" msgstr "Input"
@ -144,7 +144,7 @@ msgstr "%{confirmations} block confirmations"
msgid "%{count} transactions in this block" msgid "%{count} transactions in this block"
msgstr "%{count} transactions in this block" msgstr "%{count} transactions in this block"
#: lib/block_scout_web/views/address_view.ex:12 #: lib/block_scout_web/views/address_view.ex:48
msgid "Address" msgid "Address"
msgstr "Address" msgstr "Address"
@ -160,7 +160,7 @@ msgstr "From"
msgid "Overview" msgid "Overview"
msgstr "Overview" msgstr "Overview"
#: lib/block_scout_web/views/transaction_view.ex:81 #: lib/block_scout_web/views/transaction_view.ex:52
msgid "Success" msgid "Success"
msgstr "Success" msgstr "Success"
@ -211,9 +211,9 @@ msgstr "Showing %{count} Transactions"
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:35 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/index.html.eex:16 #: lib/block_scout_web/templates/transaction/index.html.eex:16
#: lib/block_scout_web/templates/transaction/index.html.eex:35 #: lib/block_scout_web/templates/transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/overview.html.eex:47 #: lib/block_scout_web/templates/transaction/overview.html.eex:43
#: lib/block_scout_web/views/transaction_view.ex:46 #: lib/block_scout_web/views/transaction_view.ex:51
#: lib/block_scout_web/views/transaction_view.ex:80 #: lib/block_scout_web/views/transaction_view.ex:73
msgid "Pending" msgid "Pending"
msgstr "Pending" msgstr "Pending"
@ -277,15 +277,15 @@ msgstr ""
msgid "TPM" msgid "TPM"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69 #: lib/block_scout_web/templates/tokens/holder/index.html.eex:86
msgid "Next Page" msgid "Next Page"
msgstr "" msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:78 #: lib/block_scout_web/views/transaction_view.ex:49
msgid "Failed" msgid "Failed"
msgstr "" msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:79 #: lib/block_scout_web/views/transaction_view.ex:50
msgid "Out of Gas" msgid "Out of Gas"
msgstr "" msgstr ""
@ -304,8 +304,8 @@ msgstr ""
#: #:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:22 #: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:22
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:68 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:68
#: lib/block_scout_web/templates/transaction/_tile.html.eex:24 #: lib/block_scout_web/templates/transaction/_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/overview.html.eex:91 #: lib/block_scout_web/templates/transaction/overview.html.eex:87
#: lib/block_scout_web/templates/transaction_internal_transaction/_internal_transaction.html.eex:16 #: lib/block_scout_web/templates/transaction_internal_transaction/_internal_transaction.html.eex:16
#: lib/block_scout_web/views/wei_helpers.ex:72 #: lib/block_scout_web/views/wei_helpers.ex:72
msgid "Ether" msgid "Ether"
@ -465,7 +465,7 @@ msgstr ""
msgid "Total Gas Used" msgid "Total Gas Used"
msgstr "" msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:120 #: lib/block_scout_web/views/transaction_view.ex:112
msgid "Transaction" msgid "Transaction"
msgstr "" msgstr ""
@ -479,8 +479,8 @@ msgid "View All"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
#: lib/block_scout_web/templates/transaction/_tile.html.eex:27 #: lib/block_scout_web/templates/transaction/_tile.html.eex:23
#: lib/block_scout_web/templates/transaction/overview.html.eex:64 #: lib/block_scout_web/templates/transaction/overview.html.eex:60
msgid "TX Fee" msgid "TX Fee"
msgstr "" msgstr ""
@ -488,7 +488,7 @@ msgstr ""
msgid "Contract" msgid "Contract"
msgstr "" msgstr ""
#: lib/block_scout_web/views/address_view.ex:10 #: lib/block_scout_web/views/address_view.ex:46
msgid "Contract Address" msgid "Contract Address"
msgstr "" msgstr ""
@ -505,7 +505,7 @@ msgstr ""
#: lib/block_scout_web/templates/block/index.html.eex:15 #: lib/block_scout_web/templates/block/index.html.eex:15
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:78 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:78
#: lib/block_scout_web/templates/tokens/token/show.html.eex:71 #: lib/block_scout_web/templates/tokens/token/show.html.eex:85
#: lib/block_scout_web/templates/transaction/index.html.eex:66 #: lib/block_scout_web/templates/transaction/index.html.eex:66
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:72 #: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:72
msgid "Older" msgid "Older"
@ -552,7 +552,7 @@ msgid "Newer"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:118 #: lib/block_scout_web/views/transaction_view.ex:110
msgid "Contract Creation" msgid "Contract Creation"
msgstr "" msgstr ""
@ -646,12 +646,12 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:64 #: lib/block_scout_web/templates/pending_transaction/index.html.eex:64
#: lib/block_scout_web/templates/transaction/overview.html.eex:23 #: lib/block_scout_web/views/address_view.ex:24
msgid "Contract Address Pending" msgid "Contract Address Pending"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:119 #: lib/block_scout_web/views/transaction_view.ex:111
msgid "Contract Call" msgid "Contract Call"
msgstr "" msgstr ""
@ -667,21 +667,21 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:36 #: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:36
#: lib/block_scout_web/templates/transaction/_tile.html.eex:34 #: lib/block_scout_web/templates/transaction/_tile.html.eex:30
msgid "Block #%{number}" msgid "Block #%{number}"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:29 #: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:29
#: lib/block_scout_web/templates/transaction/_tile.html.eex:47 #: lib/block_scout_web/templates/transaction/_tile.html.eex:43
msgid "IN" msgid "IN"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:27 #: lib/block_scout_web/templates/address_internal_transaction/_internal_transaction.html.eex:27
#: lib/block_scout_web/templates/transaction/_tile.html.eex:43 #: lib/block_scout_web/templates/transaction/_tile.html.eex:39
msgid "OUT" msgid "OUT"
msgstr "" msgstr ""
@ -697,10 +697,12 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:50 #: lib/block_scout_web/templates/address_token/index.html.eex:50
#: lib/block_scout_web/templates/address_token/index.html.eex:58 #: lib/block_scout_web/templates/address_token/index.html.eex:58
#: lib/block_scout_web/templates/address_transaction/index.html.eex:49 #: lib/block_scout_web/templates/address_transaction/index.html.eex:49
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:26
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:54
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:25
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:42 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:51
#: lib/block_scout_web/templates/tokens/token/show.html.eex:26 #: lib/block_scout_web/templates/tokens/token/show.html.eex:26
#: lib/block_scout_web/templates/tokens/token/show.html.eex:45 #: lib/block_scout_web/templates/tokens/token/show.html.eex:54
msgid "Read Contract" msgid "Read Contract"
msgstr "" msgstr ""
@ -726,12 +728,12 @@ msgid "Github"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:52 #: lib/block_scout_web/templates/transaction/overview.html.eex:48
msgid "Block Confirmations" msgid "Block Confirmations"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:114 #: lib/block_scout_web/templates/transaction/overview.html.eex:110
msgid "Limit" msgid "Limit"
msgstr "" msgstr ""
@ -747,14 +749,14 @@ msgid "There are no logs for this transaction."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:107 #: lib/block_scout_web/templates/transaction/overview.html.eex:103
msgid "Used" msgid "Used"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:4 #: lib/block_scout_web/templates/tokens/token/_token_transfer.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4 #: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4
#: lib/block_scout_web/views/transaction_view.ex:117 #: lib/block_scout_web/views/transaction_view.ex:109
msgid "Token Transfer" msgid "Token Transfer"
msgstr "" msgstr ""
@ -787,7 +789,7 @@ msgstr ""
msgid "Validated Transactions" msgid "Validated Transactions"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/tokens/token/show.html.eex:64 #: lib/block_scout_web/templates/tokens/token/show.html.eex:78
msgid "There are no transfers for this Token." msgid "There are no transfers for this Token."
msgstr "" msgstr ""
@ -797,13 +799,15 @@ msgid "Token Details"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:48
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:17 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:17
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:34 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:43
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:37 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:46
#: lib/block_scout_web/templates/tokens/token/show.html.eex:17 #: lib/block_scout_web/templates/tokens/token/show.html.eex:17
#: lib/block_scout_web/templates/tokens/token/show.html.eex:36 #: lib/block_scout_web/templates/tokens/token/show.html.eex:45
#: lib/block_scout_web/templates/tokens/token/show.html.eex:39 #: lib/block_scout_web/templates/tokens/token/show.html.eex:48
#: lib/block_scout_web/templates/tokens/token/show.html.eex:55 #: lib/block_scout_web/templates/tokens/token/show.html.eex:69
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:12 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:12
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:42 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:42
#: lib/block_scout_web/templates/transaction_log/index.html.eex:13 #: lib/block_scout_web/templates/transaction_log/index.html.eex:13
@ -842,7 +846,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:52 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:52
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:52 #: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:66
msgid "loading..." msgid "loading..."
msgstr "" msgstr ""
@ -1013,12 +1017,12 @@ msgid "loading....."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/_tile.html.eex:67 #: lib/block_scout_web/templates/transaction/_tile.html.eex:63
msgid "View More Transfers" msgid "View More Transfers"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction/_tile.html.eex:68 #: lib/block_scout_web/templates/transaction/_tile.html.eex:64
msgid "View Less Transfers" msgid "View Less Transfers"
msgstr "" msgstr ""
@ -1028,7 +1032,7 @@ msgid "Less than"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:42 #: lib/block_scout_web/views/transaction_view.ex:41
msgid "Max of" msgid "Max of"
msgstr "" msgstr ""
@ -1068,3 +1072,20 @@ msgstr ""
#: lib/block_scout_web/templates/address_token/index.html.eex:111 #: lib/block_scout_web/templates/address_token/index.html.eex:111
msgid "There are no tokens for this address." msgid "There are no tokens for this address."
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:79
msgid "There are no holders for this Token."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:34
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:45
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:59
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:70
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:32
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:55
#: lib/block_scout_web/templates/tokens/token/show.html.eex:34
#: lib/block_scout_web/templates/tokens/token/show.html.eex:59
msgid "Token Holders"
msgstr ""

@ -0,0 +1,92 @@
defmodule BlockScoutWeb.Tokens.HolderControllerTest do
use BlockScoutWeb.ConnCase
alias Explorer.Chain.Hash
describe "GET index/3" do
test "with invalid address hash", %{conn: conn} do
conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, "invalid_address"))
assert html_response(conn, 404)
end
test "with a token that doesn't exist", %{conn: conn} do
address = build(:address)
conn = get(conn, token_holder_path(BlockScoutWeb.Endpoint, :index, address.hash))
assert html_response(conn, 404)
end
test "successfully renders the page", %{conn: conn} do
token = insert(:token)
insert_list(
2,
:token_balance,
token_contract_address_hash: token.contract_address_hash
)
conn =
get(
conn,
token_holder_path(BlockScoutWeb.Endpoint, :index, token.contract_address_hash)
)
assert html_response(conn, 200)
end
test "returns next page of results based on last seen token balance", %{conn: conn} do
contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493")
token = insert(:token, contract_address: contract_address)
second_page_token_balances =
1..50
|> Enum.map(
&insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
value: &1 + 1000
)
)
|> Enum.map(& &1.value)
token_balance =
insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
value: 50000
)
conn =
get(conn, token_holder_path(conn, :index, token.contract_address_hash), %{
"value" => Decimal.to_integer(token_balance.value),
"address_hash" => Hash.to_string(token_balance.address_hash)
})
actual_token_balances =
conn.assigns.token_balances
|> Enum.map(& &1.value)
|> Enum.reverse()
assert second_page_token_balances == actual_token_balances
end
test "next_page_params exists if not on last page", %{conn: conn} do
contract_address = build(:contract_address, hash: "0x6937cb25eb54bc013b9c13c47ab38eb63edd1493")
token = insert(:token, contract_address: contract_address)
Enum.each(
1..51,
&insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
value: &1 + 1000
)
)
conn = get(conn, token_holder_path(conn, :index, token.contract_address_hash))
assert conn.assigns.next_page_params
end
end
end

@ -31,7 +31,7 @@ defmodule BlockScoutWeb.Tokens.ReadContractControllerTest do
assert html_response(conn, 200) assert html_response(conn, 200)
assert token.contract_address_hash == conn.assigns.token.contract_address_hash assert token.contract_address_hash == conn.assigns.token.contract_address_hash
assert conn.assigns.total_token_transfers assert conn.assigns.total_token_transfers
assert conn.assigns.total_address_in_token_transfers assert conn.assigns.total_token_holders
end end
end end
end end

@ -0,0 +1,23 @@
defmodule BlockScoutWeb.TokenPage do
@moduledoc false
use Wallaby.DSL
import Wallaby.Query, only: [css: 1, css: 2]
alias Explorer.Chain.{Address}
def visit_page(session, %Address{hash: address_hash}) do
visit_page(session, address_hash)
end
def visit_page(session, contract_address_hash) do
visit(session, "tokens/#{contract_address_hash}")
end
def click_tokens_holders(session) do
click(session, css("[data-test='token_holders_tab']"))
end
def token_holders(count: count) do
css("[data-test='token_holders']", count: count)
end
end

@ -2,7 +2,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
use BlockScoutWeb.FeatureCase, async: true use BlockScoutWeb.FeatureCase, async: true
alias Explorer.Chain.Wei alias Explorer.Chain.Wei
alias Explorer.Factory
alias BlockScoutWeb.{AddressPage, AddressView, Notifier} alias BlockScoutWeb.{AddressPage, AddressView, Notifier}
setup do setup do
@ -41,7 +40,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
describe "viewing contract creator" do describe "viewing contract creator" do
test "see the contract creator and transaction links", %{session: session} do test "see the contract creator and transaction links", %{session: session} do
address = insert(:address) address = insert(:address)
contract = insert(:address, contract_code: Factory.data("contract_code")) contract = insert(:contract_address)
transaction = insert(:transaction, from_address: address, created_contract_address: contract) transaction = insert(:transaction, from_address: address, created_contract_address: contract)
internal_transaction = internal_transaction =
@ -63,9 +62,9 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
test "see the contract creator and transaction links even when the creator is another contract", %{session: session} do test "see the contract creator and transaction links even when the creator is another contract", %{session: session} do
lincoln = insert(:address) lincoln = insert(:address)
contract = insert(:address, contract_code: Factory.data("contract_code")) contract = insert(:contract_address)
transaction = insert(:transaction) transaction = insert(:transaction)
another_contract = insert(:address, contract_code: Factory.data("contract_code")) another_contract = insert(:contract_address)
insert( insert(
:internal_transaction, :internal_transaction,
@ -285,12 +284,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
lincoln = addresses.lincoln lincoln = addresses.lincoln
taft = addresses.taft taft = addresses.taft
contract_token_address = insert(:contract_address) contract_address = insert(:contract_address)
insert(:token, contract_address: contract_token_address) insert(:token, contract_address: contract_address)
transaction = transaction =
:transaction :transaction
|> insert(from_address: lincoln, to_address: contract_token_address) |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block) |> with_block(block)
insert( insert(
@ -298,7 +297,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln, from_address: lincoln,
to_address: taft, to_address: taft,
transaction: transaction, transaction: transaction,
token_contract_address: contract_token_address token_contract_address: contract_address
) )
session session
@ -318,12 +317,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
taft = addresses.taft taft = addresses.taft
morty = build(:address) morty = build(:address)
contract_token_address = insert(:contract_address) contract_address = insert(:contract_address)
insert(:token, contract_address: contract_token_address) insert(:token, contract_address: contract_address)
transaction = transaction =
:transaction :transaction
|> insert(from_address: lincoln, to_address: contract_token_address) |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block) |> with_block(block)
insert( insert(
@ -331,7 +330,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln, from_address: lincoln,
to_address: taft, to_address: taft,
transaction: transaction, transaction: transaction,
token_contract_address: contract_token_address token_contract_address: contract_address
) )
insert( insert(
@ -339,7 +338,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln, from_address: lincoln,
to_address: morty, to_address: morty,
transaction: transaction, transaction: transaction,
token_contract_address: contract_token_address token_contract_address: contract_address
) )
session session
@ -358,17 +357,13 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
lincoln = addresses.lincoln lincoln = addresses.lincoln
taft = addresses.taft taft = addresses.taft
contract_token_address = contract_address = insert(:contract_address)
insert(
:address,
contract_code: Factory.data("contract_code")
)
insert(:token, contract_address: contract_token_address) insert(:token, contract_address: contract_address)
transaction = transaction =
:transaction :transaction
|> insert(from_address: lincoln, to_address: contract_token_address) |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block) |> with_block(block)
insert_list( insert_list(
@ -377,7 +372,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln, from_address: lincoln,
to_address: taft, to_address: taft,
transaction: transaction, transaction: transaction,
token_contract_address: contract_token_address token_contract_address: contract_address
) )
session session
@ -393,12 +388,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
lincoln = addresses.lincoln lincoln = addresses.lincoln
taft = addresses.taft taft = addresses.taft
contract_token_address = insert(:contract_address) contract_address = insert(:contract_address)
insert(:token, contract_address: contract_token_address) insert(:token, contract_address: contract_address)
transaction = transaction =
:transaction :transaction
|> insert(from_address: lincoln, to_address: contract_token_address) |> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block) |> with_block(block)
insert_list( insert_list(
@ -407,7 +402,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
from_address: lincoln, from_address: lincoln,
to_address: taft, to_address: taft,
transaction: transaction, transaction: transaction,
token_contract_address: contract_token_address token_contract_address: contract_address
) )
session session

@ -0,0 +1,22 @@
defmodule BlockScoutWeb.ViewingTokensTest do
use BlockScoutWeb.FeatureCase, async: true
alias BlockScoutWeb.TokenPage
describe "viewing token holders" do
test "list the token holders", %{session: session} do
token = insert(:token)
insert_list(
2,
:token_balance,
token_contract_address_hash: token.contract_address_hash
)
session
|> TokenPage.visit_page(token.contract_address)
|> TokenPage.click_tokens_holders()
|> assert_has(TokenPage.token_holders(count: 2))
end
end
end

@ -1,9 +1,106 @@
defmodule BlockScoutWeb.AddressViewTest do defmodule BlockScoutWeb.AddressViewTest do
use BlockScoutWeb.ConnCase, async: true use BlockScoutWeb.ConnCase, async: true
alias Explorer.Chain.Data alias Explorer.Chain.{Address, Data, Transaction}
alias BlockScoutWeb.AddressView alias BlockScoutWeb.AddressView
describe "address_partial_selector/4" do
test "for a pending contract creation to address" do
transaction = insert(:transaction, to_address: nil, created_contract_address_hash: nil)
assert AddressView.address_partial_selector(transaction, :to, nil) == "Contract Address Pending"
end
test "will truncate address" do
transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
assert %{
partial: "_link.html",
address_hash: ^hash,
contract: false,
truncate: true
} = AddressView.address_partial_selector(transaction, :to, nil, true)
end
test "for a non-contract to address not on address page" do
transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
assert %{
partial: "_link.html",
address_hash: ^hash,
contract: false,
truncate: false
} = AddressView.address_partial_selector(transaction, :to, nil)
end
test "for a non-contract to address non matching address page" do
transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
assert %{
partial: "_link.html",
address_hash: ^hash,
contract: false,
truncate: false
} = AddressView.address_partial_selector(transaction, :to, nil)
end
test "for a non-contract to address matching address page" do
transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
assert %{
partial: "_responsive_hash.html",
address_hash: ^hash,
contract: false,
truncate: false
} = AddressView.address_partial_selector(transaction, :to, transaction.to_address)
end
test "for a contract to address non matching address page" do
contract = %Address{hash: hash} = insert(:contract_address)
transaction = insert(:transaction, to_address: nil, created_contract_address: contract)
assert %{
partial: "_link.html",
address_hash: ^hash,
contract: true,
truncate: false
} = AddressView.address_partial_selector(transaction, :to, transaction.to_address)
end
test "for a contract to address matching address page" do
contract = %Address{hash: hash} = insert(:contract_address)
transaction = insert(:transaction, to_address: nil, created_contract_address: contract)
assert %{
partial: "_responsive_hash.html",
address_hash: ^hash,
contract: true,
truncate: false
} = AddressView.address_partial_selector(transaction, :to, contract)
end
test "for a non-contract from address not on address page" do
transaction = %Transaction{to_address_hash: hash} = insert(:transaction)
assert %{
partial: "_link.html",
address_hash: ^hash,
contract: false,
truncate: false
} = AddressView.address_partial_selector(transaction, :to, nil)
end
test "for a non-contract from address matching address page" do
transaction = %Transaction{from_address_hash: hash} = insert(:transaction)
assert %{
partial: "_responsive_hash.html",
address_hash: ^hash,
contract: false,
truncate: false
} = AddressView.address_partial_selector(transaction, :from, transaction.from_address)
end
end
describe "contract?/1" do describe "contract?/1" do
test "with a smart contract" do test "with a smart contract" do
{:ok, code} = Data.cast("0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef") {:ok, code} = Data.cast("0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef")
@ -15,6 +112,10 @@ defmodule BlockScoutWeb.AddressViewTest do
address = insert(:address, contract_code: nil) address = insert(:address, contract_code: nil)
refute AddressView.contract?(address) refute AddressView.contract?(address)
end end
test "with nil address" do
assert AddressView.contract?(nil)
end
end end
describe "qr_code/1" do describe "qr_code/1" do
@ -24,6 +125,27 @@ defmodule BlockScoutWeb.AddressViewTest do
end end
end end
describe "render_partial/1" do
test "renders _link partial" do
%Address{hash: hash} = build(:address)
assert {:safe, _} =
AddressView.render_partial(%{partial: "_link.html", address_hash: hash, contract: false, truncate: false})
end
test "renders _responsive_hash partial" do
%Address{hash: hash} = build(:address)
assert {:safe, _} =
AddressView.render_partial(%{
partial: "_responsive_hash.html",
address_hash: hash,
contract: false,
truncate: false
})
end
end
describe "smart_contract_verified?/1" do describe "smart_contract_verified?/1" do
test "returns true when smart contract is verified" do test "returns true when smart contract is verified" do
smart_contract = insert(:smart_contract) smart_contract = insert(:smart_contract)

@ -0,0 +1,40 @@
defmodule BlockScoutWeb.Tokens.HolderViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.Tokens.HolderView
alias Explorer.Chain.{Address.TokenBalance, Token}
doctest BlockScoutWeb.Tokens.HolderView, import: true
describe "total_supply_percentage/2" do
test "returns the percentage of the Token total supply" do
%Token{total_supply: total_supply} = build(:token, total_supply: 1000)
%TokenBalance{value: value} = build(:token_balance, value: 200)
assert HolderView.total_supply_percentage(value, total_supply) == "20.0000%"
end
test "considers 4 decimals" do
%Token{total_supply: total_supply} = build(:token, total_supply: 100_000_009)
%TokenBalance{value: value} = build(:token_balance, value: 500)
assert HolderView.total_supply_percentage(value, total_supply) == "0.0005%"
end
end
describe "format_token_balance_value/1" do
test "formats according to token decimals when it's a ERC-20" do
token = build(:token, type: "ERC-20", decimals: 2)
token_balance = build(:token_balance, value: 2_000_000)
assert HolderView.format_token_balance_value(token_balance.value, token) == "20,000"
end
test "returns the value when it's ERC-721" do
token = build(:token, type: "ERC-721")
token_balance = build(:token_balance, value: 1)
assert HolderView.format_token_balance_value(token_balance.value, token) == 1
end
end
end

@ -5,6 +5,35 @@ defmodule BlockScoutWeb.TransactionViewTest do
alias Explorer.Repo alias Explorer.Repo
alias BlockScoutWeb.TransactionView alias BlockScoutWeb.TransactionView
describe "confirmations/2" do
test "returns 0 if pending transaction" do
transaction = build(:transaction, block: nil)
assert 0 == TransactionView.confirmations(transaction, [])
end
test "returns string of number of blocks validated since subject block" do
block = insert(:block)
transaction =
:transaction
|> insert()
|> with_block(block)
assert "1" == TransactionView.confirmations(transaction, max_block_number: block.number + 1)
end
end
describe "contract_creation?/1" do
test "returns true if contract creation transaction" do
assert TransactionView.contract_creation?(build(:transaction, to_address: nil))
end
test "returns false if not contract" do
refute TransactionView.contract_creation?(build(:transaction))
end
end
describe "formatted_fee/2" do describe "formatted_fee/2" do
test "pending transaction with no Receipt" do test "pending transaction with no Receipt" do
{:ok, gas_price} = Wei.cast(3_000_000_000) {:ok, gas_price} = Wei.cast(3_000_000_000)
@ -78,10 +107,32 @@ defmodule BlockScoutWeb.TransactionViewTest do
end end
end end
test "gas/1 returns the gas as a string" do
assert "2" == TransactionView.gas(build(:transaction, gas: 2))
end
test "hash/1 returns the hash as a string" do
assert "test" == TransactionView.hash(build(:transaction, hash: "test"))
end
describe "qr_code/1" do describe "qr_code/1" do
test "it returns an encoded value" do test "it returns an encoded value" do
transaction = build(:transaction) transaction = build(:transaction)
assert {:ok, _} = Base.decode64(TransactionView.qr_code(transaction)) assert {:ok, _} = Base.decode64(TransactionView.qr_code(transaction))
end end
end end
describe "to_address_hash/1" do
test "returns contract address for created contract transaction" do
contract = insert(:contract_address)
transaction = insert(:transaction, to_address: nil, created_contract_address: contract)
assert contract.hash == TransactionView.to_address_hash(transaction)
end
test "returns hash for transaction" do
address = insert(:address)
transaction = insert(:transaction, to_address: address)
assert address.hash == TransactionView.to_address_hash(transaction)
end
end
end end

@ -1635,11 +1635,6 @@ defmodule Explorer.Chain do
TokenTransfer.count_token_transfers_from_token_hash(token_address_hash) TokenTransfer.count_token_transfers_from_token_hash(token_address_hash)
end end
@spec count_addresses_in_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_addresses_in_token_transfers_from_token_hash(token_address_hash) do
TokenTransfer.count_addresses_in_token_transfers_from_token_hash(token_address_hash)
end
@spec transaction_has_token_transfers?(Hash.t()) :: boolean() @spec transaction_has_token_transfers?(Hash.t()) :: boolean()
def transaction_has_token_transfers?(transaction_hash) do def transaction_has_token_transfers?(transaction_hash) do
query = from(tt in TokenTransfer, where: tt.transaction_hash == ^transaction_hash, limit: 1, select: 1) query = from(tt in TokenTransfer, where: tt.transaction_hash == ^transaction_hash, limit: 1, select: 1)
@ -1718,4 +1713,18 @@ defmodule Explorer.Chain do
|> TokenBalance.last_token_balances() |> TokenBalance.last_token_balances()
|> Repo.all() |> Repo.all()
end end
@spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()]
def fetch_token_holders_from_token_hash(contract_address_hash, options) do
contract_address_hash
|> TokenBalance.token_holders_ordered_by_value(options)
|> Repo.all()
end
@spec count_token_holders_from_token_hash(Hash.Address.t()) :: non_neg_integer()
def count_token_holders_from_token_hash(contract_address_hash) do
contract_address_hash
|> TokenBalance.token_holders_from_token_hash()
|> Repo.aggregate(:count, :address_hash)
end
end end

@ -5,11 +5,14 @@ defmodule Explorer.Chain.Address.TokenBalance do
use Ecto.Schema use Ecto.Schema
import Ecto.Changeset import Ecto.Changeset
import Ecto.Query, only: [from: 2] import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3, preload: 2]
alias Explorer.{Chain, PagingOptions}
alias Explorer.Chain.Address.TokenBalance alias Explorer.Chain.Address.TokenBalance
alias Explorer.Chain.{Address, Block, Hash, Token} alias Explorer.Chain.{Address, Block, Hash, Token}
@default_paging_options %PagingOptions{page_size: 50}
@typedoc """ @typedoc """
* `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner.
* `address_hash` - The address hash foreign key. * `address_hash` - The address hash foreign key.
@ -62,17 +65,68 @@ defmodule Explorer.Chain.Address.TokenBalance do
end end
@doc """ @doc """
Builds an `Ecto.Query` to fetch the last token balances. Builds an `Ecto.Query` to fetch the last token balances that have value greater than 0.
The last token balances from an Address is the last block indexed. The last token balances from an Address is the last block indexed.
""" """
def last_token_balances(address_hash) do def last_token_balances(address_hash) do
query =
from(
tb in TokenBalance,
where: tb.address_hash == ^address_hash,
distinct: :token_contract_address_hash,
order_by: [desc: :block_number]
)
from(tb in subquery(query), where: tb.value > 0, preload: :token)
end
@doc """
Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash.
The Token Holders are the addresses that own a positive amount of the Token. So this query is
considering the following conditions:
* The token balance from the last block.
* Balances greater than 0.
* Excluding the burn address (0x0000000000000000000000000000000000000000).
"""
def token_holders_from_token_hash(token_contract_address_hash) do
query = token_holders_query(token_contract_address_hash)
from(tb in subquery(query), where: tb.value > 0)
end
def token_holders_ordered_by_value(token_contract_address_hash, options) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
token_contract_address_hash
|> token_holders_from_token_hash()
|> order_by([tb], desc: tb.value, desc: tb.address_hash)
|> preload(:address)
|> page_token_balances(paging_options)
|> limit(^paging_options.page_size)
end
defp token_holders_query(contract_address_hash) do
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
from( from(
tb in TokenBalance, tb in TokenBalance,
where: tb.address_hash == ^address_hash and tb.value > 0, distinct: :address_hash,
distinct: :token_contract_address_hash, where: tb.token_contract_address_hash == ^contract_address_hash and tb.address_hash != ^burn_address_hash,
order_by: [desc: :block_number], order_by: [desc: :block_number]
preload: :token )
end
defp page_token_balances(query, %PagingOptions{key: nil}), do: query
defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do
where(
query,
[tb],
tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash)
) )
end end
end end

@ -28,7 +28,6 @@ defmodule Explorer.Chain.TokenTransfer do
alias Explorer.Chain.{Address, Block, Hash, Transaction, TokenTransfer} alias Explorer.Chain.{Address, Block, Hash, Transaction, TokenTransfer}
alias Explorer.{PagingOptions, Repo} alias Explorer.{PagingOptions, Repo}
alias Ecto.Adapters.SQL
@default_paging_options %PagingOptions{page_size: 50} @default_paging_options %PagingOptions{page_size: 50}
@ -140,32 +139,6 @@ defmodule Explorer.Chain.TokenTransfer do
Repo.one(query) Repo.one(query)
end end
@spec count_addresses_in_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_addresses_in_token_transfers_from_token_hash(token_address_hash) do
{:ok, %{rows: [[result]]}} =
SQL.query(
Repo,
"""
select count(*) as "addresses"
from
(
select to_address_hash as "address_hash"
from token_transfers tt1
where tt1.token_contract_address_hash = $1
union
select from_address_hash as "address_hash"
from token_transfers tt2
where tt2.token_contract_address_hash = $1
) as addresses_count
""",
[token_address_hash.bytes]
)
result
end
def page_token_transfer(query, %PagingOptions{key: nil}), do: query def page_token_transfer(query, %PagingOptions{key: nil}), do: query
def page_token_transfer(query, %PagingOptions{key: inserted_at}) do def page_token_transfer(query, %PagingOptions{key: inserted_at}) do

@ -142,53 +142,4 @@ defmodule Explorer.Chain.TokenTransferTest do
assert TokenTransfer.count_token_transfers_from_token_hash(token_contract_address.hash) == 2 assert TokenTransfer.count_token_transfers_from_token_hash(token_contract_address.hash) == 2
end end
end end
describe "count_addresses_in_transfers/1" do
test "counts how many unique addresses that appeared at `to` or `from`" do
token_contract_address = insert(:contract_address)
transaction =
:transaction
|> insert()
|> with_block()
john_address = insert(:address)
jane_address = insert(:address)
bob_address = insert(:address)
insert(
:token_transfer,
from_address: jane_address,
to_address: john_address,
transaction: transaction,
token_contract_address: token_contract_address
)
insert(
:token_transfer,
from_address: john_address,
to_address: jane_address,
transaction: transaction,
token_contract_address: token_contract_address
)
insert(
:token_transfer,
from_address: bob_address,
to_address: jane_address,
transaction: transaction,
token_contract_address: token_contract_address
)
insert(
:token_transfer,
from_address: jane_address,
to_address: bob_address,
transaction: transaction,
token_contract_address: token_contract_address
)
assert TokenTransfer.count_addresses_in_token_transfers_from_token_hash(token_contract_address.hash) == 3
end
end
end end

@ -507,28 +507,6 @@ defmodule Explorer.ChainTest do
end end
end end
describe "count_addresses_in_token_transfers_from_token_hash/1" do
test "without token transfers" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
assert Chain.count_addresses_in_token_transfers_from_token_hash(contract_address_hash) == 0
end
test "with token transfers" do
address = insert(:address)
transaction =
:transaction
|> insert()
|> with_block()
%TokenTransfer{token_contract_address_hash: token_contract_address_hash} =
insert(:token_transfer, to_address: address, transaction: transaction)
assert Chain.count_addresses_in_token_transfers_from_token_hash(token_contract_address_hash) == 2
end
end
describe "gas_price/2" do describe "gas_price/2" do
test ":wei unit" do test ":wei unit" do
assert Chain.gas_price(%Transaction{gas_price: %Wei{value: Decimal.new(1)}}, :wei) == Decimal.new(1) assert Chain.gas_price(%Transaction{gas_price: %Wei{value: Decimal.new(1)}}, :wei) == Decimal.new(1)
@ -2687,5 +2665,274 @@ defmodule Explorer.ChainTest do
assert Chain.fetch_last_token_balances(address.hash) == [] assert Chain.fetch_last_token_balances(address.hash) == []
end end
test "does not consider other blocks when the last block has the value 0" do
address = insert(:address)
token = insert(:token, contract_address: build(:contract_address))
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: token.contract_address_hash,
value: 5000
)
insert(
:token_balance,
address: address,
block_number: 1001,
token_contract_address_hash: token.contract_address_hash,
value: 0
)
assert Chain.fetch_last_token_balances(address.hash) == []
end
end
describe "fetch_token_holders_from_token_hash/2" do
test "returns the last value for each address" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
address = insert(:address)
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:token_balance,
block_number: 1001,
token_contract_address_hash: contract_address_hash,
value: 4000
)
insert(
:token_balance,
address: address,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 2000
)
values =
contract_address_hash
|> Chain.fetch_token_holders_from_token_hash([])
|> Enum.map(&Decimal.to_integer(&1.value))
assert values == [4000, 2000]
end
test "sort by the hightest value" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 2000
)
insert(
:token_balance,
block_number: 1001,
token_contract_address_hash: contract_address_hash,
value: 1000
)
insert(
:token_balance,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 4000
)
insert(
:token_balance,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 3000
)
values =
contract_address_hash
|> Chain.fetch_token_holders_from_token_hash([])
|> Enum.map(&Decimal.to_integer(&1.value))
assert values == [4000, 3000, 2000, 1000]
end
test "returns only token balances that have value" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
token_contract_address_hash: contract_address_hash,
value: 0
)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
end
test "returns an empty list when there are no address with value greater than 0" do
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(:token_balance, value: 1000)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
end
test "ignores the burn address" do
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
burn_address = insert(:address, hash: burn_address_hash)
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
address: burn_address,
token_contract_address_hash: contract_address_hash,
value: 1000
)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
end
test "paginates the result by value and different address" do
address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a")
address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
first_page =
insert(
:token_balance,
address: address_a,
token_contract_address_hash: contract_address_hash,
value: 4000
)
second_page =
insert(
:token_balance,
address: address_b,
token_contract_address_hash: contract_address_hash,
value: 4000
)
paging_options = %PagingOptions{
key: {first_page.value, first_page.address_hash},
page_size: 2
}
holders_paginated =
contract_address_hash
|> Chain.fetch_token_holders_from_token_hash(paging_options: paging_options)
|> Enum.map(& &1.address_hash)
assert holders_paginated == [second_page.address_hash]
end
test "considers the last block only if it has value" do
address = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:token_balance,
address: address,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 0
)
assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == []
end
end
describe "count_token_holders_from_token_hash" do
test "counts different addresses that have the token" do
address_a = insert(:address, hash: "0xe49fedd93960a0267b3c3b2c1e2d66028e013fee")
address_b = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
address: address_a,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:token_balance,
address: address_b,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 1000
)
assert Chain.count_token_holders_from_token_hash(contract_address_hash) == 2
end
test "counts only the last block" do
address = insert(:address, hash: "0xe49fedd93960a0267b3c3b2c1e2d66028e013fee")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:token_balance,
address: address,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 1000
)
assert Chain.count_token_holders_from_token_hash(contract_address_hash) == 1
end
test "counts only the last block that has value greater than 0" do
address = insert(:address, hash: "0xe49fedd93960a0267b3c3b2c1e2d66028e013fee")
%Token{contract_address_hash: contract_address_hash} = insert(:token)
insert(
:token_balance,
address: address,
block_number: 1000,
token_contract_address_hash: contract_address_hash,
value: 5000
)
insert(
:token_balance,
address: address,
block_number: 1002,
token_contract_address_hash: contract_address_hash,
value: 0
)
assert Chain.count_token_holders_from_token_hash(contract_address_hash) == 0
end
end end
end end

Loading…
Cancel
Save