Merge branch 'master' into ab-allow-enabling-internal-transaction-fetching-for-geth

pull/2752/head
Ayrat Badykov 5 years ago committed by GitHub
commit ec4aa32e5f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      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. 8
      apps/block_scout_web/assets/css/theme/_dark-theme.scss
  6. 2
      apps/block_scout_web/assets/css/theme/_ether1_variables.scss
  7. 1
      apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss
  8. 1
      apps/block_scout_web/assets/css/theme/_ethereum_variables.scss
  9. 1
      apps/block_scout_web/assets/css/theme/_goerli_variables.scss
  10. 1
      apps/block_scout_web/assets/css/theme/_kovan_variables.scss
  11. 1
      apps/block_scout_web/assets/css/theme/_poa_variables.scss
  12. 1
      apps/block_scout_web/assets/css/theme/_rinkeby_variables.scss
  13. 1
      apps/block_scout_web/assets/css/theme/_ropsten_variables.scss
  14. 2
      apps/block_scout_web/assets/css/theme/_rsk_variables.scss
  15. 2
      apps/block_scout_web/assets/css/theme/_social_variables.scss
  16. 2
      apps/block_scout_web/assets/css/theme/_sokol_variables.scss
  17. 1
      apps/block_scout_web/assets/css/theme/_tomochain_variables.scss
  18. 7
      apps/block_scout_web/assets/static/images/controller.svg
  19. 1
      apps/block_scout_web/config/config.exs
  20. 2
      apps/block_scout_web/config/prod.exs
  21. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  22. 3
      apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
  23. 3
      apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
  24. 2
      apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
  25. 34
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex
  26. 74
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex
  27. 20
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex
  28. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex
  29. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex
  30. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  31. 2
      apps/block_scout_web/lib/block_scout_web/csp_header.ex
  32. 30
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex
  33. 106
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex
  34. 15
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex
  35. 39
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex
  36. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex
  37. 28
      apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex
  38. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
  39. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex
  40. 7
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  41. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex
  42. 2
      apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex
  43. 10
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  44. 9
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex
  45. 62
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
  46. 5
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex
  47. 3
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex
  48. 21
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  49. 143
      apps/block_scout_web/priv/gettext/default.pot
  50. 143
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  51. 23
      apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs
  52. 8
      apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs
  53. 169
      apps/explorer/lib/explorer/chain.ex
  54. 46
      apps/explorer/lib/explorer/chain/token/instance.ex
  55. 40
      apps/explorer/lib/explorer/chain/token_transfer.ex
  56. 69
      apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
  57. 22
      apps/explorer/priv/repo/migrations/20190905083522_create_token_instances.exs
  58. 131
      apps/explorer/test/explorer/chain_test.exs
  59. 53
      apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs
  60. 9
      apps/explorer/test/support/factory.ex
  61. 75
      apps/indexer/lib/indexer/fetcher/token_instance.ex
  62. 3
      apps/indexer/lib/indexer/supervisor.ex
  63. 1
      docs/env-variables.md

@ -12,8 +12,10 @@
- [#2666](https://github.com/poanetwork/blockscout/pull/2666) - fetch token counters in parallel
- [#2665](https://github.com/poanetwork/blockscout/pull/2665) - new menu layout for mobile devices
- [#2663](https://github.com/poanetwork/blockscout/pull/2663) - Fetch address counters in parallel
- [#2642](https://github.com/poanetwork/blockscout/pull/2642) - add ERC721 coin instance page
### Fixes
- [#2750](https://github.com/poanetwork/blockscout/pull/2750) - fixed contract buttons color for NFT token instance on each theme
- [#2746](https://github.com/poanetwork/blockscout/pull/2746) - fixed wrong alignment in logs decoded view
- [#2745](https://github.com/poanetwork/blockscout/pull/2745) - optimize addresses page
- [#2742](https://github.com/poanetwork/blockscout/pull/2742) -

@ -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;
@ -504,7 +504,7 @@ $labels-dark: #8a8dba; // header nav, labels
border-top-color: darken($labels-dark, 30);
border-bottom-color: darken($labels-dark, 30);
}
// Form Controlls
.form-control {
background-color: $dark-light;
@ -749,7 +749,7 @@ $labels-dark: #8a8dba; // header nav, labels
color: #fff;
}
}
// alerts
.alert-link {
color: $labels-dark;
@ -825,4 +825,4 @@ $labels-dark: #8a8dba; // header nav, labels
top: 14%;
color: $dark-primary;
}
}
}

@ -37,7 +37,7 @@ $btn-line-bg: #fff; // button bg
$btn-line-color: #4b021e; // button border and font color && hover bg color
$btn-copy-color: #4b021e; // btn copy
$btn-qr-color: #4b021e; // btn qr-code
$btn-contract-color: #4b021e;
//links & tile
.tile a { color: $tertiary !important; } // links color for badges
.tile-type-block {

@ -35,6 +35,7 @@ $btn-line-color: $tertiary; // button border and font color && hover bg color
$btn-copy-color: $tertiary; // btn copy
$btn-qr-color: $tertiary; // btn qr-code
$btn-address-card-icon-color: $tertiary; // btn address color
$btn-contract-color: $tertiary;
//links & tile
$tile-body-a-color: $tertiary;

@ -38,6 +38,7 @@ $btn-line-color: $secondary; // button border and font color && hover bg color
$btn-copy-color: $secondary; // btn copy
$btn-qr-color: $secondary; // btn qr-code
$btn-address-card-icon-color: $secondary; // btn address color
$btn-contract-color: $secondary;
//links & tile
$tile-body-a-color: $secondary;

@ -44,6 +44,7 @@ $btn-line-color: $sub-accent-color; // button border and font color && hover bg
$btn-copy-color: $sub-accent-color; // btn copy
$btn-qr-color: $sub-accent-color; // btn qr-code
$btn-address-card-icon-color: $sub-accent-color; // btn address color
$btn-contract-color: $sub-accent-color;
//links & tile
$tile-body-a-color: $sub-accent-color;

@ -39,6 +39,7 @@ $btn-line-color: $tertiary; // button border and font color && hover bg color
$btn-copy-color: $tertiary; // btn copy
$btn-qr-color: $tertiary; // btn qr-code
$btn-address-card-icon-color: $tertiary; // btn address color
$btn-contract-color: $tertiary;
//links & tile
$tile-body-a-color: $tertiary;

@ -41,6 +41,7 @@ $btn-line-color: $primary; // button border and font color && hover bg color
$btn-copy-color: $primary; // btn copy
$btn-qr-color: $primary; // btn qr-code
$btn-address-card-icon-color: $primary; // btn address color
$btn-contract-color: $primary;
//links & tile
$tile-body-a-color: $primary;

@ -38,6 +38,7 @@ $btn-line-color: $secondary; // button border and font color && hover bg color
$btn-copy-color: $secondary; // btn copy
$btn-qr-color: $secondary; // btn qr-code
$btn-address-card-icon-color: $secondary; // btn address color
$btn-contract-color: $secondary;
//links & tile
$tile-body-a-color: $secondary;

@ -38,6 +38,7 @@ $btn-line-color: $secondary; // button border and font color && hover bg color
$btn-copy-color: $secondary; // btn copy
$btn-qr-color: $secondary; // btn qr-code
$btn-address-card-icon-color: $secondary; // btn address color
$btn-contract-color: $secondary;
//links & tile
$tile-body-a-color: $secondary;

@ -39,7 +39,7 @@ $btn-line-color: $secondary; // button border and font color && hover bg color
$btn-copy-color: $secondary; // btn copy
$btn-qr-color: $secondary; // btn qr-code
$btn-address-card-icon-color: $secondary; // btn address color
$btn-contract-color: $secondary;
// card
$card-background-1: $secondary;
$card-tab-active: $secondary;

@ -5,4 +5,4 @@ $tertiary: #53a9ff;
$footer-background-color: $primary;
$footer-title-color: #fff;
$footer-text-color: #fff;
$footer-item-disc-color: $secondary;
$footer-item-disc-color: $secondary;

@ -50,7 +50,7 @@ $btn-line-color: $sub-accent-color; // button border and font color && hover bg
$btn-copy-color: $sub-accent-color; // btn copy
$btn-qr-color: $sub-accent-color; // btn qr-code
$btn-address-card-icon-color: $sub-accent-color; // btn address color
$btn-contract-color: $sub-accent-color;
//links & tile
$tile-body-a-color: $sub-accent-color;
$tile-type-block-color: $sub-accent-color;

@ -1,3 +1,4 @@
$primary: #211841;
$secondary: #f16950;
$tertiary: #8b84bc;
$btn-contract-color: $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 ""
@ -1693,7 +1652,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 ""
@ -1731,6 +1690,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 ""
@ -1837,7 +1798,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 ""
@ -1693,7 +1652,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 ""
@ -1731,6 +1690,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 ""
@ -1837,7 +1798,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
@ -2932,6 +2933,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.
"""
@ -3006,11 +3028,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)
@ -3102,6 +3134,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.
@ -3159,6 +3201,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)
@ -3738,6 +3798,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,

@ -70,3 +70,4 @@ $ export NETWORK=POA
| `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+ | | |
| `INTERNAL_TRANSACTIONOS_FOR_TOKEN_TRANSFERS` | | Does not applicable for parity because we fetch all transactions for it. If set to true fetches internal transactions for simple token transfers transactions. It's disabled by default to increase internal transactions indexing speed | `false` | master | | |
| `BLOCKSCOUT_PROTOCOL` | | Url scheme for blockscout | in prod env `https` is used, in dev env `http` is used | master | | |

Loading…
Cancel
Save