Merge branch 'ab-erc-721-coin-instance-page' of github.com:poanetwork/blockscout

pull/2760/head
YegorSan 5 years ago
commit 8c3605dd28
  1. 1
      CHANGELOG.md
  2. 2
      apps/block_scout_web/assets/css/app.scss
  3. 6
      apps/block_scout_web/assets/css/components/_btn_contract.scss
  4. 10
      apps/block_scout_web/assets/css/components/_erc721_token_image_container.scss
  5. 2
      apps/block_scout_web/assets/css/theme/_dark-theme.scss
  6. 7
      apps/block_scout_web/assets/static/images/controller.svg
  7. 1
      apps/block_scout_web/config/config.exs
  8. 2
      apps/block_scout_web/config/prod.exs
  9. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  10. 3
      apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
  11. 3
      apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
  12. 2
      apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
  13. 34
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex
  14. 74
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex
  15. 20
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex
  16. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex
  17. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex
  18. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  19. 2
      apps/block_scout_web/lib/block_scout_web/csp_header.ex
  20. 30
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex
  21. 106
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex
  22. 15
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex
  23. 39
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex
  24. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex
  25. 28
      apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex
  26. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
  27. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex
  28. 7
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  29. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex
  30. 2
      apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex
  31. 10
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  32. 9
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex
  33. 62
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
  34. 5
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex
  35. 3
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex
  36. 21
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  37. 143
      apps/block_scout_web/priv/gettext/default.pot
  38. 143
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  39. 23
      apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs
  40. 8
      apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs
  41. 169
      apps/explorer/lib/explorer/chain.ex
  42. 46
      apps/explorer/lib/explorer/chain/token/instance.ex
  43. 40
      apps/explorer/lib/explorer/chain/token_transfer.ex
  44. 69
      apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
  45. 22
      apps/explorer/priv/repo/migrations/20190905083522_create_token_instances.exs
  46. 131
      apps/explorer/test/explorer/chain_test.exs
  47. 53
      apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs
  48. 9
      apps/explorer/test/support/factory.ex
  49. 75
      apps/indexer/lib/indexer/fetcher/token_instance.ex
  50. 3
      apps/indexer/lib/indexer/supervisor.ex
  51. 1
      docs/env-variables.md

@ -1,6 +1,7 @@
## Current
### Features
- [#2642](https://github.com/poanetwork/blockscout/pull/2642) - add ERC721 coin instance page
- [#2717](https://github.com/poanetwork/blockscout/pull/2717) - Improve speed of nonconsensus data removal
- [#2679](https://github.com/poanetwork/blockscout/pull/2679) - added fixed height for card chain blocks and card chain transactions
- [#2678](https://github.com/poanetwork/blockscout/pull/2678) - fixed dashboard banner height bug

@ -100,6 +100,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "components/form";
@import "components/btn_copy";
@import "components/btn_qr";
@import "components/btn_contract";
@import "components/btn_address_card";
@import "components/btn_dropdown_line";
@import "components/transaction";
@ -114,6 +115,7 @@ $fa-font-path: "~@fortawesome/fontawesome-free/webfonts";
@import "components/radio_big";
@import "components/btn_no_border";
@import "components/custom_tooltips_block_details";
@import "components/_erc721_token_image_container";
@import "theme/dark-theme";

@ -0,0 +1,6 @@
$btn-contract-color: $primary !default;
$btn-contract-dimensions: 31px !default;
.btn-contract-icon {
@include square-icon-button($btn-contract-color, $btn-contract-dimensions);
}

@ -0,0 +1,10 @@
/* ERC721 image block */
.erc721-image {
display: flex;
justify-content: center;
}
.erc721-image img {
height: 150px;
}
/* ERC721 image block end */

@ -234,7 +234,7 @@ $labels-dark: #8a8dba; // header nav, labels
}
}
.btn-copy-icon, .btn-qr-icon, .btn-address-card-icon {
.btn-copy-icon, .btn-qr-icon, .btn-address-card-icon .btn-contract-icon {
border-color: $dark-primary;
path {
fill: $dark-primary;

@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="197" height="124">
<path fill="#E0CEFF" fill-rule="evenodd" d="M33.859 20.795l.77-7.702 3.848-6.161 13.082-3.081 11.543-.77 10.773 3.851 3.848 3.08 5.386 2.311h20.008l8.465-.77 8.465-.77 6.156-5.392 12.313-2.31 13.082 3.08 9.234 3.851 1.539 10.013-46.941 13.093-81.571-12.323z"/>
<path fill="#AF81FF" fill-rule="evenodd" d="M23.855 121.689l-6.925-.77-8.465-5.391-4.617-8.472-1.539-10.013 23.086-67.006 4.617-7.702 14.621-4.621 15.39-.77 10.774 3.851 7.695 8.472 31.551.77 8.465-1.54 11.543-10.783h13.082l19.238 2.311 9.234 8.472 4.618 13.093 15.39 46.211 2.309 9.242-3.848 16.945-7.695 6.161-6.926 1.54h-8.465l-11.543-7.701-15.39-15.404-7.696-3.851-11.543-2.311H66.949l-15.39 12.323-15.391 13.864-12.313 3.08z"/>
<path fill="#884DEF" fill-rule="evenodd" d="M24.625 121.689l18.469-36.198 6.926-13.093 7.695-5.392 82.34.77 6.156 3.851 24.625 47.752-2.309 1.54-12.312-5.391-21.547-20.795-28.473-3.081-40.785 2.311-22.316 17.714-18.469 10.012z"/>
<path fill="#5AE9AB" fill-rule="evenodd" d="M120.047 73.938a6.93 6.93 0 0 1-6.926-6.932 6.929 6.929 0 0 1 6.926-6.931 6.929 6.929 0 0 1 6.926 6.931 6.93 6.93 0 0 1-6.926 6.932zm-43.094 0a6.93 6.93 0 0 1-6.926-6.932 6.929 6.929 0 0 1 6.926-6.931 6.929 6.929 0 0 1 6.926 6.931 6.93 6.93 0 0 1-6.926 6.932zM46.172 52.373c-5.525 0-10.004-4.483-10.004-10.013 0-5.529 4.479-10.012 10.004-10.012 5.525 0 10.004 4.483 10.004 10.012 0 5.53-4.479 10.013-10.004 10.013z"/>
<path fill-rule="evenodd" d="M173.145 124c-12.89 0-35.399-23.876-35.399-23.876s-5.506-6.161-12.312-6.161H98.5 71.566c-6.806 0-12.312 6.161-12.312 6.161S36.745 124 23.855 124C6.652 124 0 106.654 0 100.894c0-5.759 22.316-74.708 22.316-74.708s3.42-8.472 8.465-8.472c0-10.09 10.774-15.403 10.774-15.403S46.616 0 60.023 0c13.408 0 20.008 7.702 20.008 7.702h36.938S123.569 0 136.977 0c13.407 0 18.468 2.311 18.468 2.311s10.774 5.313 10.774 15.403c5.045 0 8.465 8.472 8.465 8.472S197 95.135 197 100.894c0 5.76-6.652 23.106-23.855 23.106zM136.207 5.391c-9.332 0-17.699 8.472-17.699 8.472H78.492s-8.367-8.472-17.699-8.472c-23.795 0-24.361 9.146-23.856 10.013.506.867 19.053-3.851 23.856-3.851 10.209 0 17.799 14.633 22.316 14.633h30.782c4.517 0 12.107-14.633 22.316-14.633 4.803 0 23.35 4.718 23.856 3.851.505-.867-.061-10.013-23.856-10.013zm33.09 23.876s-2.081-5.304-6.156-7.702c-4.076-2.398-26.934-3.851-26.934-3.851l-4.617 1.541-10.004 10.012s-4.791 3.081-7.695 3.081H98.5 83.109c-2.904 0-7.695-3.081-7.695-3.081L65.41 19.255l-4.617-1.541s-22.858 1.453-26.934 3.851c-4.075 2.398-6.156 7.702-6.156 7.702S5.387 95.941 5.387 100.124c0 1.078 2.684 17.715 18.468 17.715.022 0 13.054-30.708 16.93-33.889 3.876-3.18 4.617 2.311 4.617 2.311s-15.455 30.037-14.621 30.037c5.714-1.7 30.012-24.646 30.012-24.646s4.849-3.851 8.465-3.851h58.484c3.616 0 8.465 3.851 8.465 3.851s24.298 22.946 30.012 24.646c.72 0-10.702-22.389-13.832-28.499l-.02.002-6.926-12.323s-4.293-6.161-9.234-6.161h-5.045c-1.349 4.882-5.808 8.472-11.115 8.472s-9.767-3.59-11.115-8.472H88.068c-1.348 4.882-5.808 8.472-11.115 8.472s-9.766-3.59-11.115-8.472h-8.123c-2.289 0-6.156 5.391-6.156 5.391s-6.084 2.97-4.618-3.081c.793-1.069 4.443-8.472 10.774-8.472h8.123c1.349-4.882 5.808-8.472 11.115-8.472s9.767 3.59 11.115 8.472h20.864c1.348-4.882 5.808-8.472 11.115-8.472s9.766 3.59 11.115 8.472h5.815c9.845 0 14.621 11.553 14.621 11.553l6.925 12.323-.029.004c5.204 8.596 14.632 30.804 14.651 30.804 15.784 0 18.468-16.637 18.468-17.715 0-4.183-22.316-70.857-22.316-70.857zm-43.863 36.969a5.389 5.389 0 0 0-5.387-5.391 5.389 5.389 0 0 0-5.387 5.391 5.389 5.389 0 0 0 5.387 5.391 5.389 5.389 0 0 0 5.387-5.391zm-43.094 0a5.389 5.389 0 0 0-5.387-5.391 5.389 5.389 0 0 0-5.387 5.391 5.389 5.389 0 0 0 5.387 5.391 5.389 5.389 0 0 0 5.387-5.391zm77.723-20.795h-8.465v8.472a3.08 3.08 0 0 1-3.078 3.081 3.08 3.08 0 0 1-3.079-3.081v-8.472h-8.464a3.08 3.08 0 0 1 0-6.161h8.464v-8.473a3.079 3.079 0 0 1 6.157 0v8.473h8.465c1.7 0 3.078 1.379 3.078 3.08a3.08 3.08 0 0 1-3.078 3.081zM46.172 56.994c-8.5 0-15.391-6.897-15.391-15.404s6.891-15.404 15.391-15.404 15.391 6.897 15.391 15.404-6.891 15.404-15.391 15.404zm0-24.646c-5.1 0-9.235 4.138-9.235 9.242 0 5.104 4.135 9.242 9.235 9.242s9.234-4.138 9.234-9.242c0-5.104-4.134-9.242-9.234-9.242z"/>
</svg>

After

Width:  |  Height:  |  Size: 4.2 KiB

@ -38,6 +38,7 @@ config :block_scout_web, BlockScoutWeb.Counters.BlocksIndexedCounter, enabled: t
config :block_scout_web, BlockScoutWeb.Endpoint,
instrumenters: [BlockScoutWeb.Prometheus.Instrumenter, SpandexPhoenix.Instrumenter],
url: [
scheme: System.get_env("BLOCKSCOUT_PROTOCOL") || "http",
host: System.get_env("BLOCKSCOUT_HOST") || "localhost",
path: System.get_env("NETWORK_PATH") || "/"
],

@ -20,7 +20,7 @@ config :block_scout_web, BlockScoutWeb.Endpoint,
check_origin: System.get_env("CHECK_ORIGIN") || false,
http: [port: System.get_env("PORT")],
url: [
scheme: "https",
scheme: System.get_env("BLOCKSCOUT_PROTOCOL") || "https",
port: System.get_env("PORT"),
host: System.get_env("BLOCKSCOUT_HOST") || "localhost",
path: System.get_env("NETWORK_PATH") || "/"

@ -62,6 +62,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
View.render_to_string(
TransactionView,
"_emission_reward_tile.html",
conn: conn,
current_address: address,
emission_funds: emission_reward,
validator: validator_reward
@ -71,6 +72,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
View.render_to_string(
TransactionView,
"_tile.html",
conn: conn,
current_address: address,
transaction: transaction
)

@ -50,7 +50,8 @@ defmodule BlockScoutWeb.BlockTransactionController do
View.render_to_string(
TransactionView,
"_tile.html",
transaction: transaction
transaction: transaction,
conn: conn
)
end)

@ -43,7 +43,8 @@ defmodule BlockScoutWeb.PendingTransactionController do
View.render_to_string(
TransactionView,
"_tile.html",
transaction: transaction
transaction: transaction,
conn: conn
)
end),
next_page_path: next_page_url

@ -23,7 +23,7 @@ defmodule BlockScoutWeb.RecentTransactionsController do
%{
transaction_hash: Hash.to_string(transaction.hash),
transaction_html:
View.render_to_string(BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction)
View.render_to_string(BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction, conn: conn)
}
end)

@ -0,0 +1,34 @@
defmodule BlockScoutWeb.Tokens.Instance.MetadataController do
use BlockScoutWeb, :controller
alias Explorer.{Chain, Market}
def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id}) do
options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}]
with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash),
{:ok, token} <- Chain.token_from_address_hash(hash, options),
{:ok, token_transfer} <-
Chain.erc721_token_instance_from_token_id_and_token_address(token_id, hash) do
if token_transfer.instance && token_transfer.instance.metadata do
render(
conn,
"index.html",
token_instance: token_transfer,
current_path: current_path(conn),
token: Market.add_price(token),
total_token_transfers: Chain.count_token_transfers_from_token_hash_and_token_id(hash, token_id)
)
else
not_found(conn)
end
else
_ ->
not_found(conn)
end
end
def index(conn, _) do
not_found(conn)
end
end

@ -0,0 +1,74 @@
defmodule BlockScoutWeb.Tokens.Instance.TransferController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.Tokens.TransferView
alias Explorer.{Chain, Market}
alias Phoenix.View
import BlockScoutWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3]
def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id, "type" => "JSON"} = params) do
with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash),
{:ok, token} <- Chain.token_from_address_hash(hash),
token_transfers <-
Chain.fetch_token_transfers_from_token_hash_and_token_id(hash, token_id, paging_options(params)) do
{token_transfers_paginated, next_page} = split_list_by_page(token_transfers)
next_page_path =
case next_page_params(next_page, token_transfers_paginated, params) do
nil ->
nil
next_page_params ->
token_instance_transfer_path(
conn,
:index,
token_id,
token.contract_address_hash,
Map.delete(next_page_params, "type")
)
end
transfers_json =
Enum.map(token_transfers_paginated, fn transfer ->
View.render_to_string(
TransferView,
"_token_transfer.html",
conn: conn,
token: token,
token_transfer: transfer
)
end)
json(conn, %{items: transfers_json, next_page_path: next_page_path})
else
_ ->
not_found(conn)
end
end
def index(conn, %{"token_id" => token_address_hash, "instance_id" => token_id}) do
options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}]
with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash),
{:ok, token} <- Chain.token_from_address_hash(hash, options),
{:ok, token_transfer} <-
Chain.erc721_token_instance_from_token_id_and_token_address(token_id, hash) do
render(
conn,
"index.html",
token_instance: token_transfer,
current_path: current_path(conn),
token: Market.add_price(token),
total_token_transfers: Chain.count_token_transfers_from_token_hash_and_token_id(hash, token_id)
)
else
_ ->
not_found(conn)
end
end
def index(conn, _) do
not_found(conn)
end
end

@ -0,0 +1,20 @@
defmodule BlockScoutWeb.Tokens.InstanceController do
use BlockScoutWeb, :controller
alias Explorer.Chain
def show(conn, %{"token_id" => token_address_hash, "id" => token_id}) do
with {:ok, hash} <- Chain.string_to_address_hash(token_address_hash),
:ok <- Chain.check_token_exists(hash),
:ok <- Chain.check_erc721_token_instance_exists(token_id, hash) do
redirect(conn, to: token_instance_transfer_path(conn, :index, token_address_hash, token_id))
else
_ ->
not_found(conn)
end
end
def show(conn, _) do
not_found(conn)
end
end

@ -40,7 +40,9 @@ defmodule BlockScoutWeb.Tokens.InventoryController do
View.render_to_string(
InventoryView,
"_token.html",
token_transfer: token_transfer
token_transfer: token_transfer,
token: token,
conn: conn
)
end)

@ -30,7 +30,7 @@ defmodule BlockScoutWeb.Tokens.TransferController do
"_token_transfer.html",
conn: conn,
token: token,
transfer: transfer
token_transfer: transfer
)
end)

@ -41,7 +41,8 @@ defmodule BlockScoutWeb.TransactionController do
View.render_to_string(
TransactionView,
"_tile.html",
transaction: transaction
transaction: transaction,
conn: conn
)
end),
next_page_path: next_page_path

@ -15,7 +15,7 @@ defmodule BlockScoutWeb.CSPHeader do
default-src 'self';\
script-src 'self' 'unsafe-inline' 'unsafe-eval';\
style-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.googleapis.com;\
img-src 'self' 'unsafe-inline' 'unsafe-eval' data:;\
img-src 'self' 'unsafe-inline' 'unsafe-eval' data: https:;\
font-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.gstatic.com data:;\
"
})

@ -0,0 +1,30 @@
<section class="container">
<%= render(
OverviewView,
"_details.html",
token: @token,
total_token_transfers: @total_token_transfers,
token_id: @token_instance.token_id,
token_instance: @token_instance,
conn: @conn
) %>
<section>
<div class="card">
<%= render OverviewView, "_tabs.html", assigns %>
<div class="card-body">
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Metadata" %></h3>
<button type="button" class="btn-line" id="button" data-clipboard-text="<%= format_metadata(@token_instance.instance.metadata) %>" aria-label="Copy Metadata">
<%= gettext "Copy Metadata" %>
</button>
</div>
<div class="tile tile-muted mb-4">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= format_metadata(@token_instance.instance.metadata) %></code>
</pre>
</div>
</section>
</div>
</section>
</section>

@ -0,0 +1,106 @@
<section class="address-overview">
<div class="row">
<div class="card-section col-md-12 col-lg-7 pr-0-md">
<div class="card">
<div class="card-body">
<h1 class="card-title">
<%= if token_name?(@token) do %>
<%= @token.name %>
<% else %>
<%= gettext("Token Details") %>
<% end %>
<!-- buttons -->
<span class="overview-title-buttons float-right">
<span class="overview-title-item">
<span
aria-label='<%= gettext("View Contract") %>'
class="btn-contract-icon"
data-placement="top"
data-toggle="tooltip"
title='<%= gettext("View Contract") %>'
onclick='<%= "location='#{address_path(@conn, :show, @token.contract_address_hash)}'" %>'
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.5 32.5" width="32" height="32" transform="translate(8,8)">
<path fill-rule="evenodd" d="M15 16H1a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h14a1 1 0 0 1 1 1v14a1 1 0 0 1-1 1zM14 2H2v12h12V2z"/>
<path fill-rule="evenodd" d="M11 9h-1a1 1 0 0 1 0-2h1a1 1 0 0 1 0 2zm0-3H5a1 1 0 0 1 0-2h6a1 1 0 0 1 0 2zM5 7h1a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2zm0 3h6a1 1 0 0 1 0 2H5a1 1 0 0 1 0-2z"/>
</svg>
</span>
</span>
<span class="overview-title-item" data-clipboard-text="<%= @token_id %>">
<span
aria-label='<%= gettext("Copy Token ID") %>'
class="btn-copy-icon"
data-placement="top"
data-toggle="tooltip"
title='<%= gettext("Copy Token ID") %>'
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.5 32.5" width="32" height="32">
<path fill-rule="evenodd" d="M23.5 20.5a1 1 0 0 1-1-1v-9h-9a1 1 0 0 1 0-2h10a1 1 0 0 1 1 1v10a1 1 0 0 1-1 1zm-3-7v10a1 1 0 0 1-1 1h-10a1 1 0 0 1-1-1v-10a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1zm-2 1h-8v8h8v-8z"/>
</svg>
</span>
</span>
<span
class="overview-title-item"
data-target="#qrModal"
data-toggle="modal"
>
<span
class="btn-qr-icon"
data-toggle="tooltip"
data-placement="top"
title='<%= gettext("QR Code") %>'
aria-label='<%= gettext("Show QR Code") %>'
>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32.5 32.5" width="32" height="32">
<path fill-rule="evenodd" d="M22.5 24.5v-2h2v2h-2zm-1-4v-1h1v1h-1zm1-3h2v2h-2v-2zm1-2h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1zm-1-5h-3v3h3v-3zm-8 14h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1zm-1-5h-3v3h3v-3zm1-4h-5a1 1 0 0 1-1-1v-5a1 1 0 0 1 1-1h5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1zm-1-5h-3v3h3v-3zm6 9h-2v-2h2v2zm1 1h-1v-1h1v1zm0 1v1h-1v-1h1zm-1 3h-2v-2h2v2z"/>
</svg>
</span>
</span>
</span>
</h1>
<h3><%= to_string(@token_id) %></h3>
<div class="d-flex flex-column flex-md-row justify-content-start text-muted">
<div class="d-flex flex-row justify-content-start text-muted">
<span class="mr-4"> <%= @token.type %> </span>
<span class="mr-4"><%= @total_token_transfers %> <%= gettext "Transfers" %></span>
<%= if decimals?(@token) do %>
<span class="mr-4"><%= @token.decimals %> <%= gettext "Decimals" %></span>
<% end %>
</div>
</div>
</div>
</div>
</div>
<div class="card-section col-md-12 col-lg-5 pl-0-md">
<div class="card">
<div class="card-body">
<div class="erc721-image" >
<img src=<%=image_src(@token_instance.instance)%> />
</div>
</div>
</div>
</div>
</div>
</section>
<!-- Modal -->
<div class="modal fade" id="qrModal" tabindex="-1" role="dialog" aria-labelledby="qrModalLabel" aria-hidden="true">
<div class="modal-dialog modal-sm" role="document">
<div class="modal-content">
<div class="modal-header">
<h2 class="modal-title" id="qrModalLabel"><%= gettext "QR Code" %></h2>
<button type="button" class="close" data-dismiss="modal" aria-label="<%= gettext("Close") %>">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<img src="data:image/png;base64, <%= qr_code(@conn, @token_id, @token.contract_address_hash) %> " class="qr-code" alt="qr_code" title="<%= @token.contract_address %>" />
</div>
<div class="modal-footer">
<button type="button" class="btn btn-primary" data-dismiss="modal"><%= gettext "Close" %></button>
</div>
</div>
</div>
</div>

@ -0,0 +1,15 @@
<div class="card-tabs js-card-tabs">
<%= link(
gettext("Token Transfers"),
class: "card-tab #{tab_status("token_transfers", @conn.request_path)}",
to: token_instance_path(@conn, :show, @token.contract_address_hash, to_string(@token_instance.token_id))
)
%>
<%= if @token_instance.instance do %>
<%= link(
gettext("Metadata"),
to: token_instance_metadata_path(@conn, :index, @token.contract_address_hash, to_string(@token_instance.token_id)),
class: "card-tab #{tab_status("metadata", @conn.request_path)}")
%>
<% end %>
</div>

@ -0,0 +1,39 @@
<section class="container">
<%= render(
OverviewView,
"_details.html",
token: @token,
total_token_transfers: @total_token_transfers,
token_id: @token_instance.token_id,
token_instance: @token_instance,
conn: @conn
) %>
<section>
<div class="card">
<%= render OverviewView, "_tabs.html", assigns %>
<div class="card-body" data-async-load data-async-listing="<%= @current_path %>">
<h2 class="card-title"><%= gettext "Token Transfers" %></h2>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
<button data-error-message class="alert alert-danger col-12 text-left" style="display: none;">
<span href="#" class="alert-link"><%= gettext("Something went wrong, click to reload.") %></span>
</button>
<div data-empty-response-message class="tile tile-muted text-center" style="display: none;">
<span data-selector="empty-transactions-list">
<%= gettext "There are no transfers for this Token." %>
</span>
</div>
<div data-items>
<%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %>
</div>
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %>
</div>
</div>
</section>
</section>

@ -10,7 +10,7 @@
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="mr-1"><%= gettext "Token ID" %>:</span>
<span class="tile-title">
<%= @token_transfer.token_id %>
<%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, @token.contract_address_hash, to_string(@token_transfer.token_id))) %>
</span>
</span>

@ -6,29 +6,35 @@
</div>
<!-- Content -->
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @transfer.transaction_hash %>
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @token_transfer.transaction_hash %>
<span class="text-nowrap">
<%= link to: address_token_transfers_path(@conn, :index, @transfer.from_address, @token.contract_address_hash), "data-test": "address_hash_link" do %>
<%= link to: address_token_transfers_path(@conn, :index, @token_transfer.from_address, @token.contract_address_hash), "data-test": "address_hash_link" do %>
<%= render(
BlockScoutWeb.AddressView,
"_responsive_hash.html",
address: @transfer.from_address,
contract: BlockScoutWeb.AddressView.contract?(@transfer.from_address)
address: @token_transfer.from_address,
contract: BlockScoutWeb.AddressView.contract?(@token_transfer.from_address)
) %>
<% end %>
&rarr;
<%= link to: address_token_transfers_path(@conn, :index, @transfer.to_address, @token.contract_address_hash), "data-test": "address_hash_link" do %>
<%= link to: address_token_transfers_path(@conn, :index, @token_transfer.to_address, @token.contract_address_hash), "data-test": "address_hash_link" do %>
<%= render(
BlockScoutWeb.AddressView,
"_responsive_hash.html",
address: @transfer.to_address,
contract: BlockScoutWeb.AddressView.contract?(@transfer.to_address)
address: @token_transfer.to_address,
contract: BlockScoutWeb.AddressView.contract?(@token_transfer.to_address)
) %>
<% end %>
</span>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="tile-title">
<%= token_transfer_amount(@transfer) %> <%= @transfer.token.symbol %>
<%= case token_transfer_amount(@token_transfer) do %>
<% {:ok, :erc721_instance} -> %>
<%= "TokenID ["%><%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, @token_transfer.token.contract_address_hash, to_string(@token_transfer.token_id))) %><%= "]" %>
<% {:ok, value} -> %>
<%= value %>
<% end %>
<%= @token_transfer.token.symbol %>
</span>
</span>
</div>
@ -36,11 +42,11 @@
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-center text-md-right mt-3 mt-md-0">
<span class="mr-2 mr-md-0 order-1">
<%= link(
gettext("Block #%{number}", number: @transfer.block_number),
to: block_path(BlockScoutWeb.Endpoint, :show, @transfer.block_number)
gettext("Block #%{number}", number: @token_transfer.block_number),
to: block_path(BlockScoutWeb.Endpoint, :show, @token_transfer.block_number)
) %>
</span>
<span class="mr-2 mr-md-0 order-2" data-from-now="<%= @transfer.transaction.block.timestamp %>"></span>
<span class="mr-2 mr-md-0 order-2" data-from-now="<%= @token_transfer.transaction.block.timestamp %>"></span>
</div>
</div>
</div>

@ -38,11 +38,11 @@
<div class="d-flex flex-column mt-2">
<% [first_token_transfer | remaining_token_transfers] = @transaction.token_transfers %>
<%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: first_token_transfer %>
<%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: first_token_transfer, conn: @conn %>
<div class="collapse token-transfer-toggle" id="transaction-<%= @transaction.hash %>">
<%= for token_transfer <- remaining_token_transfers do %>
<%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: token_transfer %>
<%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: token_transfer, conn: @conn %>
<% end %>
</div>
</div>

@ -20,6 +20,12 @@
</span>
</span>
<span class="col-xs-12 col-lg-4 ml-3 ml-sm-0 text-truncate">
<%= token_transfer_amount(@token_transfer) %> <%= link(token_symbol(@token_transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @token_transfer.token.contract_address_hash)) %>
<%= case token_transfer_amount(@token_transfer) do %>
<% {:ok, :erc721_instance} -> %>
<%= "TokenID ["%><%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, @token_transfer.token.contract_address_hash, to_string(@token_transfer.token_id))) %><%= "] " %>
<% {:ok, value} -> %>
<%= "#{value} " %>
<% end %>
<%= link(token_symbol(@token_transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @token_transfer.token.contract_address_hash)) %>
</span>
</div>

@ -193,7 +193,12 @@
<div class="text-right">
<%= for transfer <- aggregate_token_transfers(transaction_with_transfers.token_transfers) do %>
<h3 class="address-balance-text">
<%= token_transfer_amount(transfer) %>
<%= case token_transfer_amount(transfer) do %>
<% {:ok, :erc721_instance} -> %>
<%= "TokenID ["%><%= link(transfer.token_id, to: token_instance_path(@conn, :show, transfer.token.contract_address_hash, to_string(transfer.token_id))) %><%= "] " %>
<% {:ok, value} -> %>
<%= value %>
<% end %>
<%= " "%>
<%= link(token_symbol(transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, transfer.token.contract_address_hash)) %>
</h3>

@ -13,7 +13,13 @@
</span>
<span class="tile-title text-truncate">
<%= token_transfer_amount(@token_transfer) %> <%= link(token_symbol(@token_transfer.token), to: token_path(@conn, :show, @token_transfer.token.contract_address_hash)) %>
<%= case token_transfer_amount(@token_transfer) do%>
<% {:ok, :erc721_instance} -> %>
<%= "TokenID ["%><%= link(@token_transfer.token_id, to: token_instance_path(@conn, :show, @token_transfer.token.contract_address_hash, to_string(@token_transfer.token_id))) %><%= "]" %>
<% {:ok, value} -> %>
<%= value %>
<% end %>
<%= link(token_symbol(@token_transfer.token), to: token_path(BlockScoutWeb.Endpoint, :show, @token_transfer.token.contract_address_hash)) %>
</span>
</div>
</div>

@ -34,7 +34,7 @@ defmodule BlockScoutWeb.APIDocsView do
end)
end
defp blockscout_url do
def blockscout_url do
url_params = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url]
host = url_params[:host]
path = url_params[:path]

@ -21,19 +21,19 @@ defmodule BlockScoutWeb.Tokens.Helpers do
end
defp do_token_transfer_amount(%Token{type: "ERC-20"}, nil, _token_id) do
"--"
{:ok, "--"}
end
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: nil}, amount, _token_id) do
CurrencyHelpers.format_according_to_decimals(amount, Decimal.new(0))
{:ok, CurrencyHelpers.format_according_to_decimals(amount, Decimal.new(0))}
end
defp do_token_transfer_amount(%Token{type: "ERC-20", decimals: decimals}, amount, _token_id) do
CurrencyHelpers.format_according_to_decimals(amount, decimals)
{:ok, CurrencyHelpers.format_according_to_decimals(amount, decimals)}
end
defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, token_id) do
"TokenID [#{token_id}]"
defp do_token_transfer_amount(%Token{type: "ERC-721"}, _amount, _token_id) do
{:ok, :erc721_instance}
end
defp do_token_transfer_amount(_token, _amount, _token_id) do

@ -0,0 +1,9 @@
defmodule BlockScoutWeb.Tokens.Instance.MetadataView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Tokens.Instance.OverviewView
def format_metadata(nil), do: ""
def format_metadata(metadata), do: Poison.encode!(metadata, pretty: true)
end

@ -0,0 +1,62 @@
defmodule BlockScoutWeb.Tokens.Instance.OverviewView do
use BlockScoutWeb, :view
alias BlockScoutWeb.CurrencyHelpers
alias Explorer.Chain.{Address, SmartContract, Token}
import BlockScoutWeb.APIDocsView, only: [blockscout_url: 0]
@tabs ["token_transfers", "metadata"]
def token_name?(%Token{name: nil}), do: false
def token_name?(%Token{name: _}), do: true
def decimals?(%Token{decimals: nil}), do: false
def decimals?(%Token{decimals: _}), do: true
def total_supply?(%Token{total_supply: nil}), do: false
def total_supply?(%Token{total_supply: _}), do: true
def image_src(nil), do: "/images/controller.svg"
def image_src(instance) do
cond do
instance.metadata && instance.metadata["image_url"] -> instance.metadata["image_url"]
instance.metadata && instance.metadata["image"] -> instance.metadata["image"]
true -> image_src(nil)
end
end
def total_supply_usd(token) do
tokens = CurrencyHelpers.divide_decimals(token.total_supply, token.decimals)
price = token.usd_value
Decimal.mult(tokens, price)
end
def smart_contract_with_read_only_functions?(
%Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token
) do
Enum.any?(token.contract_address.smart_contract.abi, & &1["constant"])
end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false
def qr_code(conn, token_id, hash) do
token_instance_path = token_instance_path(conn, :show, to_string(hash), to_string(token_id))
url = Path.join(blockscout_url(), token_instance_path)
url
|> QRCode.to_png()
|> Base.encode64()
end
def current_tab_name(request_path) do
@tabs
|> Enum.filter(&tab_active?(&1, request_path))
|> tab_name()
end
defp tab_name(["token_transfers"]), do: gettext("Token Transfers")
defp tab_name(["metadata"]), do: gettext("Metadata")
end

@ -0,0 +1,5 @@
defmodule BlockScoutWeb.Tokens.Instance.TransferView do
use BlockScoutWeb, :view
alias BlockScoutWeb.Tokens.Instance.OverviewView
end

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

@ -178,6 +178,27 @@ defmodule BlockScoutWeb.WebRouter do
only: [:index],
as: :inventory
)
resources(
"/instance",
Tokens.InstanceController,
only: [:show],
as: :instance
) do
resources(
"/token_transfers",
Tokens.Instance.TransferController,
only: [:index],
as: :transfer
)
resources(
"/metadata",
Tokens.Instance.MetadataController,
only: [:index],
as: :metadata
)
end
end
resources(

@ -187,7 +187,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_link.html.eex:2
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:39
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:45
msgid "Block #%{number}"
msgstr ""
@ -300,15 +300,6 @@ msgstr ""
msgid "Clear"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:145
#: lib/block_scout_web/templates/address/overview.html.eex:153
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:110
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
msgid "Close"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:42
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165
@ -525,11 +516,6 @@ msgstr ""
msgid "Data"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
msgid "Decimals"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:31
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:37
@ -694,7 +680,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/_tile.html.eex:29
#: lib/block_scout_web/templates/transaction/overview.html.eex:179
#: lib/block_scout_web/templates/transaction/overview.html.eex:209
#: lib/block_scout_web/templates/transaction/overview.html.eex:214
#: lib/block_scout_web/views/wei_helpers.ex:78
msgid "Ether"
msgstr ""
@ -771,11 +757,6 @@ msgstr ""
msgid "Position %{index}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:227
msgid "Gas"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:13
msgid "Genesis Block"
@ -824,11 +805,6 @@ msgstr ""
msgid "IN"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/not_found.html.eex:26
msgid "If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
#: lib/block_scout_web/views/transaction_view.ex:185
@ -894,6 +870,7 @@ msgid "There are no transactions for this block."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:26
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:25
msgid "There are no transfers for this Token."
msgstr ""
@ -917,6 +894,7 @@ msgid "Toggle navigation"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:10
msgid "Token Details"
msgstr ""
@ -941,10 +919,13 @@ msgid "Token Transfer"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:60
#: lib/block_scout_web/views/tokens/overview_view.ex:35
#: lib/block_scout_web/views/transaction_view.ex:313
msgid "Token Transfers"
@ -968,11 +949,6 @@ msgstr ""
msgid "Total Difficulty"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:56
msgid "Total transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:18
#: lib/block_scout_web/views/transaction_view.ex:262
@ -997,11 +973,6 @@ msgstr ""
msgid "Indexed?"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:58
msgid "Indexing Tokens"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3
msgid "Input"
@ -1078,16 +1049,10 @@ msgid "License ID"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:237
#: lib/block_scout_web/templates/transaction/overview.html.eex:242
msgid "Limit"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:22
#: lib/block_scout_web/templates/chain/show.html.eex:13
msgid "Loading chart"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:21
@ -1156,11 +1121,6 @@ msgstr ""
msgid "Model"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58
msgid "Module"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:10
msgid "More internal transactions have come in"
@ -1276,6 +1236,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:33
#: lib/block_scout_web/templates/address/overview.html.eex:144
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:51
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:93
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:36
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:109
msgid "QR Code"
@ -1394,6 +1356,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:34
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:37
msgid "Show QR Code"
msgstr ""
@ -1420,6 +1383,7 @@ msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:91
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:19
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:21
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:21
#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:21
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:20
#: lib/block_scout_web/templates/transaction/index.html.eex:20
@ -1610,11 +1574,6 @@ msgstr ""
msgid "Transactions sent"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:64
msgid "Transfers"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47
@ -1662,7 +1621,7 @@ msgid "Use the search box to find a hosted network, or select from the list of a
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:231
#: lib/block_scout_web/templates/transaction/overview.html.eex:236
msgid "Used"
msgstr ""
@ -1698,7 +1657,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:179
#: lib/block_scout_web/templates/transaction/overview.html.eex:209
#: lib/block_scout_web/templates/transaction/overview.html.eex:214
msgid "Value"
msgstr ""
@ -1736,6 +1695,8 @@ msgid "View All Transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:55
msgid "View Contract"
msgstr ""
@ -1842,7 +1803,79 @@ msgstr ""
msgid "true"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:145
#: lib/block_scout_web/templates/address/overview.html.eex:153
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:94
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:102
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:110
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
msgid "Close"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20
msgid "Copy Metadata"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:31
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:35
msgid "Copy Token ID"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:69
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
msgid "Decimals"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:232
msgid "Gas"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/not_found.html.eex:26
msgid "If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:58
msgid "Indexing Tokens"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:22
#: lib/block_scout_web/templates/chain/show.html.eex:13
msgid "Loading chart"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:189
msgid "Log Index"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:61
msgid "Metadata"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58
msgid "Module"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:56
msgid "Total transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:67
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:64
msgid "Transfers"
msgstr ""

@ -187,7 +187,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_link.html.eex:2
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:28
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:39
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:45
msgid "Block #%{number}"
msgstr ""
@ -300,15 +300,6 @@ msgstr ""
msgid "Clear"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:145
#: lib/block_scout_web/templates/address/overview.html.eex:153
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:110
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
msgid "Close"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:42
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165
@ -525,11 +516,6 @@ msgstr ""
msgid "Data"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
msgid "Decimals"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:31
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:37
@ -694,7 +680,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/_tile.html.eex:29
#: lib/block_scout_web/templates/transaction/overview.html.eex:179
#: lib/block_scout_web/templates/transaction/overview.html.eex:209
#: lib/block_scout_web/templates/transaction/overview.html.eex:214
#: lib/block_scout_web/views/wei_helpers.ex:78
msgid "Ether"
msgstr ""
@ -771,11 +757,6 @@ msgstr ""
msgid "Position %{index}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:227
msgid "Gas"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:13
msgid "Genesis Block"
@ -824,11 +805,6 @@ msgstr ""
msgid "IN"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/not_found.html.eex:26
msgid "If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
#: lib/block_scout_web/views/transaction_view.ex:185
@ -894,6 +870,7 @@ msgid "There are no transactions for this block."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:26
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:25
msgid "There are no transfers for this Token."
msgstr ""
@ -917,6 +894,7 @@ msgid "Toggle navigation"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:10
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:10
msgid "Token Details"
msgstr ""
@ -941,10 +919,13 @@ msgid "Token Transfer"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:60
#: lib/block_scout_web/views/tokens/overview_view.ex:35
#: lib/block_scout_web/views/transaction_view.ex:313
msgid "Token Transfers"
@ -968,11 +949,6 @@ msgstr ""
msgid "Total Difficulty"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/chain/show.html.eex:56
msgid "Total transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:18
#: lib/block_scout_web/views/transaction_view.ex:262
@ -997,11 +973,6 @@ msgstr ""
msgid "Indexed?"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:58
msgid "Indexing Tokens"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/_decoded_input.html.eex:3
msgid "Input"
@ -1078,16 +1049,10 @@ msgid "License ID"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:237
#: lib/block_scout_web/templates/transaction/overview.html.eex:242
msgid "Limit"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:22
#: lib/block_scout_web/templates/chain/show.html.eex:13
msgid "Loading chart"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:14
#: lib/block_scout_web/templates/tokens/read_contract/index.html.eex:21
@ -1156,11 +1121,6 @@ msgstr ""
msgid "Model"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58
msgid "Module"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:10
msgid "More internal transactions have come in"
@ -1276,6 +1236,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:33
#: lib/block_scout_web/templates/address/overview.html.eex:144
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:51
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:93
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:36
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:109
msgid "QR Code"
@ -1394,6 +1356,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:34
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:37
msgid "Show QR Code"
msgstr ""
@ -1420,6 +1383,7 @@ msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:91
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:19
#: lib/block_scout_web/templates/tokens/holder/index.html.eex:21
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:21
#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:21
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:20
#: lib/block_scout_web/templates/transaction/index.html.eex:20
@ -1610,11 +1574,6 @@ msgstr ""
msgid "Transactions sent"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:64
msgid "Transfers"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:40
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:47
@ -1662,7 +1621,7 @@ msgid "Use the search box to find a hosted network, or select from the list of a
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:231
#: lib/block_scout_web/templates/transaction/overview.html.eex:236
msgid "Used"
msgstr ""
@ -1698,7 +1657,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:179
#: lib/block_scout_web/templates/transaction/overview.html.eex:209
#: lib/block_scout_web/templates/transaction/overview.html.eex:214
msgid "Value"
msgstr ""
@ -1736,6 +1695,8 @@ msgid "View All Transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:16
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:20
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:55
msgid "View Contract"
msgstr ""
@ -1842,7 +1803,79 @@ msgstr ""
msgid "true"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:145
#: lib/block_scout_web/templates/address/overview.html.eex:153
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:94
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:102
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:110
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
msgid "Close"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:20
msgid "Copy Metadata"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:31
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:35
msgid "Copy Token ID"
msgstr ""
#, elixir-format, fuzzy
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:69
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
msgid "Decimals"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/overview.html.eex:232
msgid "Gas"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/not_found.html.eex:26
msgid "If it still does not show up after 1 hour, please check with your sender/exchange/wallet/transaction provider for additional information."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:58
msgid "Indexing Tokens"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:22
#: lib/block_scout_web/templates/chain/show.html.eex:13
msgid "Loading chart"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:189
msgid "Log Index"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex:18
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:10
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:61
msgid "Metadata"
msgstr ""
#, elixir-format, fuzzy
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:58
msgid "Module"
msgstr ""
#, elixir-format, fuzzy
#: lib/block_scout_web/templates/chain/show.html.eex:56
msgid "Total transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:67
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:64
msgid "Transfers"
msgstr ""

@ -0,0 +1,23 @@
defmodule BlockScoutWeb.Tokens.InstanceControllerTest do
use BlockScoutWeb.ConnCase, async: false
describe "GET show/2" do
test "redirects with valid params", %{conn: conn} do
contract_address = insert(:address)
insert(:token, contract_address: contract_address)
token_id = 10
insert(:token_transfer,
from_address: contract_address,
token_contract_address: contract_address,
token_id: token_id
)
conn = get(conn, token_instance_path(BlockScoutWeb.Endpoint, :show, to_string(contract_address.hash), token_id))
assert conn.status == 302
end
end
end

@ -8,28 +8,28 @@ defmodule BlockScoutWeb.Tokens.HelpersTest do
token = build(:token, type: "ERC-20")
token_transfer = build(:token_transfer, token: token, amount: nil)
assert Helpers.token_transfer_amount(token_transfer) == "--"
assert Helpers.token_transfer_amount(token_transfer) == {:ok, "--"}
end
test "returns the formatted amount according to token decimals with ERC-20 token" do
token = build(:token, type: "ERC-20", decimals: Decimal.new(6))
token_transfer = build(:token_transfer, token: token, amount: Decimal.new(1_000_000))
assert Helpers.token_transfer_amount(token_transfer) == "1"
assert Helpers.token_transfer_amount(token_transfer) == {:ok, "1"}
end
test "returns the formatted amount when the decimals is nil with ERC-20 token" do
token = build(:token, type: "ERC-20", decimals: nil)
token_transfer = build(:token_transfer, token: token, amount: Decimal.new(1_000_000))
assert Helpers.token_transfer_amount(token_transfer) == "1,000,000"
assert Helpers.token_transfer_amount(token_transfer) == {:ok, "1,000,000"}
end
test "returns a string with the token_id with ERC-721 token" do
token = build(:token, type: "ERC-721", decimals: nil)
token_transfer = build(:token_transfer, token: token, amount: nil, token_id: 1)
assert Helpers.token_transfer_amount(token_transfer) == "TokenID [1]"
assert Helpers.token_transfer_amount(token_transfer) == {:ok, :erc721_instance}
end
test "returns nothing for unknown token's type" do

@ -41,6 +41,7 @@ defmodule Explorer.Chain do
SmartContract,
StakingPool,
Token,
Token.Instance,
TokenTransfer,
Transaction,
Wei
@ -2889,6 +2890,27 @@ defmodule Explorer.Chain do
Repo.stream_reduce(query, initial, reducer)
end
@spec stream_unfetched_token_instances(
initial :: accumulator,
reducer :: (entry :: map(), accumulator -> accumulator)
) :: {:ok, accumulator}
when accumulator: term()
def stream_unfetched_token_instances(initial, reducer) when is_function(reducer, 2) do
query =
from(
token_transfer in TokenTransfer,
inner_join: token in Token,
on: token.contract_address_hash == token_transfer.token_contract_address_hash,
left_join: instance in Instance,
on: token_transfer.token_id == instance.token_id,
where: token.type == ^"ERC-721" and is_nil(instance.token_id),
distinct: [token_transfer.token_contract_address_hash, token_transfer.token_id],
select: %{contract_address_hash: token_transfer.token_contract_address_hash, token_id: token_transfer.token_id}
)
Repo.stream_reduce(query, initial, reducer)
end
@doc """
Streams a list of token contract addresses that have been cataloged.
"""
@ -2963,11 +2985,21 @@ defmodule Explorer.Chain do
TokenTransfer.fetch_token_transfers_from_token_hash(token_address_hash, options)
end
@spec fetch_token_transfers_from_token_hash_and_token_id(Hash.t(), binary(), [paging_options]) :: []
def fetch_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options \\ []) do
TokenTransfer.fetch_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options)
end
@spec count_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_token_transfers_from_token_hash(token_address_hash) do
TokenTransfer.count_token_transfers_from_token_hash(token_address_hash)
end
@spec count_token_transfers_from_token_hash_and_token_id(Hash.t(), binary()) :: non_neg_integer()
def count_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id) do
TokenTransfer.count_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id)
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)
@ -3059,6 +3091,16 @@ defmodule Explorer.Chain do
end
end
@spec upsert_token_instance(map()) :: {:ok, Instance.t()} | {:error, Ecto.Changeset.t()}
def upsert_token_instance(params) do
changeset = Instance.changeset(%Instance{}, params)
Repo.insert(changeset,
on_conflict: :replace_all,
conflict_target: [:token_id, :token_contract_address_hash]
)
end
@doc """
Update a new `t:Token.t/0` record.
@ -3116,6 +3158,24 @@ defmodule Explorer.Chain do
|> Repo.all()
end
@spec erc721_token_instance_from_token_id_and_token_address(binary(), Hash.Address.t()) ::
{:ok, TokenTransfer.t()} | {:error, :not_found}
def erc721_token_instance_from_token_id_and_token_address(token_id, token_contract_address) do
query =
from(tt in TokenTransfer,
left_join: instance in Instance,
on: tt.token_contract_address_hash == instance.token_contract_address_hash and tt.token_id == instance.token_id,
where: tt.token_contract_address_hash == ^token_contract_address and tt.token_id == ^token_id,
limit: 1,
select: %{tt | instance: instance}
)
case Repo.one(query) do
nil -> {:error, :not_found}
token_instance -> {:ok, token_instance}
end
end
@spec address_to_coin_balances(Hash.Address.t(), [paging_options]) :: []
def address_to_coin_balances(address_hash, options) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
@ -3695,6 +3755,115 @@ defmodule Explorer.Chain do
Repo.exists?(query)
end
@doc """
Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists.
Returns `:ok` if found
iex> address = insert(:address)
iex> insert(:token, contract_address: address)
iex> Explorer.Chain.check_token_exists(address.hash)
:ok
Returns `:not_found` if not found
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
iex> Explorer.Chain.check_token_exists(hash)
:not_found
"""
@spec check_token_exists(Hash.Address.t()) :: :ok | :not_found
def check_token_exists(hash) do
hash
|> token_exists?()
|> boolean_to_check_result()
end
@doc """
Checks if a `t:Explorer.Chain.Token.t/0` with the given `hash` exists.
Returns `true` if found
iex> address = insert(:address)
iex> insert(:token, contract_address: address)
iex> Explorer.Chain.token_exists?(address.hash)
true
Returns `false` if not found
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
iex> Explorer.Chain.token_exists?(hash)
false
"""
@spec token_exists?(Hash.Address.t()) :: boolean()
def token_exists?(hash) do
query =
from(
token in Token,
where: token.contract_address_hash == ^hash
)
Repo.exists?(query)
end
@doc """
Checks if a `t:Explorer.Chain.TokenTransfer.t/0` with the given `hash` and `token_id` exists.
Returns `:ok` if found
iex> contract_address = insert(:address)
iex> token_id = 10
iex> insert(:token_transfer,
...> from_address: contract_address,
...> token_contract_address: contract_address,
...> token_id: token_id
...> )
iex> Explorer.Chain.check_erc721_token_instance_exists(token_id, contract_address.hash)
:ok
Returns `:not_found` if not found
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
iex> Explorer.Chain.check_erc721_token_instance_exists(10, hash)
:not_found
"""
@spec check_erc721_token_instance_exists(binary() | non_neg_integer(), Hash.Address.t()) :: :ok | :not_found
def check_erc721_token_instance_exists(token_id, hash) do
token_id
|> erc721_token_instance_exist?(hash)
|> boolean_to_check_result()
end
@doc """
Checks if a `t:Explorer.Chain.TokenTransfer.t/0` with the given `hash` and `token_id` exists.
Returns `true` if found
iex> contract_address = insert(:address)
iex> token_id = 10
iex> insert(:token_transfer,
...> from_address: contract_address,
...> token_contract_address: contract_address,
...> token_id: token_id
...> )
iex> Explorer.Chain.erc721_token_instance_exist?(token_id, contract_address.hash)
true
Returns `false` if not found
iex> {:ok, hash} = Explorer.Chain.string_to_address_hash("0x5aaeb6053f3e94c9b9a09f33669435e7ef1beaed")
iex> Explorer.Chain.erc721_token_instance_exist?(10, hash)
false
"""
@spec erc721_token_instance_exist?(binary() | non_neg_integer(), Hash.Address.t()) :: boolean()
def erc721_token_instance_exist?(token_id, hash) do
query =
from(tt in TokenTransfer,
where: tt.token_contract_address_hash == ^hash and tt.token_id == ^token_id
)
Repo.exists?(query)
end
defp boolean_to_check_result(true), do: :ok
defp boolean_to_check_result(false), do: :not_found

@ -0,0 +1,46 @@
defmodule Explorer.Chain.Token.Instance do
@moduledoc """
Represents an ERC 721 token instance and stores metadata defined in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-721.md.
"""
use Explorer.Schema
alias Explorer.Chain.{Hash, Token}
alias Explorer.Chain.Token.Instance
@typedoc """
* `token_id` - ID of the token
* `token_contract_address_hash` - Address hash foreign key
* `metadata` - Token instance metadata
"""
@type t :: %Instance{
token_id: non_neg_integer(),
token_contract_address_hash: Hash.Address.t(),
metadata: Map.t()
}
@primary_key false
schema "token_instances" do
field(:token_id, :decimal, primary_key: true)
field(:metadata, :map)
belongs_to(
:token,
Token,
foreign_key: :token_contract_address_hash,
references: :contract_address_hash,
type: Hash.Address,
primary_key: true
)
timestamps()
end
def changeset(%Instance{} = instance, params \\ %{}) do
instance
|> cast(params, [:token_id, :metadata, :token_contract_address_hash])
|> validate_required([:token_id, :token_contract_address_hash])
|> foreign_key_constraint(:token_contract_address_hash)
end
end

@ -28,6 +28,7 @@ defmodule Explorer.Chain.TokenTransfer do
import Ecto.Query, only: [from: 2, limit: 2, where: 3]
alias Explorer.Chain.{Address, Hash, TokenTransfer, Transaction}
alias Explorer.Chain.Token.Instance
alias Explorer.{PagingOptions, Repo}
@default_paging_options %PagingOptions{page_size: 50}
@ -92,6 +93,13 @@ defmodule Explorer.Chain.TokenTransfer do
type: Hash.Full
)
has_one(
:instance,
Instance,
foreign_key: :token_contract_address_hash,
references: :token_contract_address_hash
)
has_one(:token, through: [:token_contract_address, :token])
timestamps()
@ -140,6 +148,26 @@ defmodule Explorer.Chain.TokenTransfer do
|> Repo.all()
end
@spec fetch_token_transfers_from_token_hash_and_token_id(Hash.t(), binary(), [paging_options]) :: []
def fetch_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id, options) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
query =
from(
tt in TokenTransfer,
where: tt.token_contract_address_hash == ^token_address_hash,
where: tt.token_id == ^token_id,
where: not is_nil(tt.block_number),
preload: [{:transaction, :block}, :token, :from_address, :to_address],
order_by: [desc: tt.block_number, desc: tt.log_index]
)
query
|> page_token_transfer(paging_options)
|> limit(^paging_options.page_size)
|> Repo.all()
end
@spec count_token_transfers_from_token_hash(Hash.t()) :: non_neg_integer()
def count_token_transfers_from_token_hash(token_address_hash) do
query =
@ -152,6 +180,18 @@ defmodule Explorer.Chain.TokenTransfer do
Repo.one(query)
end
@spec count_token_transfers_from_token_hash_and_token_id(Hash.t(), binary()) :: non_neg_integer()
def count_token_transfers_from_token_hash_and_token_id(token_address_hash, token_id) do
query =
from(
tt in TokenTransfer,
where: tt.token_contract_address_hash == ^token_address_hash and tt.token_id == ^token_id,
select: fragment("COUNT(*)")
)
Repo.one(query)
end
def page_token_transfer(query, %PagingOptions{key: nil}), do: query
def page_token_transfer(query, %PagingOptions{key: {token_id}}) do

@ -0,0 +1,69 @@
defmodule Explorer.Token.InstanceMetadataRetriever do
@moduledoc """
Fetches ERC721 token instance metadata.
"""
require Logger
alias Explorer.SmartContract.Reader
alias HTTPoison.{Error, Response}
@abi [
%{
"type" => "function",
"stateMutability" => "view",
"payable" => false,
"outputs" => [
%{"type" => "string", "name" => ""}
],
"name" => "tokenURI",
"inputs" => [
%{
"type" => "uint256",
"name" => "_tokenId"
}
],
"constant" => true
}
]
@cryptokitties_address_hash "0x06012c8cf97bead5deae237070f9587f8e7a266d"
def fetch_metadata(unquote(@cryptokitties_address_hash), token_id) do
%{"tokenURI" => {:ok, ["https://api.cryptokitties.co/kitties/#{token_id}"]}}
|> fetch_json()
end
def fetch_metadata(contract_address_hash, token_id) do
contract_functions = %{"tokenURI" => [token_id]}
contract_address_hash
|> query_contract(contract_functions)
|> fetch_json()
end
def query_contract(contract_address_hash, contract_functions) do
Reader.query_contract(contract_address_hash, @abi, contract_functions)
end
defp fetch_json(%{"tokenURI" => {:ok, [""]}}) do
{:error, :no_uri}
end
defp fetch_json(%{"tokenURI" => {:ok, [token_uri]}}) do
case HTTPoison.get(token_uri) do
{:ok, %Response{body: body, status_code: 200}} ->
Jason.decode(body)
{:ok, %Response{body: body}} ->
{:error, body}
{:error, %Error{reason: reason}} ->
{:error, reason}
end
end
defp fetch_json(result) do
{:error, result}
end
end

@ -0,0 +1,22 @@
defmodule Explorer.Repo.Migrations.CreateTokenInstances do
use Ecto.Migration
def change do
create table(:token_instances, primary_key: false) do
# ERC-721 tokens have IDs
# 10^x = 2^256, x ~ 77.064, so 78 decimal digits will store the full 256-bits of a native EVM type
add(:token_id, :numeric, precision: 78, scale: 0, null: false, primary_key: true)
add(:token_contract_address_hash, references(:tokens, column: :contract_address_hash, type: :bytea),
null: false,
primary_key: true
)
add(:metadata, :jsonb)
timestamps(null: false, type: :utc_datetime_usec)
end
create_if_not_exists(index(:token_instances, [:token_id]))
end
end

@ -99,6 +99,82 @@ defmodule Explorer.ChainTest do
end
end
describe "ERC721_token_instance_from_token_id_and_token_address/2" do
test "return ERC721 token instance" do
contract_address = insert(:address)
token_id = 10
insert(:token_transfer,
from_address: contract_address,
token_contract_address: contract_address,
token_id: token_id
)
assert {:ok, result} =
Chain.erc721_token_instance_from_token_id_and_token_address(token_id, contract_address.hash)
assert result.token_id == Decimal.new(token_id)
end
end
describe "upsert_token_instance/1" do
test "insert a new token instance with valid params" do
token = insert(:token)
params = %{
token_id: 1,
token_contract_address_hash: token.contract_address_hash,
metadata: %{uri: "http://example.com"}
}
{:ok, result} = Chain.upsert_token_instance(params)
assert result.token_id == Decimal.new(1)
assert result.metadata == params.metadata
assert result.token_contract_address_hash == token.contract_address_hash
end
test "replaces existing token instance record" do
token = insert(:token)
params = %{
token_id: 1,
token_contract_address_hash: token.contract_address_hash,
metadata: %{uri: "http://example.com"}
}
{:ok, _} = Chain.upsert_token_instance(params)
params1 = %{
token_id: 1,
token_contract_address_hash: token.contract_address_hash,
metadata: %{uri: "http://example1.com"}
}
{:ok, result} = Chain.upsert_token_instance(params1)
assert result.token_id == Decimal.new(1)
assert result.metadata == params1.metadata
assert result.token_contract_address_hash == token.contract_address_hash
end
test "fails to import with invalid params" do
params = %{
token_id: 1,
metadata: %{uri: "http://example.com"}
}
{:error,
%{
errors: [
token_contract_address_hash: {"can't be blank", [validation: :required]}
],
valid?: false
}} = Chain.upsert_token_instance(params)
end
end
describe "address_to_logs/2" do
test "fetches logs" do
%Address{hash: address_hash} = address = insert(:address)
@ -3543,6 +3619,61 @@ defmodule Explorer.ChainTest do
end
end
describe "stream_unfetched_token_instances/2" do
test "reduces wuth given reducer and accumulator" do
token_contract_address = insert(:contract_address)
token = insert(:token, contract_address: token_contract_address, type: "ERC-721")
transaction =
:transaction
|> insert()
|> with_block(insert(:block, number: 1))
token_transfer =
insert(
:token_transfer,
block_number: 1000,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token,
token_id: 11
)
assert {:ok, [result]} = Chain.stream_unfetched_token_instances([], &[&1 | &2])
assert result.token_id == token_transfer.token_id
assert result.contract_address_hash == token_transfer.token_contract_address_hash
end
test "do not fetch records with token instances" do
token_contract_address = insert(:contract_address)
token = insert(:token, contract_address: token_contract_address, type: "ERC-721")
transaction =
:transaction
|> insert()
|> with_block(insert(:block, number: 1))
token_transfer =
insert(
:token_transfer,
block_number: 1000,
to_address: build(:address),
transaction: transaction,
token_contract_address: token_contract_address,
token: token,
token_id: 11
)
insert(:token_instance,
token_id: token_transfer.token_id,
token_contract_address_hash: token_transfer.token_contract_address_hash
)
assert {:ok, []} = Chain.stream_unfetched_token_instances([], &[&1 | &2])
end
end
describe "search_token/1" do
test "finds by part of the name" do
token = insert(:token, name: "magic token", symbol: "MAGIC")

@ -0,0 +1,53 @@
defmodule Explorer.Token.InstanceMetadataRetrieverTest do
use EthereumJSONRPC.Case
alias Explorer.Token.InstanceMetadataRetriever
import Mox
setup :verify_on_exit!
setup :set_mox_global
describe "fetch_metadata/2" do
@tag :no_parity
@tag :no_geth
test "fetches json metadata", %{json_rpc_named_arguments: json_rpc_named_arguments} do
if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_call",
params: [
%{
data:
"0xc87b56dd000000000000000000000000000000000000000000000000fdd5b9fa9d4bfb20",
to: "0x5caebd3b32e210e85ce3e9d51638b9c445481567"
},
"latest"
]
}
],
_options ->
{:ok,
[
%{
id: 0,
jsonrpc: "2.0",
result:
"0x0000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000003568747470733a2f2f7661756c742e7761727269646572732e636f6d2f31383239303732393934373636373130323439362e6a736f6e0000000000000000000000"
}
]}
end)
end
assert %{
"tokenURI" => {:ok, ["https://vault.warriders.com/18290729947667102496.json"]}
} ==
InstanceMetadataRetriever.query_contract("0x5caebd3b32e210e85ce3e9d51638b9c445481567", %{
"tokenURI" => [18_290_729_947_667_102_496]
})
end
end
end

@ -26,6 +26,7 @@ defmodule Explorer.Factory do
SmartContract,
Token,
TokenTransfer,
Token.Instance,
Transaction,
StakingPool,
StakingPoolsDelegator
@ -542,6 +543,14 @@ defmodule Explorer.Factory do
}
end
def token_instance_factory do
%Instance{
token_contract_address_hash: build(:address),
token_id: 5,
metadata: %{key: "value"}
}
end
def token_balance_factory do
%TokenBalance{
address: build(:address),

@ -0,0 +1,75 @@
defmodule Indexer.Fetcher.TokenInstance do
@moduledoc """
Fetches information about a token instance.
"""
use Indexer.Fetcher
use Spandex.Decorators
alias Explorer.Chain
alias Explorer.Token.InstanceMetadataRetriever
alias Indexer.BufferedTask
@behaviour BufferedTask
@defaults [
flush_interval: 300,
max_batch_size: 1,
max_concurrency: 10,
task_supervisor: Indexer.Fetcher.TokenInstance.TaskSupervisor
]
@doc false
def child_spec([init_options, gen_server_options]) do
{state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments)
unless state do
raise ArgumentError,
":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <>
"to allow for json_rpc calls when running."
end
merged_init_opts =
@defaults
|> Keyword.merge(mergeable_init_options)
|> Keyword.put(:state, state)
Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_opts}, gen_server_options]}, id: __MODULE__)
end
@impl BufferedTask
def init(initial_acc, reducer, _) do
{:ok, acc} =
Chain.stream_unfetched_token_instances(initial_acc, fn data, acc ->
reducer.(data, acc)
end)
acc
end
@impl BufferedTask
def run([%{contract_address_hash: token_contract_address_hash, token_id: token_id}], _json_rpc_named_arguments) do
case InstanceMetadataRetriever.fetch_metadata(to_string(token_contract_address_hash), Decimal.to_integer(token_id)) do
{:ok, metadata} ->
params = %{
token_id: token_id,
token_contract_address_hash: token_contract_address_hash,
metadata: metadata
}
{:ok, _result} = Chain.upsert_token_instance(params)
_other ->
:ok
end
:ok
end
@doc """
Fetches token instance data asynchronously.
"""
def async_fetch(data) do
BufferedTask.buffer(__MODULE__, data)
end
end

@ -19,6 +19,7 @@ defmodule Indexer.Supervisor do
StakingPools,
Token,
TokenBalance,
TokenInstance,
TokenUpdater,
UncleBlock
}
@ -111,6 +112,8 @@ defmodule Indexer.Supervisor do
{CoinBalance.Supervisor,
[[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
{Token.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
{TokenInstance.Supervisor,
[[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
{ContractCode.Supervisor,
[[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]},
{TokenBalance.Supervisor,

@ -69,3 +69,4 @@ $ export NETWORK=POA
| `COIN_GECKO_ID` | | CoinGecko coin id required for fetching an exchange rate | poa-network | v2.0.4+ | | master |
| `EMISSION_FORMAT` | | Should be set to `POA` if you have block emission indentical to POA Network. This env var is used only if `CHAIN_SPEC_PATH` is set | `STANDARD` | v2.0.4+ | | |
| `REWARDS_CONTRACT_ADDRESS` | | Emission rewards contract address. This env var is used only if `EMISSION_FORMAT` is set to `POA` | `0xeca443e8e1ab29971a45a9c57a6a9875701698a5` | v2.0.4+ | | |
| `BLOCKSCOUT_PROTOCOL` | | Url scheme for blockscout | in prod env `https` is used, in dev env `http` is used | master | | |

Loading…
Cancel
Save