Merge pull request #496 from poanetwork/ln-tokens-transferred-on-transaction-details

Show Token Transfers on Transaction Details page
pull/554/head
Luke Imhoff 6 years ago committed by GitHub
commit 96f9f20ae6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      apps/block_scout_web/lib/block_scout_web/chain.ex
  2. 6
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  3. 4
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_internal_transaction_controller.ex
  4. 4
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex
  5. 63
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_token_transfer_controller.ex
  6. 2
      apps/block_scout_web/lib/block_scout_web/router.ex
  7. 21
      apps/block_scout_web/lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex
  8. 44
      apps/block_scout_web/lib/block_scout_web/templates/transaction_log/index.html.eex
  9. 21
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex
  10. 83
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/index.html.eex
  11. 4
      apps/block_scout_web/lib/block_scout_web/views/token_helpers.ex
  12. 3
      apps/block_scout_web/lib/block_scout_web/views/transaction_token_transfer_view.ex
  13. 53
      apps/block_scout_web/priv/gettext/default.pot
  14. 53
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  15. 23
      apps/block_scout_web/test/block_scout_web/controllers/transaction_controller_test.exs
  16. 140
      apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
  17. 2
      apps/block_scout_web/test/block_scout_web/views/token_helpers_test.exs
  18. 45
      apps/explorer/lib/explorer/chain.ex
  19. 4
      apps/explorer/lib/explorer/chain/token_transfer.ex
  20. 60
      apps/explorer/test/explorer/chain_test.exs
  21. 5
      apps/explorer/test/support/factory.ex

@ -12,7 +12,7 @@ defmodule BlockScoutWeb.Chain do
string_to_transaction_hash: 1 string_to_transaction_hash: 1
] ]
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Transaction, TokenTransfer} alias Explorer.Chain.{Address, Block, InternalTransaction, Log, TokenTransfer, Transaction}
alias Explorer.PagingOptions alias Explorer.PagingOptions
@page_size 50 @page_size 50

@ -34,6 +34,12 @@ defmodule BlockScoutWeb.TransactionController do
end end
def show(conn, %{"id" => id, "locale" => locale}) do def show(conn, %{"id" => id, "locale" => locale}) do
{:ok, transaction_hash} = Chain.string_to_transaction_hash(id)
if Chain.transaction_has_token_transfers?(transaction_hash) do
redirect(conn, to: transaction_token_transfer_path(conn, :index, locale, id))
else
redirect(conn, to: transaction_internal_transaction_path(conn, :index, locale, id)) redirect(conn, to: transaction_internal_transaction_path(conn, :index, locale, id))
end end
end end
end

@ -14,7 +14,8 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
necessity_by_association: %{ necessity_by_association: %{
block: :optional, block: :optional,
from_address: :optional, from_address: :optional,
to_address: :optional to_address: :optional,
token_transfers: :optional
} }
) do ) do
full_options = full_options =
@ -40,6 +41,7 @@ defmodule BlockScoutWeb.TransactionInternalTransactionController do
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
internal_transactions: internal_transactions, internal_transactions: internal_transactions,
max_block_number: max_block_number, max_block_number: max_block_number,
show_token_transfers: Chain.transaction_has_token_transfers?(hash),
next_page_params: next_page_params(next_page, internal_transactions, params), next_page_params: next_page_params(next_page, internal_transactions, params),
transaction: transaction transaction: transaction
) )

@ -14,7 +14,8 @@ defmodule BlockScoutWeb.TransactionLogController do
necessity_by_association: %{ necessity_by_association: %{
block: :optional, block: :optional,
from_address: :required, from_address: :required,
to_address: :optional to_address: :optional,
token_transfers: :optional
} }
) do ) do
full_options = full_options =
@ -36,6 +37,7 @@ defmodule BlockScoutWeb.TransactionLogController do
"index.html", "index.html",
logs: logs, logs: logs,
max_block_number: max_block_number(), max_block_number: max_block_number(),
show_token_transfers: Chain.transaction_has_token_transfers?(transaction_hash),
next_page_params: next_page_params(next_page, logs, params), next_page_params: next_page_params(next_page, logs, params),
transaction: transaction, transaction: transaction,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()

@ -0,0 +1,63 @@
defmodule BlockScoutWeb.TransactionTokenTransferController do
use BlockScoutWeb, :controller
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
def index(conn, %{"transaction_id" => hash_string} = params) do
with {:ok, hash} <- Chain.string_to_transaction_hash(hash_string),
{:ok, transaction} <-
Chain.hash_to_transaction(
hash,
necessity_by_association: %{
block: :optional,
from_address: :optional,
to_address: :optional,
token_transfers: :optional
}
) do
full_options =
Keyword.merge(
[
necessity_by_association: %{
from_address: :required,
to_address: :required,
token: :required
}
],
paging_options(params)
)
token_transfers_plus_one = Chain.transaction_to_token_transfers(transaction, full_options)
{token_transfers, next_page} = split_list_by_page(token_transfers_plus_one)
max_block_number = max_block_number()
render(
conn,
"index.html",
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
max_block_number: max_block_number,
next_page_params: next_page_params(next_page, token_transfers, params),
token_transfers: token_transfers,
transaction: transaction
)
else
:error ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end
defp max_block_number do
case Chain.max_block_number() do
{:ok, number} -> number
{:error, :not_found} -> 0
end
end
end

@ -60,6 +60,8 @@ defmodule BlockScoutWeb.Router do
) )
resources("/logs", TransactionLogController, only: [:index], as: :log) resources("/logs", TransactionLogController, only: [:index], as: :log)
resources("/token_transfers", TransactionTokenTransferController, only: [:index], as: :token_transfer)
end end
resources "/addresses", AddressController, only: [:show] do resources "/addresses", AddressController, only: [:show] do

@ -6,11 +6,20 @@
<!-- DESKTOP TAB NAV --> <!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex"> <ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<%= if @show_token_transfers do %>
<li class="nav-item">
<%= link(
gettext("Token Transfers"),
class: "nav-link",
to: transaction_token_transfer_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
</li>
<% end %>
<li class="nav-item"> <li class="nav-item">
<%= link( <%= link(
gettext("Internal Transactions"), gettext("Internal Transactions"),
class: "nav-link active", class: "nav-link active",
to: transaction_path(@conn, :show, @conn.assigns.locale, @transaction) to: transaction_internal_transaction_path(@conn, :index, @conn.assigns.locale, @transaction)
) %> ) %>
</li> </li>
<li class="nav-item"> <li class="nav-item">
@ -28,10 +37,18 @@
<li class="nav-item dropdown flex-fill text-center"> <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("Internal Transactions") %></a> <a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false"><%= gettext("Internal Transactions") %></a>
<div class="dropdown-menu"> <div class="dropdown-menu">
<%= if @show_token_transfers do %>
<%= link(
gettext("Token Transfers"),
class: "dropdown-item",
to: transaction_token_transfer_path(@conn, :index, @conn.assigns.locale, @transaction),
"data-test": "transaction_token_transfer_link"
) %>
<% end %>
<%= link( <%= link(
gettext("Internal Transactions"), gettext("Internal Transactions"),
class: "dropdown-item", class: "dropdown-item",
to: transaction_path(@conn, :show, @conn.assigns.locale, @transaction) to: transaction_internal_transaction_path(@conn, :index, @conn.assigns.locale, @transaction)
) %> ) %>
<%= link( <%= link(
gettext("Logs"), gettext("Logs"),

@ -4,12 +4,23 @@
<div class="card"> <div class="card">
<div class="card-header"> <div class="card-header">
<ul class="nav nav-tabs card-header-tabs">
<!-- DESKTOP TAB NAV -->
<ul class="nav nav-tabs card-header-tabs d-none d-md-inline-flex">
<li class="nav-item">
<%= if @show_token_transfers do %>
<%= link(
gettext("Token Transfers"),
class: "nav-link",
to: transaction_token_transfer_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
<% end %>
</li>
<li class="nav-item"> <li class="nav-item">
<%= link( <%= link(
gettext("Internal Transactions"), gettext("Internal Transactions"),
class: "nav-link", class: "nav-link",
to: transaction_path(@conn, :show, @conn.assigns.locale, @transaction) to: transaction_internal_transaction_path(@conn, :index, @conn.assigns.locale, @transaction)
) %> ) %>
</li> </li>
<li class="nav-item"> <li class="nav-item">
@ -20,7 +31,36 @@
) %> ) %>
</li> </li>
</ul> </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("Logs") %></a>
<div class="dropdown-menu">
<%= if @show_token_transfers do %>
<%= link(
gettext("Token Transfers"),
class: "dropdown-item",
to: transaction_token_transfer_path(@conn, :index, @conn.assigns.locale, @transaction),
"data-test": "transaction_token_transfer_link"
) %>
<% end %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
to: transaction_path(@conn, :show, @conn.assigns.locale, @transaction)
) %>
<%= link(
gettext("Logs"),
class: "dropdown-item",
to: transaction_log_path(@conn, :index, @conn.assigns.locale, @transaction),
"data-test": "transaction_logs_link"
) %>
</div> </div>
</li>
</ul>
</div>
<div class="card-body"> <div class="card-body">
<h2 class="card-title"><%= gettext "Logs" %></h2> <h2 class="card-title"><%= gettext "Logs" %></h2>
<%= if Enum.count(@logs) > 0 do %> <%= if Enum.count(@logs) > 0 do %>

@ -0,0 +1,21 @@
<div class="tile tile-type-token fade-in">
<div class="row justify-content-end">
<div class="col-md-3 col-lg-2 d-flex align-items-center justify-content-start justify-content-lg-center tile-label">
<%= gettext("Token Transfer") %>
</div>
<div class="col-md-9 col-lg-10 d-flex flex-column text-nowrap">
<%= render BlockScoutWeb.TransactionView, "_link.html", locale: @locale, transaction_hash: @token_transfer.transaction_hash %>
<span class="text-nowrap">
<%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @token_transfer.from_address_hash, contract: BlockScoutWeb.AddressView.contract?(@token_transfer.from_address), locale: @locale %>
&rarr;
<%= render BlockScoutWeb.AddressView, "_link.html", address_hash: @token_transfer.to_address_hash, contract: BlockScoutWeb.AddressView.contract?(@token_transfer.to_address), locale: @locale %>
</span>
<span class="tile-title text-truncate">
<%= token_transfer_amount(@token_transfer) %>
<%= link(token_symbol(@token_transfer.token), to: token_path(@conn, :show, @locale, @token_transfer.token.contract_address_hash)) %>
</span>
</div>
</div>
</div>

@ -0,0 +1,83 @@
<section class="container">
<%= render BlockScoutWeb.TransactionView, "overview.html", assigns %>
<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 active",
to: transaction_token_transfer_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Internal Transactions"),
class: "nav-link",
to: transaction_internal_transaction_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
</li>
<li class="nav-item">
<%= link(
gettext("Logs"),
class: "nav-link",
to: transaction_log_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
</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 Transfers") %></a>
<div class="dropdown-menu">
<%= link(
gettext("Token Transfers"),
class: "dropdown-item",
to: transaction_token_transfer_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
<%= link(
gettext("Internal Transactions"),
class: "dropdown-item",
to: transaction_internal_transaction_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
<%= link(
gettext("Logs"),
class: "dropdown-item",
to: transaction_log_path(@conn, :index, @conn.assigns.locale, @transaction)
) %>
</div>
</li>
</ul>
</div>
<div class="card-body">
<h2 class="card-title"><%= gettext "Token Transfers" %></h2>
<%= if Enum.any?(@token_transfers) do %>
<%= for token_transfer <- @token_transfers do %>
<%= render "_token_transfer.html", locale: @locale, token_transfer: token_transfer, conn: @conn %>
<% end %>
<% else %>
<div class="tile tile-muted text-center">
<span><%= gettext "There are no token transfers for this transaction." %></span>
</div>
<% end %>
</div>
</div>
<%= if @next_page_params do %>
<%= link(
gettext("Older"),
class: "button button--secondary button--sm u-float-left mt-3",
to: transaction_token_transfer_path(
@conn,
:index,
@conn.assigns.locale,
@transaction,
@next_page_params
)
) %>
<% end %>
</section>

@ -6,6 +6,8 @@ defmodule BlockScoutWeb.TokenHelpers do
alias Explorer.Chain.{Token, TokenTransfer} alias Explorer.Chain.{Token, TokenTransfer}
alias BlockScoutWeb.{CurrencyHelpers} alias BlockScoutWeb.{CurrencyHelpers}
import BlockScoutWeb.Gettext
@doc """ @doc """
Returns the token transfers' amount according to the token's type and decimails. Returns the token transfers' amount according to the token's type and decimails.
@ -33,7 +35,7 @@ defmodule BlockScoutWeb.TokenHelpers do
end end
defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, token_id) do defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, token_id) do
"TokenID [#{token_id}]" gettext("ERC-721 TokenID [%{token_id}]", token_id: token_id)
end end
defp do_token_transfer_amount(_token, _amount, _token_id) do defp do_token_transfer_amount(_token, _amount, _token_id) do

@ -0,0 +1,3 @@
defmodule BlockScoutWeb.TransactionTokenTransferView do
use BlockScoutWeb, :view
end

@ -212,10 +212,14 @@ msgid "Last Seen"
msgstr "" msgstr ""
#: #:
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:18 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:27
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:37 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:54
#: lib/block_scout_web/templates/transaction_log/index.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:28
#: lib/block_scout_web/templates/transaction_log/index.html.eex:25 #: lib/block_scout_web/templates/transaction_log/index.html.eex:38
#: lib/block_scout_web/templates/transaction_log/index.html.eex:54
#: lib/block_scout_web/templates/transaction_log/index.html.eex:65
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:25
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:48
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -306,11 +310,14 @@ msgstr ""
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_transaction/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:20
#: lib/block_scout_web/templates/address_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_transaction/index.html.eex:60
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:20
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:29 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:38
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:49
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:47 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:64
#: lib/block_scout_web/templates/transaction_log/index.html.eex:10 #: lib/block_scout_web/templates/transaction_log/index.html.eex:21
#: lib/block_scout_web/templates/transaction_log/index.html.eex:49
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:18
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:43
msgid "Internal Transactions" msgid "Internal Transactions"
msgstr "" msgstr ""
@ -476,6 +483,7 @@ msgstr ""
#: 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/token/show.html.eex:102 #: lib/block_scout_web/templates/token/show.html.eex:102
#: 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
msgid "Older" msgid "Older"
msgstr "" msgstr ""
@ -510,8 +518,8 @@ msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:61 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:78
#: lib/block_scout_web/templates/transaction_log/index.html.eex:82 #: lib/block_scout_web/templates/transaction_log/index.html.eex:122
msgid "Newer" msgid "Newer"
msgstr "" msgstr ""
@ -700,12 +708,12 @@ msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:54 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:71
msgid "There are no internal transactions for this transaction." msgid "There are no internal transactions for this transaction."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction_log/index.html.eex:77 #: lib/block_scout_web/templates/transaction_log/index.html.eex:117
msgid "There are no logs for this transaction." msgid "There are no logs for this transaction."
msgstr "" msgstr ""
@ -716,6 +724,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/token/_token_transfer.html.eex:4 #: lib/block_scout_web/templates/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:116 #: lib/block_scout_web/views/transaction_view.ex:116
msgid "Token Transfer" msgid "Token Transfer"
msgstr "" msgstr ""
@ -764,6 +773,14 @@ msgstr ""
#: lib/block_scout_web/templates/token/show.html.eex:73 #: lib/block_scout_web/templates/token/show.html.eex:73
#: lib/block_scout_web/templates/token/show.html.eex:76 #: lib/block_scout_web/templates/token/show.html.eex:76
#: lib/block_scout_web/templates/token/show.html.eex:86 #: lib/block_scout_web/templates/token/show.html.eex:86
#: 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
#: lib/block_scout_web/templates/transaction_log/index.html.eex:42
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:11
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:35
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:38
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:58
msgid "Token Transfers" msgid "Token Transfers"
msgstr "" msgstr ""
@ -786,3 +803,13 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:23 #: lib/block_scout_web/templates/layout/_topnav.html.eex:23
msgid "API" msgid "API"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/views/token_helpers.ex:38
msgid "ERC-721 TokenID [%{token_id}]"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:65
msgid "There are no token transfers for this transaction."
msgstr ""

@ -224,10 +224,14 @@ msgid "Last Seen"
msgstr "" msgstr ""
#: #:
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:18 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:27
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:37 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:54
#: lib/block_scout_web/templates/transaction_log/index.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:28
#: lib/block_scout_web/templates/transaction_log/index.html.eex:25 #: lib/block_scout_web/templates/transaction_log/index.html.eex:38
#: lib/block_scout_web/templates/transaction_log/index.html.eex:54
#: lib/block_scout_web/templates/transaction_log/index.html.eex:65
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:25
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:48
msgid "Logs" msgid "Logs"
msgstr "" msgstr ""
@ -318,11 +322,14 @@ msgstr ""
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_transaction/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:20
#: lib/block_scout_web/templates/address_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_transaction/index.html.eex:60
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:11 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:20
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:29 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:38
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:32 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:49
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:47 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:64
#: lib/block_scout_web/templates/transaction_log/index.html.eex:10 #: lib/block_scout_web/templates/transaction_log/index.html.eex:21
#: lib/block_scout_web/templates/transaction_log/index.html.eex:49
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:18
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:43
msgid "Internal Transactions" msgid "Internal Transactions"
msgstr "" msgstr ""
@ -488,6 +495,7 @@ msgstr ""
#: 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/token/show.html.eex:102 #: lib/block_scout_web/templates/token/show.html.eex:102
#: 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
msgid "Older" msgid "Older"
msgstr "" msgstr ""
@ -522,8 +530,8 @@ msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:61 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:78
#: lib/block_scout_web/templates/transaction_log/index.html.eex:82 #: lib/block_scout_web/templates/transaction_log/index.html.eex:122
msgid "Newer" msgid "Newer"
msgstr "" msgstr ""
@ -712,12 +720,12 @@ msgstr ""
#, elixir-format #, elixir-format
#: #:
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:54 #: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:71
msgid "There are no internal transactions for this transaction." msgid "There are no internal transactions for this transaction."
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/transaction_log/index.html.eex:77 #: lib/block_scout_web/templates/transaction_log/index.html.eex:117
msgid "There are no logs for this transaction." msgid "There are no logs for this transaction."
msgstr "" msgstr ""
@ -728,6 +736,7 @@ msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/token/_token_transfer.html.eex:4 #: lib/block_scout_web/templates/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:116 #: lib/block_scout_web/views/transaction_view.ex:116
msgid "Token Transfer" msgid "Token Transfer"
msgstr "" msgstr ""
@ -776,6 +785,14 @@ msgstr ""
#: lib/block_scout_web/templates/token/show.html.eex:73 #: lib/block_scout_web/templates/token/show.html.eex:73
#: lib/block_scout_web/templates/token/show.html.eex:76 #: lib/block_scout_web/templates/token/show.html.eex:76
#: lib/block_scout_web/templates/token/show.html.eex:86 #: lib/block_scout_web/templates/token/show.html.eex:86
#: 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
#: lib/block_scout_web/templates/transaction_log/index.html.eex:42
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:11
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:35
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:38
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:58
msgid "Token Transfers" msgid "Token Transfers"
msgstr "" msgstr ""
@ -798,3 +815,13 @@ msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:23 #: lib/block_scout_web/templates/layout/_topnav.html.eex:23
msgid "API" msgid "API"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/views/token_helpers.ex:38
msgid "ERC-721 TokenID [%{token_id}]"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:65
msgid "There are no token transfers for this transaction."
msgstr ""

@ -2,7 +2,8 @@ defmodule BlockScoutWeb.TransactionControllerTest do
use BlockScoutWeb.ConnCase use BlockScoutWeb.ConnCase
alias Explorer.Chain.{Block, Transaction} alias Explorer.Chain.{Block, Transaction}
import BlockScoutWeb.Router.Helpers, only: [transaction_path: 4, transaction_internal_transaction_path: 4] import BlockScoutWeb.Router.Helpers,
only: [transaction_path: 4, transaction_internal_transaction_path: 4, transaction_token_transfer_path: 4]
describe "GET index/2" do describe "GET index/2" do
test "returns a collated transactions", %{conn: conn} do test "returns a collated transactions", %{conn: conn} do
@ -98,12 +99,24 @@ defmodule BlockScoutWeb.TransactionControllerTest do
end end
describe "GET show/3" do describe "GET show/3" do
test "redirects to transactions/:transaction_id/internal_transactions", %{conn: conn} do test "redirects to transactions/:transaction_id/token_transfers when there are token transfers", %{conn: conn} do
locale = "en" locale = "en"
hash = "0x9" transaction = insert(:transaction)
conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, locale, hash)) insert(:token_transfer, transaction: transaction)
conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, locale, transaction))
assert redirected_to(conn) =~ transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, locale, hash) assert redirected_to(conn) =~ transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, locale, transaction)
end
test "redirects to transactions/:transaction_id/internal_transactions when there are no token transfers", %{
conn: conn
} do
locale = "en"
transaction = insert(:transaction)
conn = get(conn, transaction_path(BlockScoutWeb.Endpoint, :show, locale, transaction))
assert redirected_to(conn) =~
transaction_internal_transaction_path(BlockScoutWeb.Endpoint, :index, locale, transaction)
end end
end end
end end

@ -0,0 +1,140 @@
defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do
use BlockScoutWeb.ConnCase
import BlockScoutWeb.Router.Helpers, only: [transaction_token_transfer_path: 4]
alias Explorer.ExchangeRates.Token
describe "GET index/3" do
test "load token transfers", %{conn: conn} do
transaction = insert(:transaction)
token_transfer = insert(:token_transfer, transaction: transaction)
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, transaction.hash))
assert List.first(conn.assigns.transaction.token_transfers).id == token_transfer.id
end
test "with missing transaction", %{conn: conn} do
hash = transaction_hash()
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, hash))
assert html_response(conn, 404)
end
test "with invalid transaction hash", %{conn: conn} do
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, "nope"))
assert html_response(conn, 404)
end
test "includes transaction data", %{conn: conn} do
block = insert(:block, %{number: 777})
transaction =
:transaction
|> insert()
|> with_block(block)
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, transaction.hash))
assert html_response(conn, 200)
assert conn.assigns.transaction.hash == transaction.hash
end
test "includes token transfers for the transaction", %{conn: conn} do
transaction = insert(:transaction)
expected_token_transfer = insert(:token_transfer, transaction: transaction)
insert(:token_transfer, transaction: transaction)
path = transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, transaction.hash)
conn = get(conn, path)
actual_token_transfer_ids =
conn.assigns.token_transfers
|> Enum.map(fn it -> it.id end)
assert html_response(conn, 200)
assert Enum.member?(actual_token_transfer_ids, expected_token_transfer.id)
end
test "includes USD exchange rate value for address in assigns", %{conn: conn} do
transaction = insert(:transaction)
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, transaction.hash))
assert %Token{} = conn.assigns.exchange_rate
end
test "returns next page of results based on last seen token transfer", %{conn: conn} do
transaction =
:transaction
|> insert()
|> with_block()
{:ok, first_transfer_time} = NaiveDateTime.new(2000, 1, 1, 0, 0, 5)
{:ok, remaining_transfers_time} = NaiveDateTime.new(1999, 1, 1, 0, 0, 0)
insert(:token_transfer, transaction: transaction, inserted_at: first_transfer_time)
1..5
|> Enum.each(fn log_index ->
insert(:token_transfer, transaction: transaction, inserted_at: remaining_transfers_time, log_index: log_index)
end)
conn =
get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, transaction.hash), %{
"inserted_at" => first_transfer_time |> DateTime.from_naive!("Etc/UTC") |> DateTime.to_iso8601()
})
actual_times =
conn.assigns.token_transfers
|> Enum.map(& &1.inserted_at)
refute Enum.any?(actual_times, fn time -> first_transfer_time == time end)
end
test "next_page_params exist if not on last page", %{conn: conn} do
transaction =
:transaction
|> insert()
|> with_block()
1..51
|> Enum.map(fn log_index ->
insert(
:token_transfer,
transaction: transaction,
log_index: log_index
)
end)
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, transaction.hash))
assert Enum.any?(conn.assigns.next_page_params)
end
test "next_page_params are empty if on last page", %{conn: conn} do
transaction =
:transaction
|> insert()
|> with_block()
1..2
|> Enum.map(fn log_index ->
insert(
:token_transfer,
transaction: transaction,
log_index: log_index
)
end)
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, :en, transaction.hash))
assert is_nil(conn.assigns.next_page_params)
end
end
end

@ -29,7 +29,7 @@ defmodule BlockScoutWeb.TokenHelpersTest do
token = build(:token, type: "ERC-721", decimals: nil) token = build(:token, type: "ERC-721", decimals: nil)
token_transfer = build(:token_transfer, token: token, amount: nil, token_id: 1) token_transfer = build(:token_transfer, token: token, amount: nil, token_id: 1)
assert TokenHelpers.token_transfer_amount(token_transfer) == "TokenID [1]" assert TokenHelpers.token_transfer_amount(token_transfer) == "ERC-721 TokenID [1]"
end end
test "returns nothing for unknow token's type" do test "returns nothing for unknow token's type" do

@ -1211,7 +1211,7 @@ defmodule Explorer.Chain do
`:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association, `:required`, and the `t:Explorer.Chain.InternalTransaction.t/0` has no associated record for that association,
then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the list. then the `t:Explorer.Chain.InternalTransaction.t/0` will not be included in the list.
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
`:key` (a tuple of the lowest/oldest `{index}`) and. Results will be the internal transactions older than `:key` (a tuple of the lowest/oldest `{index}`). Results will be the internal transactions older than
the `index` that is passed. the `index` that is passed.
""" """
@ -1246,7 +1246,7 @@ defmodule Explorer.Chain do
`:required`, and the `t:Explorer.Chain.Log.t/0` has no associated record for that association, then the `:required`, and the `t:Explorer.Chain.Log.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Log.t/0` will not be included in the page `entries`. `t:Explorer.Chain.Log.t/0` will not be included in the page `entries`.
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and * `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
`:key` (a tuple of the lowest/oldest `{index}`) and. Results will be the transactions older than `:key` (a tuple of the lowest/oldest `{index}`). Results will be the transactions older than
the `index` that are passed. the `index` that are passed.
""" """
@ -1269,6 +1269,40 @@ defmodule Explorer.Chain do
|> Repo.all() |> Repo.all()
end end
@doc """
Finds all `t:Explorer.Chain.TokenTransfer.t/0`s for `t:Explorer.Chain.Transaction.t/0`.
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.TokenTransfer.t/0` has no associated record for that association, then the
`t:Explorer.Chain.TokenTransfer.t/0` will not be included in the page `entries`.
* `:paging_options` - a `t:Explorer.PagingOptions.t/0` used to specify the `:page_size` and
`:key` (in the form of `%{"inserted_at" => inserted_at}`). Results will be the transactions older than
the `index` that are passed.
"""
@spec transaction_to_token_transfers(Transaction.t(), [paging_options | necessity_by_association_option]) :: [
TokenTransfer.t()
]
def transaction_to_token_transfers(
%Transaction{hash: %Hash{byte_count: unquote(Hash.Full.byte_count())} = transaction_hash},
options \\ []
)
when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
TokenTransfer
|> join(:inner, [token_transfer], transaction in assoc(token_transfer, :transaction))
|> where([_, transaction], transaction.hash == ^transaction_hash)
|> TokenTransfer.page_token_transfer(paging_options)
|> limit(^paging_options.page_size)
|> order_by([token_transfer], asc: token_transfer.inserted_at)
|> join_associations(necessity_by_association)
|> Repo.all()
end
@doc """ @doc """
Converts `transaction` to the status of the `t:Explorer.Chain.Transaction.t/0` whether pending or collated. Converts `transaction` to the status of the `t:Explorer.Chain.Transaction.t/0` whether pending or collated.
@ -1516,4 +1550,11 @@ defmodule Explorer.Chain do
def count_addresses_in_token_transfers_from_token_hash(token_address_hash) do 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) TokenTransfer.count_addresses_in_token_transfers_from_token_hash(token_address_hash)
end 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)
Repo.one(query) != nil
end
end end

@ -167,9 +167,9 @@ defmodule Explorer.Chain.TokenTransfer do
result result
end end
defp page_token_transfer(query, %PagingOptions{key: nil}), do: query def page_token_transfer(query, %PagingOptions{key: nil}), do: query
defp page_token_transfer(query, %PagingOptions{key: inserted_at}) do def page_token_transfer(query, %PagingOptions{key: inserted_at}) do
where( where(
query, query,
[token_transfer], [token_transfer],

@ -11,6 +11,7 @@ defmodule Explorer.ChainTest do
InternalTransaction, InternalTransaction,
Log, Log,
Token, Token,
TokenTransfer,
Transaction, Transaction,
SmartContract, SmartContract,
Wei Wei
@ -933,6 +934,50 @@ defmodule Explorer.ChainTest do
end end
end end
describe "transaction_to_token_transfers/2" do
test "without token transfers" do
transaction = insert(:transaction)
assert [] = Chain.transaction_to_token_transfers(transaction)
end
test "with token transfers" do
transaction =
:transaction
|> insert()
|> with_block()
%TokenTransfer{id: id} = insert(:token_transfer, transaction: transaction)
assert [%TokenTransfer{id: ^id}] = Chain.transaction_to_token_transfers(transaction)
end
test "token transfers necessity_by_association loads associations" do
transaction =
:transaction
|> insert()
|> with_block()
insert(:token_transfer, transaction: transaction)
assert [%TokenTransfer{token: %Token{}, transaction: %Transaction{}}] =
Chain.transaction_to_token_transfers(
transaction,
necessity_by_association: %{
token: :optional,
transaction: :optional
}
)
assert [
%TokenTransfer{
token: %Ecto.Association.NotLoaded{},
transaction: %Ecto.Association.NotLoaded{}
}
] = Chain.transaction_to_token_transfers(transaction)
end
end
describe "value/2" do describe "value/2" do
test "with InternalTransaction.t with :wei" do test "with InternalTransaction.t with :wei" do
assert Chain.value(%InternalTransaction{value: %Wei{value: Decimal.new(1)}}, :wei) == Decimal.new(1) assert Chain.value(%InternalTransaction{value: %Wei{value: Decimal.new(1)}}, :wei) == Decimal.new(1)
@ -1544,4 +1589,19 @@ defmodule Explorer.ChainTest do
%Token{contract_address_hash: uncatalog_address} = insert(:token, cataloged: false) %Token{contract_address_hash: uncatalog_address} = insert(:token, cataloged: false)
assert Chain.stream_uncataloged_token_contract_address_hashes([], &[&1 | &2]) == {:ok, [uncatalog_address]} assert Chain.stream_uncataloged_token_contract_address_hashes([], &[&1 | &2]) == {:ok, [uncatalog_address]}
end end
describe "transaction_has_token_transfers?/1" do
test "returns true if transaction has token transfers" do
transaction = insert(:transaction)
insert(:token_transfer, transaction: transaction)
assert Chain.transaction_has_token_transfers?(transaction.hash) == true
end
test "returns false if transaction has no token transfers" do
transaction = insert(:transaction)
assert Chain.transaction_has_token_transfers?(transaction.hash) == false
end
end
end end

@ -331,11 +331,14 @@ defmodule Explorer.Factory do
from_address = build(:address, hash: from_address_hash) from_address = build(:address, hash: from_address_hash)
contract_code = Map.fetch!(contract_code_info(), :bytecode) contract_code = Map.fetch!(contract_code_info(), :bytecode)
token_address = insert(:contract_address, contract_code: contract_code)
insert(:token, contract_address: token_address)
%TokenTransfer{ %TokenTransfer{
amount: Decimal.new(1), amount: Decimal.new(1),
from_address: from_address, from_address: from_address,
to_address: to_address, to_address: to_address,
token_contract_address: build(:address, contract_code: contract_code), token_contract_address: token_address,
transaction: log.transaction, transaction: log.transaction,
log_index: log.index log_index: log.index
} }

Loading…
Cancel
Save