diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index b5115a24a4..0f95dbf824 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -12,7 +12,18 @@ defmodule BlockScoutWeb.Chain do 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 @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}), 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 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} 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 with {:ok, hash} <- string_to_transaction_hash(param) do hash_to_transaction(hash) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex new file mode 100644 index 0000000000..f4443db645 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex index c1f0a42671..0d73cfa237 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex @@ -11,7 +11,7 @@ defmodule BlockScoutWeb.Tokens.ReadContractController do "index.html", token: token, 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 :error -> diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex index 92e14d819f..9185a90ecb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.Tokens.TokenController do transfers: token_transfers_paginated, token: token, 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) ) else diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index e6bfe0e5ba..f521e03647 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -113,6 +113,13 @@ defmodule BlockScoutWeb.Router do only: [:index], as: :read_contract ) + + resources( + "/token_holders", + Tokens.HolderController, + only: [:index], + as: :holder + ) end resources( diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex index 8c8490a69a..463d3ba5ba 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_link.html.eex @@ -1,5 +1,3 @@ -<%= if @address_hash 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] %> - <% end %> +<%= 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] %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex new file mode 100644 index 0000000000..9901f73584 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/_token_balances.html.eex @@ -0,0 +1,19 @@ +
+
+
+ + <%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @token_balance.address_hash, contract: BlockScoutWeb.AddressView.contract?(@token_balance.address) %> + + + + + <%= format_token_balance_value(@token_balance.value, @token) %> <%= @token.symbol %> + + + <%= if @token.total_supply > 0 do %> + (<%= total_supply_percentage(@token_balance.value, @token.total_supply) %>) + <% end %> + +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex new file mode 100644 index 0000000000..be4226dd10 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/holder/index.html.eex @@ -0,0 +1,94 @@ +
+ <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + total_token_holders: @total_token_holders + ) %> + +
+
+
+ + + + + +
+ + +
+

<%= gettext "Token Holders" %>

+ + <%= if Enum.any?(@token_balances) do %> + <%= for token_balance <- @token_balances do %> + <%= render "_token_balances.html", token: @token, token_balance: token_balance %> + <% end %> + <% else %> +
+ + <%= gettext "There are no holders for this Token." %> + +
+ <% 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 %> +
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex index bbdcfd045f..6c634c5fea 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex @@ -27,7 +27,7 @@
<%= @token.type %> - <%= @total_address_in_token_transfers %> <%= gettext "addresses" %> + <%= @total_token_holders %> <%= gettext "addresses" %> <%= @total_token_transfers %> <%= gettext "Transfers" %> <%= if decimals?(@token) do %> <%= @token.decimals %> <%= gettext "decimals" %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex index 06991581f9..bc34b96501 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/read_contract/index.html.eex @@ -4,7 +4,7 @@ "_details.html", token: @token, total_token_transfers: @total_token_transfers, - total_address_in_token_transfers: @total_address_in_token_transfers + total_token_holders: @total_token_holders ) %>
@@ -26,6 +26,15 @@ to: token_read_contract_path(@conn, :index, @conn.params["token_id"]), class: "nav-link active")%> + + @@ -42,6 +51,11 @@ gettext("Read Contract"), to: "#", class: "nav-link")%> + <%= link( + gettext("Token Holders"), + class: "dropdown-item", + to: token_holder_path(@conn, :index, @token.contract_address_hash) + ) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/token/show.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/token/show.html.eex index 59b1a762ab..85e0b61b17 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/token/show.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/token/show.html.eex @@ -4,7 +4,7 @@ "_details.html", token: @token, total_token_transfers: @total_token_transfers, - total_address_in_token_transfers: @total_address_in_token_transfers + total_token_holders: @total_token_holders ) %>
@@ -28,6 +28,15 @@ class: "nav-link")%> <% end %> + + @@ -37,15 +46,20 @@ diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex index ce506dbcfb..fd17a86d69 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex @@ -11,13 +11,9 @@
<%= render "_link.html", transaction_hash: @transaction.hash %> - <%= 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() %> → - <%= if assigns[:current_address] && assigns[:current_address].hash == to_address_hash(@transaction) do %> - <%= 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 %> + <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex index 538dab337f..3ee194e882 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex @@ -11,9 +11,9 @@ <% 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() %> → - <%= 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() %> <%= token_transfer_amount(@token_transfer) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex index e5c9debace..63b34d0df2 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex @@ -14,14 +14,10 @@

<%= gettext "Transaction Details" %>

<%= @transaction %>

- - <%= 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() %> - <%= if @transaction.to_address_hash do %> - <%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @transaction.to_address_hash, contract: BlockScoutWeb.AddressView.contract?(@transaction.to_address) %> - <% else %> - <%= gettext("Contract Address Pending") %> - <% end %> + <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, nil) |> BlockScoutWeb.AddressView.render_partial() %>
<%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index 5cfd4ec703..83e236f7db 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -1,10 +1,46 @@ defmodule BlockScoutWeb.AddressView do use BlockScoutWeb, :view - alias Explorer.Chain.{Address, Hash, SmartContract} + alias Explorer.Chain.{Address, Hash, SmartContract, TokenTransfer, Transaction} @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 if contract?(address) do gettext("Contract Address") @@ -45,16 +81,20 @@ defmodule BlockScoutWeb.AddressView do |> Base.encode64() 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 - string_hash = to_string(hash) - "#{String.slice(string_hash, 0..5)}–#{String.slice(string_hash, -6..-1)}" - end + def smart_contract_verified?(%Address{smart_contract: %SmartContract{}}), do: true - 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 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 display_address_hash(current_address, target_address, truncate \\ false) - - def display_address_hash(nil, target_address, truncate) do - render( - "_link.html", - address_hash: target_address.hash, - contract: contract?(target_address), - truncate: truncate - ) + def trimmed_hash(%Hash{} = hash) do + string_hash = to_string(hash) + "#{String.slice(string_hash, 0..5)}–#{String.slice(string_hash, -6..-1)}" end - def display_address_hash(current_address, target_address, truncate) do - if current_address.hash == target_address.hash do - render( - "_responsive_hash.html", - address_hash: current_address.hash, - contract: contract?(current_address), + def trimmed_hash(_), do: "" + + defp matching_address_check(current_address, hash, contract?, truncate) do + if current_address && current_address.hash == hash do + %{ + partial: "_responsive_hash.html", + address_hash: hash, + contract: contract?, truncate: truncate - ) + } else - render( - "_link.html", - address_hash: target_address.hash, - contract: contract?(target_address), + %{ + partial: "_link.html", + address_hash: hash, + contract: contract?, truncate: truncate - ) + } end end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex new file mode 100644 index 0000000000..a69a859ab2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/holder_view.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 402352dd95..fea9d63815 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -10,6 +10,8 @@ defmodule BlockScoutWeb.TransactionView do 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 case block do nil -> 0 @@ -17,22 +19,19 @@ defmodule BlockScoutWeb.TransactionView do end end - 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 - - # 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 contract_creation?(%Transaction{to_address: nil}), do: true - def to_address_hash(%Transaction{to_address: %Address{hash: address_hash}}), do: address_hash + def contract_creation?(_), do: false def fee(%Transaction{} = transaction) do {_, value} = Chain.fee(transaction, :wei) value end + def format_gas_limit(gas) do + Number.to_string!(gas) + end + def formatted_fee(%Transaction{} = transaction, opts) do transaction |> Chain.fee(:wei) @@ -43,34 +42,6 @@ defmodule BlockScoutWeb.TransactionView do 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 transaction |> Chain.transaction_to_status() @@ -82,7 +53,11 @@ defmodule BlockScoutWeb.TransactionView do 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 Cldr.Number.to_string!(gas) @@ -95,22 +70,39 @@ defmodule BlockScoutWeb.TransactionView do format_wei_value(gas_price, unit) 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 to_string(hash) 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 Chain.transaction_to_status(transaction) 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 + # 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 transaction_display_type(%Transaction{} = transaction) do cond do @@ -121,6 +113,15 @@ defmodule BlockScoutWeb.TransactionView do 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 """ Converts a transaction's Wei value to Ether and returns a formatted display value. diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index de0e2977bd..ec80196c48 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -52,7 +52,7 @@ msgstr "" msgid "Transactions" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:91 +#: lib/block_scout_web/templates/transaction/overview.html.eex:87 msgid "Value" msgstr "" @@ -76,7 +76,7 @@ msgid "Miner" msgstr "" #: 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" msgstr "" @@ -100,7 +100,7 @@ msgstr "" msgid "Total Difficulty" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:38 +#: lib/block_scout_web/templates/transaction/overview.html.eex:34 msgid "Block Number" msgstr "" @@ -112,7 +112,7 @@ msgstr "" msgid "Cumulative Gas Used" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:102 +#: lib/block_scout_web/templates/transaction/overview.html.eex:98 msgid "Gas" msgstr "" @@ -120,7 +120,7 @@ msgstr "" msgid "Gas Price" msgstr "" -#: lib/block_scout_web/templates/transaction/overview.html.eex:72 +#: lib/block_scout_web/templates/transaction/overview.html.eex:68 msgid "Input" msgstr "" @@ -132,7 +132,7 @@ msgstr "" msgid "%{count} transactions in this block" msgstr "" -#: lib/block_scout_web/views/address_view.ex:12 +#: lib/block_scout_web/views/address_view.ex:48 msgid "Address" msgstr "" @@ -148,7 +148,7 @@ msgstr "" msgid "Overview" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:81 +#: lib/block_scout_web/views/transaction_view.ex:52 msgid "Success" msgstr "" @@ -199,9 +199,9 @@ msgstr "" #: 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:35 -#: lib/block_scout_web/templates/transaction/overview.html.eex:47 -#: lib/block_scout_web/views/transaction_view.ex:46 -#: lib/block_scout_web/views/transaction_view.ex:80 +#: lib/block_scout_web/templates/transaction/overview.html.eex:43 +#: lib/block_scout_web/views/transaction_view.ex:51 +#: lib/block_scout_web/views/transaction_view.ex:73 msgid "Pending" msgstr "" @@ -265,15 +265,15 @@ msgstr "" msgid "TPM" 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" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:78 +#: lib/block_scout_web/views/transaction_view.ex:49 msgid "Failed" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:79 +#: lib/block_scout_web/views/transaction_view.ex:50 msgid "Out of Gas" msgstr "" @@ -292,8 +292,8 @@ msgstr "" #: #: 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/transaction/_tile.html.eex:24 -#: lib/block_scout_web/templates/transaction/overview.html.eex:91 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:20 +#: 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/views/wei_helpers.ex:72 msgid "Ether" @@ -453,7 +453,7 @@ msgstr "" msgid "Total Gas Used" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:120 +#: lib/block_scout_web/views/transaction_view.ex:112 msgid "Transaction" msgstr "" @@ -467,8 +467,8 @@ msgid "View All" msgstr "" #: 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/overview.html.eex:64 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:23 +#: lib/block_scout_web/templates/transaction/overview.html.eex:60 msgid "TX Fee" msgstr "" @@ -476,7 +476,7 @@ msgstr "" msgid "Contract" msgstr "" -#: lib/block_scout_web/views/address_view.ex:10 +#: lib/block_scout_web/views/address_view.ex:46 msgid "Contract Address" msgstr "" @@ -493,7 +493,7 @@ msgstr "" #: 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/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_token_transfer/index.html.eex:72 msgid "Older" @@ -540,7 +540,7 @@ msgid "Newer" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:118 +#: lib/block_scout_web/views/transaction_view.ex:110 msgid "Contract Creation" msgstr "" @@ -634,12 +634,12 @@ msgstr "" #, elixir-format #: 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" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:119 +#: lib/block_scout_web/views/transaction_view.ex:111 msgid "Contract Call" msgstr "" @@ -655,21 +655,21 @@ msgstr "" #, elixir-format #: 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}" msgstr "" #, elixir-format #: #: 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" msgstr "" #, elixir-format #: #: 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" 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:58 #: 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: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:45 +#: lib/block_scout_web/templates/tokens/token/show.html.eex:54 msgid "Read Contract" msgstr "" @@ -714,12 +716,12 @@ msgid "Github" msgstr "" #, 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" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:114 +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 msgid "Limit" msgstr "" @@ -735,14 +737,14 @@ msgid "There are no logs for this transaction." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:107 +#: lib/block_scout_web/templates/transaction/overview.html.eex:103 msgid "Used" msgstr "" #, elixir-format #: 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/views/transaction_view.ex:117 +#: lib/block_scout_web/views/transaction_view.ex:109 msgid "Token Transfer" msgstr "" @@ -775,7 +777,7 @@ msgstr "" msgid "Validated Transactions" 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." msgstr "" @@ -785,13 +787,15 @@ msgid "Token Details" msgstr "" #, 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:34 -#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:37 +#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:43 +#: 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:36 -#: lib/block_scout_web/templates/tokens/token/show.html.eex:39 -#: lib/block_scout_web/templates/tokens/token/show.html.eex:55 +#: lib/block_scout_web/templates/tokens/token/show.html.eex:45 +#: lib/block_scout_web/templates/tokens/token/show.html.eex:48 +#: 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:42 #: lib/block_scout_web/templates/transaction_log/index.html.eex:13 @@ -830,7 +834,7 @@ msgstr "" #, elixir-format #: 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..." msgstr "" @@ -1001,12 +1005,12 @@ msgid "loading....." msgstr "" #, 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" msgstr "" #, 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" msgstr "" @@ -1016,7 +1020,7 @@ msgid "Less than" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:42 +#: lib/block_scout_web/views/transaction_view.ex:41 msgid "Max of" msgstr "" @@ -1056,3 +1060,20 @@ msgstr "" #: lib/block_scout_web/templates/address_token/index.html.eex:111 msgid "There are no tokens for this address." 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 "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index dd61522398..c052890aed 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -64,7 +64,7 @@ msgstr "BlockScout" msgid "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" msgstr "Value" @@ -88,7 +88,7 @@ msgid "Miner" msgstr "Validator" #: 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" msgstr "Nonce" @@ -112,7 +112,7 @@ msgstr "Timestamp" msgid "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" msgstr "Block Height" @@ -124,7 +124,7 @@ msgstr "Transaction Details" msgid "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" msgstr "Gas" @@ -132,7 +132,7 @@ msgstr "Gas" msgid "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" msgstr "Input" @@ -144,7 +144,7 @@ msgstr "%{confirmations} block confirmations" msgid "%{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" msgstr "Address" @@ -160,7 +160,7 @@ msgstr "From" msgid "Overview" msgstr "Overview" -#: lib/block_scout_web/views/transaction_view.ex:81 +#: lib/block_scout_web/views/transaction_view.ex:52 msgid "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/transaction/index.html.eex:16 #: lib/block_scout_web/templates/transaction/index.html.eex:35 -#: lib/block_scout_web/templates/transaction/overview.html.eex:47 -#: lib/block_scout_web/views/transaction_view.ex:46 -#: lib/block_scout_web/views/transaction_view.ex:80 +#: lib/block_scout_web/templates/transaction/overview.html.eex:43 +#: lib/block_scout_web/views/transaction_view.ex:51 +#: lib/block_scout_web/views/transaction_view.ex:73 msgid "Pending" msgstr "Pending" @@ -277,15 +277,15 @@ msgstr "" msgid "TPM" 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" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:78 +#: lib/block_scout_web/views/transaction_view.ex:49 msgid "Failed" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:79 +#: lib/block_scout_web/views/transaction_view.ex:50 msgid "Out of Gas" msgstr "" @@ -304,8 +304,8 @@ msgstr "" #: #: 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/transaction/_tile.html.eex:24 -#: lib/block_scout_web/templates/transaction/overview.html.eex:91 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:20 +#: 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/views/wei_helpers.ex:72 msgid "Ether" @@ -465,7 +465,7 @@ msgstr "" msgid "Total Gas Used" msgstr "" -#: lib/block_scout_web/views/transaction_view.ex:120 +#: lib/block_scout_web/views/transaction_view.ex:112 msgid "Transaction" msgstr "" @@ -479,8 +479,8 @@ msgid "View All" msgstr "" #: 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/overview.html.eex:64 +#: lib/block_scout_web/templates/transaction/_tile.html.eex:23 +#: lib/block_scout_web/templates/transaction/overview.html.eex:60 msgid "TX Fee" msgstr "" @@ -488,7 +488,7 @@ msgstr "" msgid "Contract" msgstr "" -#: lib/block_scout_web/views/address_view.ex:10 +#: lib/block_scout_web/views/address_view.ex:46 msgid "Contract Address" msgstr "" @@ -505,7 +505,7 @@ msgstr "" #: 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/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_token_transfer/index.html.eex:72 msgid "Older" @@ -552,7 +552,7 @@ msgid "Newer" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:118 +#: lib/block_scout_web/views/transaction_view.ex:110 msgid "Contract Creation" msgstr "" @@ -646,12 +646,12 @@ msgstr "" #, elixir-format #: 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" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:119 +#: lib/block_scout_web/views/transaction_view.ex:111 msgid "Contract Call" msgstr "" @@ -667,21 +667,21 @@ msgstr "" #, elixir-format #: 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}" msgstr "" #, elixir-format #: #: 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" msgstr "" #, elixir-format #: #: 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" 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:58 #: 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: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:45 +#: lib/block_scout_web/templates/tokens/token/show.html.eex:54 msgid "Read Contract" msgstr "" @@ -726,12 +728,12 @@ msgid "Github" msgstr "" #, 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" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:114 +#: lib/block_scout_web/templates/transaction/overview.html.eex:110 msgid "Limit" msgstr "" @@ -747,14 +749,14 @@ msgid "There are no logs for this transaction." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/transaction/overview.html.eex:107 +#: lib/block_scout_web/templates/transaction/overview.html.eex:103 msgid "Used" msgstr "" #, elixir-format #: 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/views/transaction_view.ex:117 +#: lib/block_scout_web/views/transaction_view.ex:109 msgid "Token Transfer" msgstr "" @@ -787,7 +789,7 @@ msgstr "" msgid "Validated Transactions" 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." msgstr "" @@ -797,13 +799,15 @@ msgid "Token Details" msgstr "" #, 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:34 -#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:37 +#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:43 +#: 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:36 -#: lib/block_scout_web/templates/tokens/token/show.html.eex:39 -#: lib/block_scout_web/templates/tokens/token/show.html.eex:55 +#: lib/block_scout_web/templates/tokens/token/show.html.eex:45 +#: lib/block_scout_web/templates/tokens/token/show.html.eex:48 +#: 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:42 #: lib/block_scout_web/templates/transaction_log/index.html.eex:13 @@ -842,7 +846,7 @@ msgstr "" #, elixir-format #: 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..." msgstr "" @@ -1013,12 +1017,12 @@ msgid "loading....." msgstr "" #, 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" msgstr "" #, 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" msgstr "" @@ -1028,7 +1032,7 @@ msgid "Less than" msgstr "" #, elixir-format -#: lib/block_scout_web/views/transaction_view.ex:42 +#: lib/block_scout_web/views/transaction_view.ex:41 msgid "Max of" msgstr "" @@ -1068,3 +1072,20 @@ msgstr "" #: lib/block_scout_web/templates/address_token/index.html.eex:111 msgid "There are no tokens for this address." 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 "" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs new file mode 100644 index 0000000000..6ce8d7915b --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs index 5c188d1762..5b72d3b92f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/read_contract_controller_test.exs @@ -31,7 +31,7 @@ defmodule BlockScoutWeb.Tokens.ReadContractControllerTest do assert html_response(conn, 200) assert token.contract_address_hash == conn.assigns.token.contract_address_hash assert conn.assigns.total_token_transfers - assert conn.assigns.total_address_in_token_transfers + assert conn.assigns.total_token_holders end end end diff --git a/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex b/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex new file mode 100644 index 0000000000..6db529aff9 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/features/pages/token_page.ex @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs index bbd3b1a1ec..ad998dbaaf 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs @@ -2,7 +2,6 @@ defmodule BlockScoutWeb.ViewingAddressesTest do use BlockScoutWeb.FeatureCase, async: true alias Explorer.Chain.Wei - alias Explorer.Factory alias BlockScoutWeb.{AddressPage, AddressView, Notifier} setup do @@ -41,7 +40,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do describe "viewing contract creator" do test "see the contract creator and transaction links", %{session: session} do 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) 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 lincoln = insert(:address) - contract = insert(:address, contract_code: Factory.data("contract_code")) + contract = insert(:contract_address) transaction = insert(:transaction) - another_contract = insert(:address, contract_code: Factory.data("contract_code")) + another_contract = insert(:contract_address) insert( :internal_transaction, @@ -285,12 +284,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do lincoln = addresses.lincoln taft = addresses.taft - contract_token_address = insert(:contract_address) - insert(:token, contract_address: contract_token_address) + contract_address = insert(:contract_address) + insert(:token, contract_address: contract_address) transaction = :transaction - |> insert(from_address: lincoln, to_address: contract_token_address) + |> insert(from_address: lincoln, to_address: contract_address) |> with_block(block) insert( @@ -298,7 +297,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: lincoln, to_address: taft, transaction: transaction, - token_contract_address: contract_token_address + token_contract_address: contract_address ) session @@ -318,12 +317,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do taft = addresses.taft morty = build(:address) - contract_token_address = insert(:contract_address) - insert(:token, contract_address: contract_token_address) + contract_address = insert(:contract_address) + insert(:token, contract_address: contract_address) transaction = :transaction - |> insert(from_address: lincoln, to_address: contract_token_address) + |> insert(from_address: lincoln, to_address: contract_address) |> with_block(block) insert( @@ -331,7 +330,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: lincoln, to_address: taft, transaction: transaction, - token_contract_address: contract_token_address + token_contract_address: contract_address ) insert( @@ -339,7 +338,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: lincoln, to_address: morty, transaction: transaction, - token_contract_address: contract_token_address + token_contract_address: contract_address ) session @@ -358,17 +357,13 @@ defmodule BlockScoutWeb.ViewingAddressesTest do lincoln = addresses.lincoln taft = addresses.taft - contract_token_address = - insert( - :address, - contract_code: Factory.data("contract_code") - ) + contract_address = insert(:contract_address) - insert(:token, contract_address: contract_token_address) + insert(:token, contract_address: contract_address) transaction = :transaction - |> insert(from_address: lincoln, to_address: contract_token_address) + |> insert(from_address: lincoln, to_address: contract_address) |> with_block(block) insert_list( @@ -377,7 +372,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: lincoln, to_address: taft, transaction: transaction, - token_contract_address: contract_token_address + token_contract_address: contract_address ) session @@ -393,12 +388,12 @@ defmodule BlockScoutWeb.ViewingAddressesTest do lincoln = addresses.lincoln taft = addresses.taft - contract_token_address = insert(:contract_address) - insert(:token, contract_address: contract_token_address) + contract_address = insert(:contract_address) + insert(:token, contract_address: contract_address) transaction = :transaction - |> insert(from_address: lincoln, to_address: contract_token_address) + |> insert(from_address: lincoln, to_address: contract_address) |> with_block(block) insert_list( @@ -407,7 +402,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do from_address: lincoln, to_address: taft, transaction: transaction, - token_contract_address: contract_token_address + token_contract_address: contract_address ) session diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs new file mode 100644 index 0000000000..8a5d769e62 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs index 7ba5c71171..3bce75c435 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs @@ -1,9 +1,106 @@ defmodule BlockScoutWeb.AddressViewTest do use BlockScoutWeb.ConnCase, async: true - alias Explorer.Chain.Data + alias Explorer.Chain.{Address, Data, Transaction} 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 test "with a smart contract" do {:ok, code} = Data.cast("0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef") @@ -15,6 +112,10 @@ defmodule BlockScoutWeb.AddressViewTest do address = insert(:address, contract_code: nil) refute AddressView.contract?(address) end + + test "with nil address" do + assert AddressView.contract?(nil) + end end describe "qr_code/1" do @@ -24,6 +125,27 @@ defmodule BlockScoutWeb.AddressViewTest do 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 test "returns true when smart contract is verified" do smart_contract = insert(:smart_contract) diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs new file mode 100644 index 0000000000..6cd0cd3134 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/holder_view_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs index cef595b5aa..ee32c5df4e 100644 --- a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs @@ -5,6 +5,35 @@ defmodule BlockScoutWeb.TransactionViewTest do alias Explorer.Repo 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 test "pending transaction with no Receipt" do {:ok, gas_price} = Wei.cast(3_000_000_000) @@ -78,10 +107,32 @@ defmodule BlockScoutWeb.TransactionViewTest do 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 test "it returns an encoded value" do transaction = build(:transaction) assert {:ok, _} = Base.decode64(TransactionView.qr_code(transaction)) 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 diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index d2c7ec0aaa..6816f16ba3 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -1635,11 +1635,6 @@ defmodule Explorer.Chain do TokenTransfer.count_token_transfers_from_token_hash(token_address_hash) 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() def transaction_has_token_transfers?(transaction_hash) do 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() |> Repo.all() 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 diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index 0ce0981da3..c8dec08d45 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -5,11 +5,14 @@ defmodule Explorer.Chain.Address.TokenBalance do use Ecto.Schema 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, Block, Hash, Token} + @default_paging_options %PagingOptions{page_size: 50} + @typedoc """ * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address_hash` - The address hash foreign key. @@ -62,17 +65,68 @@ defmodule Explorer.Chain.Address.TokenBalance do end @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. """ 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( tb in TokenBalance, - where: tb.address_hash == ^address_hash and tb.value > 0, - distinct: :token_contract_address_hash, - order_by: [desc: :block_number], - preload: :token + distinct: :address_hash, + where: tb.token_contract_address_hash == ^contract_address_hash and tb.address_hash != ^burn_address_hash, + order_by: [desc: :block_number] + ) + 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 diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 1723c45ace..fd012da665 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -28,7 +28,6 @@ defmodule Explorer.Chain.TokenTransfer do alias Explorer.Chain.{Address, Block, Hash, Transaction, TokenTransfer} alias Explorer.{PagingOptions, Repo} - alias Ecto.Adapters.SQL @default_paging_options %PagingOptions{page_size: 50} @@ -140,32 +139,6 @@ defmodule Explorer.Chain.TokenTransfer do Repo.one(query) 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: inserted_at}) do diff --git a/apps/explorer/test/explorer/chain/token_transfer_test.exs b/apps/explorer/test/explorer/chain/token_transfer_test.exs index 3a2ec6a13c..a0ec6cf378 100644 --- a/apps/explorer/test/explorer/chain/token_transfer_test.exs +++ b/apps/explorer/test/explorer/chain/token_transfer_test.exs @@ -142,53 +142,4 @@ defmodule Explorer.Chain.TokenTransferTest do assert TokenTransfer.count_token_transfers_from_token_hash(token_contract_address.hash) == 2 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 diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 3270d9aa4c..ce7c6351c7 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -507,28 +507,6 @@ defmodule Explorer.ChainTest do 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 test ":wei unit" do 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) == [] 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