Merge branch 'master' into realtime-pubsub-by-postges-notify

pull/2449/head
Ayrat Badykov 5 years ago committed by GitHub
commit c010eebabf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      CHANGELOG.md
  2. 2
      README.md
  3. 4
      apps/block_scout_web/assets/css/_layout.scss
  4. 2
      apps/block_scout_web/assets/css/app.scss
  5. 4
      apps/block_scout_web/assets/css/components/_address-overview.scss
  6. 6
      apps/block_scout_web/assets/css/components/_btn_contract.scss
  7. 10
      apps/block_scout_web/assets/css/components/_erc721_token_image_container.scss
  8. 24
      apps/block_scout_web/assets/css/theme/_dark-theme.scss
  9. 2
      apps/block_scout_web/assets/css/theme/_ether1_variables.scss
  10. 1
      apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss
  11. 1
      apps/block_scout_web/assets/css/theme/_ethereum_variables.scss
  12. 1
      apps/block_scout_web/assets/css/theme/_goerli_variables.scss
  13. 1
      apps/block_scout_web/assets/css/theme/_kovan_variables.scss
  14. 1
      apps/block_scout_web/assets/css/theme/_poa_variables.scss
  15. 1
      apps/block_scout_web/assets/css/theme/_rinkeby_variables.scss
  16. 1
      apps/block_scout_web/assets/css/theme/_ropsten_variables.scss
  17. 2
      apps/block_scout_web/assets/css/theme/_rsk_variables.scss
  18. 2
      apps/block_scout_web/assets/css/theme/_sokol_variables.scss
  19. 1
      apps/block_scout_web/assets/css/theme/_tomochain_variables.scss
  20. 7
      apps/block_scout_web/assets/static/images/controller.svg
  21. 1
      apps/block_scout_web/config/config.exs
  22. 2
      apps/block_scout_web/config/prod.exs
  23. 3
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  24. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  25. 3
      apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
  26. 3
      apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex
  27. 2
      apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex
  28. 34
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex
  29. 74
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex
  30. 20
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex
  31. 4
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex
  32. 2
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex
  33. 3
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex
  34. 2
      apps/block_scout_web/lib/block_scout_web/csp_header.ex
  35. 5
      apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex
  36. 2
      apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex
  37. 4
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  38. 30
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/metadata/index.html.eex
  39. 106
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex
  40. 15
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex
  41. 39
      apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex
  42. 2
      apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex
  43. 28
      apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex
  44. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
  45. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex
  46. 7
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  47. 2
      apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex
  48. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex
  49. 2
      apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex
  50. 10
      apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex
  51. 9
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex
  52. 73
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
  53. 5
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex
  54. 3
      apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex
  55. 21
      apps/block_scout_web/lib/block_scout_web/web_router.ex
  56. 148
      apps/block_scout_web/priv/gettext/default.pot
  57. 148
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  58. 2
      apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs
  59. 2
      apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
  60. 23
      apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs
  61. 8
      apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs
  62. 41
      apps/block_scout_web/test/block_scout_web/views/tokens/instance/overview_view_test.exs
  63. 2
      apps/block_scout_web/test/support/conn_case.ex
  64. 2
      apps/block_scout_web/test/support/feature_case.ex
  65. 8
      apps/explorer/config/config.exs
  66. 8
      apps/explorer/lib/explorer/application.ex
  67. 220
      apps/explorer/lib/explorer/chain.ex
  68. 42
      apps/explorer/lib/explorer/chain/cache/pending_transactions.ex
  69. 48
      apps/explorer/lib/explorer/chain/cache/uncles.ex
  70. 83
      apps/explorer/lib/explorer/chain/import/runner/blocks.ex
  71. 109
      apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex
  72. 6
      apps/explorer/lib/explorer/chain/import/runner/internal_transactions_indexed_at_blocks.ex
  73. 46
      apps/explorer/lib/explorer/chain/token/instance.ex
  74. 40
      apps/explorer/lib/explorer/chain/token_transfer.ex
  75. 15
      apps/explorer/lib/explorer/smart_contract/verifier.ex
  76. 69
      apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
  77. 22
      apps/explorer/priv/repo/migrations/20190905083522_create_token_instances.exs
  78. 17
      apps/explorer/test/explorer/chain/cache/pending_transactions_test.exs
  79. 28
      apps/explorer/test/explorer/chain/cache/uncles_test.exs
  80. 72
      apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs
  81. 131
      apps/explorer/test/explorer/chain_test.exs
  82. 28
      apps/explorer/test/explorer/smart_contract/verifier_test.exs
  83. 53
      apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs
  84. 2
      apps/explorer/test/support/data_case.ex
  85. 9
      apps/explorer/test/support/factory.ex
  86. 15
      apps/indexer/lib/indexer/block/fetcher.ex
  87. 3
      apps/indexer/lib/indexer/fetcher/pending_transaction.ex
  88. 75
      apps/indexer/lib/indexer/fetcher/token_instance.ex
  89. 9
      apps/indexer/lib/indexer/fetcher/uncle_block.ex
  90. 3
      apps/indexer/lib/indexer/supervisor.ex
  91. 2
      apps/indexer/lib/indexer/temporary/uncles_without_index.ex
  92. 1
      docs/env-variables.md

@ -1,6 +1,9 @@
## Current
### Features
- [#2733](https://github.com/poanetwork/blockscout/pull/2733) - Add cache for first page of uncles
- [#2735](https://github.com/poanetwork/blockscout/pull/2735) - Add pending transactions cache
- [#2726](https://github.com/poanetwork/blockscout/pull/2726) - Remove internal_transaction block_number setting from blocks runner
- [#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
@ -9,8 +12,18 @@
- [#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
- [#2753](https://github.com/poanetwork/blockscout/pull/2753) - fix nft token instance images
- [#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) -
fixed menu hovers in dark mode desktop view
- [#2737](https://github.com/poanetwork/blockscout/pull/2737) - switched hardcoded subnetwork value to elixir expression for mobile menu
- [#2736](https://github.com/poanetwork/blockscout/pull/2736) - do not update cache if no blocks were inserted
- [#2731](https://github.com/poanetwork/blockscout/pull/2731) - fix library verification
- [#2718](https://github.com/poanetwork/blockscout/pull/2718) - Include all addresses taking part in transactions in wallets' addresses counter
- [#2709](https://github.com/poanetwork/blockscout/pull/2709) - Fix stuck label and value for uncle block height
- [#2707](https://github.com/poanetwork/blockscout/pull/2707) - fix for dashboard banner chart legend items
@ -26,11 +39,11 @@
- [#2671](https://github.com/poanetwork/blockscout/pull/2671) - fixed buttons color at smart contract section
- [#2660](https://github.com/poanetwork/blockscout/pull/2660) - set correct last value for coin balances chart data
- [#2619](https://github.com/poanetwork/blockscout/pull/2619) - Enforce DB transaction's order to prevent deadlocks
- [#2738](https://github.com/poanetwork/blockscout/pull/2738) - do not fail block `internal_transactions_indexed_at` field update
### Chore
- [#2724](https://github.com/poanetwork/blockscout/pull/2724) - fix ci by commenting a line in hackney library
- [#2708](https://github.com/poanetwork/blockscout/pull/2708) - add log index to logs view
- [#2723](https://github.com/poanetwork/blockscout/pull/2723) - get rid of ex_json_schema warnings

@ -32,7 +32,7 @@ Currently available full-featured block explorers (Etherscan, Etherchain, Blockc
|--------------------------------------------------------|-------------------------------------------------------|------------------------------------------------------|----------------------------------------------------------------|
| [Callisto](https://blockscout.com/callisto/mainnet) | [Goerli Testnet](https://blockscout.com/eth/goerli) | [ARTIS](https://explorer.sigma1.artis.network) | [Celo Testnet](https://alfajores-blockscout.celo-testnet.org/) |
| [Ethereum Classic](https://blockscout.com/etc/mainnet) | [Kovan Testnet](https://blockscout.com/eth/kovan) | [Ether-1](https://blocks.ether1.wattpool.net/) | [Matic Testnet](https://explorer.testnet2.matic.network/) |
| [Ethereum Mainnet](https://blockscout.com/eth/mainnet) | [LUKSO L14 Testnet](https://blockscout.com/lukso/l14) | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | |
| [Ethereum Mainnet](https://blockscout.com/eth/mainnet) | [LUKSO L14 Testnet](https://blockscout.com/lukso/l14) | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | [Mordor Testnet](https://mordorexplorer.ethernode.io/) |
| [POA Core Network](https://blockscout.com/poa/core) | [POA Sokol Testnet](https://blockscout.com/poa/sokol) | [Fuse Network](https://explorer.fuse.io/) | |
| [RSK](https://blockscout.com/rsk/mainnet) | [Rinkeby Testnet](https://blockscout.com/eth/rinkeby) | [Oasis Labs](https://blockexplorer.oasiscloud.io/) | |
| [xDai Chain](https://blockscout.com/poa/dai) | [Ropsten Testnet](https://blockscout.com/eth/ropsten) | [Petrichor](https://explorer.petrachor.com/) | |

@ -8,3 +8,7 @@
background-color: #fbfafc;
}
}
.logs-hash {
line-height: 24px !important;
}

@ -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";

@ -80,3 +80,7 @@
margin-bottom: 0;
}
}
.logs-decoded {
line-height: 25px!important;
}

@ -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;
@ -792,8 +792,18 @@ $labels-dark: #8a8dba; // header nav, labels
.dark-theme-applied .dropdown-item.active, .dark-theme-applied .dropdown-item:hover, .dark-theme-applied .dropdown-item:focus {
background-image: none;
width: 100%;
background-color: #35335d!important;
background-color: #3f426c!important;
}
@media (max-width: 991.98px) {
.dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link:hover,
.dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link.activeLink,
.dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link:focus {
background-image: none;
width: 100%;
background-color: #35335d!important;
color: white;
border: none;
}
.dark-theme-applied .dropdown-item:hover:before {
content: "|";
height: 50px;
@ -803,15 +813,6 @@ $labels-dark: #8a8dba; // header nav, labels
right: 17%;
color: $dark-primary;
position: relative;
}
.dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link:hover,
.dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link.activeLink,
.dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link:focus {
background-image: none;
width: 100%;
background-color: #35335d!important;
color: white;
border: none;
}
.dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link:hover:before
{
@ -824,3 +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;

@ -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") || "/"

@ -44,8 +44,7 @@ defmodule BlockScoutWeb.AddressController do
index: index,
exchange_rate: exchange_rate,
total_supply: total_supply,
tx_count: tx_count,
validation_count: validation_count(address.hash)
tx_count: tx_count
)
end)

@ -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' * data:;\
font-src 'self' 'unsafe-inline' 'unsafe-eval' https://fonts.gstatic.com data:;\
"
})

@ -29,11 +29,6 @@
<span data-test="transaction_count">
<%= @tx_count %>
</span> <%= gettext "Transactions sent" %>
<% if validator?(@address) do %>
<span data-test="validation_count">
<%= @validation_count %>
</span> <%= gettext "Validations" %>
<% end %>
</span>
</td>
</tr>

@ -17,7 +17,7 @@
<dl class="row">
<dt class="col-md-2"> <%= gettext "Transaction" %> </dt>
<dd class="col-md-10">
<h3 class="">
<h3 class="logs-decoded">
<%= link(
@log.transaction,
to: transaction_path(@conn, :show, @log.transaction),

@ -16,7 +16,7 @@
<span class="nav-link-icon">
<svg xmlns="http://www.w3.org/2000/svg" width="6" height="6">
<circle cx="3" cy="3" r="3" fill="#80d6a1"></circle>
</svg> </span>Sokol Testnet</a>
</svg> </span><%= subnetwork_title() %></a>
<button class="new-button" id="dark-mode-changer">
<svg xmlns="http://www.w3.org/2000/svg" width="15" height="16">
<path fill="#a3a9b5" fill-rule="evenodd" d="M14.88 11.578a.544.544 0 0 0-.599-.166 5.7 5.7 0 0 1-1.924.321c-3.259 0-5.91-2.632-5.91-5.866 0-1.947.968-3.759 2.59-4.849a.534.534 0 0 0-.225-.97A5.289 5.289 0 0 0 8.059 0C3.615 0 0 3.588 0 8s3.615 8 8.059 8c2.82 0 5.386-1.423 6.862-3.806a.533.533 0 0 0-.041-.616z"></path>
@ -27,7 +27,7 @@
document.getElementById("navbar-logo").style.filter = "brightness(0) invert(1)";
}
</script>
<button class="navbar-toggler" id="toggleButton" onclick="switchVisible(); type="button" value="Click" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="<%= gettext("Toggle navigation") %>">
<button class="navbar-toggler" id="toggleButton" type="button" value="Click" data-toggle="collapse" data-target="#navbarSupportedContent" aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="<%= gettext("Toggle navigation") %>">
<span class="navbar-toggler-icon" id="toggleImage1" style="width="26px;"></span>
<span class="navbar-toggler-icon-1" id="toggleImage2"> <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" version="1.1" id="Capa_1" x="0px" y="0px" viewBox="0 0 47.971 47.971" style="enable-background:new 0 0 47.971 47.971; width: 17px;
transform: translate(0px, -1.5px);" xml:space="preserve"> <g><g>

@ -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>

@ -18,7 +18,7 @@
<dl class="row">
<dt class="col-lg-2"> <%= gettext "Address" %> </dt>
<dd class="col-lg-10">
<h3 class="">
<h3 class="logs-hash">
<%= link(
@log.address,
to: address_path(@conn, :show, @log.address),

@ -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,73 @@
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
result =
cond do
instance.metadata && instance.metadata["image_url"] ->
instance.metadata["image_url"]
instance.metadata && instance.metadata["image"] ->
instance.metadata["image"]
instance.metadata && instance.metadata["properties"]["image"]["description"] ->
instance.metadata["properties"]["image"]["description"]
true ->
image_src(nil)
end
if String.trim(result) == "", do: image_src(nil), else: result
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:71
#: 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 ""
@ -1676,11 +1635,6 @@ msgstr ""
msgid "Validated Transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tile.html.eex:35
msgid "Validations"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30
msgid "Validator Creation Date"
@ -1698,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 ""
@ -1736,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 ""
@ -1842,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:72
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:71
#: 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 ""
@ -1676,11 +1635,6 @@ msgstr ""
msgid "Validated Transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tile.html.eex:35
msgid "Validations"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:30
msgid "Validator Creation Date"
@ -1698,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 ""
@ -1736,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 ""
@ -1842,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:72
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 ""

@ -5,6 +5,8 @@ defmodule BlockScoutWeb.BlockControllerTest do
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
:ok
end

@ -11,6 +11,8 @@ defmodule BlockScoutWeb.ChainControllerTest do
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()

@ -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

@ -0,0 +1,41 @@
defmodule BlockScoutWeb.Tokens.Instance.OverviewViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.Tokens.Instance.OverviewView
describe "image_src/1" do
test "fetches image from ['properties']['image']['description'] path" do
json = """
{
"type": "object",
"title": "TestCore Metadata",
"properties": {
"name": {
"type": "string",
"description": "Identifies the asset to which this NFT represents"
},
"image": {
"type": "string",
"description": "https://img.paoditu.com/images/cut_trace_images/6b/5f/5b754f6b5f3b5_500_500.jpg"
},
"description": {
"type": "string",
"description": "Describes the asset to which this NFT represents"
}
}
}
"""
data = Jason.decode!(json)
assert OverviewView.image_src(%{metadata: data}) ==
"https://img.paoditu.com/images/cut_trace_images/6b/5f/5b754f6b5f3b5_500_500.jpg"
end
test "handles empty images" do
instance = %{metadata: %{"image" => ""}}
assert OverviewView.image_src(instance) != ""
end
end
end

@ -44,6 +44,8 @@ defmodule BlockScoutWeb.ConnCase do
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.PendingTransactions.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.PendingTransactions.child_id())
{:ok, conn: Phoenix.ConnTest.build_conn()}
end

@ -31,6 +31,8 @@ defmodule BlockScoutWeb.FeatureCase do
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.PendingTransactions.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.PendingTransactions.child_id())
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(Explorer.Repo, self())
{:ok, session} = Wallaby.start_session(metadata: metadata)

@ -146,6 +146,14 @@ config :explorer, Explorer.Chain.Cache.Accounts,
ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false),
global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.PendingTransactions,
ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false),
global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.Uncles,
ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false),
global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5))
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

@ -13,8 +13,10 @@ defmodule Explorer.Application do
BlockNumber,
Blocks,
NetVersion,
PendingTransactions,
TransactionCount,
Transactions
Transactions,
Uncles
}
alias Explorer.Chain.Events.Listener
@ -54,7 +56,9 @@ defmodule Explorer.Application do
con_cache_child_spec(MarketHistoryCache.cache_name()),
con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
Transactions,
Accounts
Accounts,
PendingTransactions,
Uncles
]
children = base_children ++ configurable_children()

@ -41,6 +41,7 @@ defmodule Explorer.Chain do
SmartContract,
StakingPool,
Token,
Token.Instance,
TokenTransfer,
Transaction,
Wei
@ -53,8 +54,10 @@ defmodule Explorer.Chain do
BlockCount,
BlockNumber,
Blocks,
PendingTransactions,
TransactionCount,
Transactions
Transactions,
Uncles
}
alias Explorer.Chain.Import.Runner
@ -1343,7 +1346,19 @@ defmodule Explorer.Chain do
paging_options = Keyword.get(options, :paging_options) || @default_paging_options
block_type = Keyword.get(options, :block_type, "Block")
if block_type == "Block" && !paging_options.key do
cond do
block_type == "Block" && !paging_options.key ->
block_from_cache(block_type, paging_options, necessity_by_association)
block_type == "Uncle" && !paging_options.key ->
uncles_from_cache(block_type, paging_options, necessity_by_association)
true ->
fetch_blocks(block_type, paging_options, necessity_by_association)
end
end
defp block_from_cache(block_type, paging_options, necessity_by_association) do
case Blocks.take_enough(paging_options.page_size) do
nil ->
elements = fetch_blocks(block_type, paging_options, necessity_by_association)
@ -1355,8 +1370,19 @@ defmodule Explorer.Chain do
blocks ->
blocks
end
else
fetch_blocks(block_type, paging_options, necessity_by_association)
end
def uncles_from_cache(block_type, paging_options, necessity_by_association) do
case Uncles.take_enough(paging_options.page_size) do
nil ->
elements = fetch_blocks(block_type, paging_options, necessity_by_association)
Uncles.update(elements)
elements
blocks ->
blocks
end
end
@ -2215,6 +2241,24 @@ defmodule Explorer.Chain do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
if is_nil(paging_options.key) do
paging_options.page_size
|> PendingTransactions.take_enough()
|> case do
nil ->
pending_transactions = fetch_recent_pending_transactions(paging_options, necessity_by_association)
PendingTransactions.update(pending_transactions)
pending_transactions
pending_transactions ->
pending_transactions
end
else
fetch_recent_pending_transactions(paging_options, necessity_by_association)
end
end
defp fetch_recent_pending_transactions(paging_options, necessity_by_association) do
Transaction
|> page_pending_transaction(paging_options)
|> limit(^paging_options.page_size)
@ -2889,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.
"""
@ -2963,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)
@ -3059,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.
@ -3116,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)
@ -3695,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,42 @@
defmodule Explorer.Chain.Cache.PendingTransactions do
@moduledoc """
Caches the latest pending transactions
"""
alias Explorer.Chain.Transaction
use Explorer.Chain.OrderedCache,
name: :pending_transactions,
max_size: 51,
preloads: [
:block,
created_contract_address: :names,
from_address: :names,
to_address: :names,
token_transfers: :token,
token_transfers: :from_address,
token_transfers: :to_address
],
ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval],
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl]
@type element :: Transaction.t()
@type id :: {non_neg_integer(), non_neg_integer()}
def element_to_id(%Transaction{inserted_at: inserted_at, hash: hash}) do
{inserted_at, hash}
end
def update_pending(transactions) when is_nil(transactions), do: :ok
def update_pending(transactions) do
transactions
|> Enum.filter(&pending?(&1))
|> update()
end
defp pending?(transaction) do
is_nil(transaction.block_hash) and (is_nil(transaction.error) or transaction.error != "dropped/replaced")
end
end

@ -0,0 +1,48 @@
defmodule Explorer.Chain.Cache.Uncles do
@moduledoc """
Caches the last known uncles
"""
alias Explorer.Chain.Block
alias Explorer.Repo
use Explorer.Chain.OrderedCache,
name: :uncles,
max_size: 60,
ids_list_key: "uncle_numbers",
preload: :transactions,
preload: [miner: :names],
preload: :rewards,
preload: :nephews,
ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval],
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl]
import Ecto.Query
@type element :: Block.t()
@type id :: non_neg_integer()
def element_to_id(%Block{number: number}), do: number
def update_from_second_degree_relations(second_degree_relations) when is_nil(second_degree_relations), do: :ok
def update_from_second_degree_relations(second_degree_relations) do
uncle_hashes =
second_degree_relations
|> Enum.map(& &1.uncle_hash)
|> Enum.uniq()
query =
from(
block in Block,
where: block.consensus == false and block.hash in ^uncle_hashes,
inner_join: nephews in assoc(block, :nephews),
preload: [nephews: block]
)
query
|> Repo.all()
|> update()
end
end

@ -56,7 +56,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
# Note, needs to be executed after `lose_consensus` for lock acquisition
insert(repo, changes_list, insert_options)
end)
|> Multi.run(:uncle_fetched_block_second_degree_relations, fn repo, %{blocks: blocks} when is_list(blocks) ->
|> Multi.run(:uncle_fetched_block_second_degree_relations, fn repo, _ ->
update_block_second_degree_relations(repo, hashes, %{
timeout:
options[Runner.Block.SecondDegreeRelations.option_key()][:timeout] ||
@ -86,15 +86,9 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
|> Multi.run(:remove_nonconsensus_logs, fn repo, %{derive_transaction_forks: transactions} ->
remove_nonconsensus_logs(repo, transactions, insert_options)
end)
|> Multi.run(:acquire_internal_transactions, fn repo, %{derive_transaction_forks: transactions} ->
acquire_internal_transactions(repo, hashes, transactions)
end)
|> Multi.run(:remove_nonconsensus_internal_transactions, fn repo, %{derive_transaction_forks: transactions} ->
remove_nonconsensus_internal_transactions(repo, transactions, insert_options)
end)
|> Multi.run(:internal_transaction_transaction_block_number, fn repo, _ ->
update_internal_transaction_block_number(repo, hashes)
end)
|> Multi.run(:acquire_contract_address_tokens, fn repo, _ ->
acquire_contract_address_tokens(repo, consensus_block_numbers)
end)
@ -139,26 +133,6 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
Tokens.acquire_contract_address_tokens(repo, contract_address_hashes)
end
defp acquire_internal_transactions(repo, hashes, forked_transaction_hashes) do
query =
from(internal_transaction in InternalTransaction,
join: transaction in Transaction,
on: internal_transaction.transaction_hash == transaction.hash,
where: transaction.block_hash in ^hashes,
or_where: transaction.hash in ^forked_transaction_hashes,
select: {internal_transaction.transaction_hash, internal_transaction.index},
# Enforce InternalTransaction ShareLocks order (see docs: sharelocks.md)
order_by: [
internal_transaction.transaction_hash,
internal_transaction.index
],
# NOTE: find a better way to know the alias that ecto gives to token
lock: "FOR UPDATE OF i0"
)
{:ok, repo.all(query)}
end
defp fork_transactions(%{
repo: repo,
timeout: timeout,
@ -168,11 +142,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
query =
from(
transaction in where_forked(blocks_changes),
select: %{
block_hash: transaction.block_hash,
index: transaction.index,
hash: transaction.hash
},
select: transaction,
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
order_by: [asc: :hash],
lock: "FOR UPDATE"
@ -196,11 +166,7 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
updated_at: ^updated_at
]
],
select: %{
block_hash: s.block_hash,
index: s.index,
hash: s.hash
}
select: s
)
{_num, transactions} = repo.update_all(update_query, [], timeout: timeout)
@ -379,13 +345,27 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
defp remove_nonconsensus_internal_transactions(repo, forked_transaction_hashes, %{timeout: timeout}) do
query =
from(internal_transaction in InternalTransaction,
from(
internal_transaction in InternalTransaction,
where: internal_transaction.transaction_hash in ^forked_transaction_hashes,
select: map(internal_transaction, [:transaction_hash, :index])
select: %{transaction_hash: internal_transaction.transaction_hash},
# Enforce InternalTransaction ShareLocks order (see docs: sharelocks.md)
order_by: [
internal_transaction.transaction_hash,
internal_transaction.index
],
lock: "FOR UPDATE"
)
# ShareLocks order already enforced by `acquire_internal_transactions` (see docs: sharelocks.md)
{_count, deleted_internal_transactions} = repo.delete_all(query, timeout: timeout)
delete_query =
from(
i in InternalTransaction,
join: s in subquery(query),
on: i.transaction_hash == s.transaction_hash,
select: map(i, [:transaction_hash, :index])
)
{_count, deleted_internal_transactions} = repo.delete_all(delete_query, timeout: timeout)
{:ok, deleted_internal_transactions}
rescue
@ -632,7 +612,8 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
b in Block.SecondDegreeRelation,
join: s in subquery(query),
on: b.nephew_hash == s.nephew_hash and b.uncle_hash == s.uncle_hash,
update: [set: [uncle_fetched_at: ^updated_at]]
update: [set: [uncle_fetched_at: ^updated_at]],
select: map(b, [:nephew_hash, :uncle_hash, :index])
)
try do
@ -645,24 +626,6 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
end
end
defp update_internal_transaction_block_number(repo, blocks_hashes) when is_list(blocks_hashes) do
query =
from(
internal_transaction in InternalTransaction,
join: transaction in Transaction,
on: internal_transaction.transaction_hash == transaction.hash,
join: block in Block,
on: block.hash == transaction.block_hash,
where: block.hash in ^blocks_hashes,
update: [set: [block_number: block.number]]
)
# ShareLocks order already enforced by `acquire_internal_transactions` (see docs: sharelocks.md)
{total, _} = repo.update_all(query, [])
{:ok, total}
end
defp where_forked(blocks_changes) when is_list(blocks_changes) do
initial = from(t in Transaction, where: false)

@ -4,9 +4,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
"""
require Ecto.Query
require Logger
alias Ecto.{Changeset, Multi, Repo}
alias Explorer.Chain.{Hash, Import, InternalTransaction, Transaction}
alias Explorer.Chain.{Block, Hash, Import, InternalTransaction, Transaction}
alias Explorer.Chain.Import.Runner
import Ecto.Query, only: [from: 2]
@ -52,32 +53,43 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
|> Multi.run(:acquire_transactions, fn repo, _ ->
acquire_transactions(repo, changes_list)
end)
|> Multi.run(:internal_transactions, fn repo, _ ->
insert(repo, changes_list, insert_options)
|> Multi.run(:internal_transactions, fn repo, %{acquire_transactions: transactions} ->
insert(repo, changes_list, transactions, insert_options)
end)
|> Multi.run(:internal_transactions_indexed_at_transactions, fn repo, %{acquire_transactions: transaction_hashes} ->
update_transactions(repo, transaction_hashes, update_transactions_options)
|> Multi.run(:internal_transactions_indexed_at_transactions, fn repo, %{acquire_transactions: transactions} ->
update_transactions(repo, transactions, update_transactions_options)
end)
|> Multi.run(
:remove_consensus_of_missing_transactions_blocks,
fn repo, %{internal_transactions: inserted} = results_map ->
# NOTE: for this to work it has to follow the runner `internal_transactions_indexed_at_blocks`
block_hashes = Map.get(results_map, :internal_transactions_indexed_at_blocks, [])
remove_consensus_of_missing_transactions_blocks(repo, block_hashes, changes_list, inserted)
end
)
end
@impl Runner
def timeout, do: @timeout
@spec insert(Repo.t(), [map], %{
@spec insert(Repo.t(), [map], [Transaction.t()], %{
optional(:on_conflict) => Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [%{index: non_neg_integer, transaction_hash: Hash.t()}]}
| {:error, [Changeset.t()]}
defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options)
defp insert(repo, changes_list, transactions, %{timeout: timeout, timestamps: timestamps} = options)
when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce InternalTransaction ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.index})
transactions_map = Map.new(transactions, &{&1.hash, &1})
final_changes_list = reject_pending_transactions(ordered_changes_list, repo)
final_changes_list =
changes_list
# Enforce InternalTransaction ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(&{&1.transaction_hash, &1.index})
|> reject_missing_transactions(transactions_map)
{:ok, internal_transactions} =
Import.insert_changes_list(
@ -86,16 +98,12 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
conflict_target: [:transaction_hash, :index],
for: InternalTransaction,
on_conflict: on_conflict,
returning: [:transaction_hash, :index],
returning: true,
timeout: timeout,
timestamps: timestamps
)
{:ok,
for(
internal_transaction <- internal_transactions,
do: Map.take(internal_transaction, [:id, :index, :transaction_hash])
)}
{:ok, internal_transactions}
end
defp default_on_conflict do
@ -158,26 +166,28 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
from(
t in Transaction,
where: t.hash in ^transaction_hashes,
# do not consider pending transactions
where: not is_nil(t.block_hash),
select: t.hash,
select: map(t, [:hash, :block_hash, :block_number]),
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
order_by: t.hash,
lock: "FOR UPDATE"
)
hashes = repo.all(query)
{:ok, hashes}
{:ok, repo.all(query)}
end
defp update_transactions(repo, transaction_hashes, %{
defp update_transactions(repo, transactions, %{
timeout: timeout,
timestamps: timestamps
})
when is_list(transaction_hashes) do
when is_list(transactions) do
transaction_hashes = Enum.map(transactions, & &1.hash)
update_query =
from(
t in Transaction,
# pending transactions are already excluded by `acquire_transactions`
where: t.hash in ^transaction_hashes,
# ShareLocks order already enforced by `acquire_transactions` (see docs: sharelocks.md)
update: [
@ -214,22 +224,51 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
end
end
defp reject_pending_transactions(ordered_changes_list, repo) do
transaction_hashes =
ordered_changes_list
|> Enum.map(& &1.transaction_hash)
|> Enum.dedup()
# If not using Parity this is not relevant
defp remove_consensus_of_missing_transactions_blocks(_, [], _, _), do: {:ok, []}
query =
from(t in Transaction,
where: t.hash in ^transaction_hashes,
where: is_nil(t.block_hash),
select: t.hash
defp remove_consensus_of_missing_transactions_blocks(repo, block_hashes, changes_list, inserted) do
inserted_block_numbers = MapSet.new(inserted, & &1.block_number)
missing_transactions_block_numbers =
changes_list
|> MapSet.new(& &1.block_number)
|> MapSet.difference(inserted_block_numbers)
|> MapSet.to_list()
update_query =
from(
b in Block,
where: b.number in ^missing_transactions_block_numbers,
where: b.hash in ^block_hashes,
# ShareLocks order already enforced by `internal_transactions_indexed_at_blocks` (see docs: sharelocks.md)
update: [set: [consensus: false, internal_transactions_indexed_at: nil]]
)
pending_transactions = repo.all(query)
try do
{_num, result} = repo.update_all(update_query, [])
Logger.debug(fn ->
[
"consensus removed from blocks with numbers: ",
inspect(missing_transactions_block_numbers),
" because of missing transactions"
]
end)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, missing_transactions_block_numbers: missing_transactions_block_numbers}}
end
end
ordered_changes_list
|> Enum.reject(fn %{transaction_hash: hash} -> Enum.member?(pending_transactions, hash) end)
defp reject_missing_transactions(ordered_changes_list, transactions_map) do
Enum.reject(ordered_changes_list, fn %{transaction_hash: hash} ->
transactions_map
|> Map.get(hash, %{})
|> Map.get(:block_hash)
|> is_nil()
end)
end
end

@ -65,12 +65,10 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsIndexedAtBlocks do
lock: "FOR UPDATE"
)
block_count = Enum.count(block_numbers)
try do
{^block_count, result} =
{_, result} =
repo.update_all(
from(b in Block, join: s in subquery(query), on: b.hash == s.hash),
from(b in Block, join: s in subquery(query), on: b.hash == s.hash, select: b.hash),
[set: [internal_transactions_indexed_at: timestamps.updated_at]],
timeout: timeout
)

@ -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

@ -69,7 +69,8 @@ defmodule Explorer.SmartContract.Verifier do
blockchain_bytecode_without_whisper = extract_bytecode(blockchain_bytecode)
cond do
generated_bytecode != blockchain_bytecode_without_whisper ->
generated_bytecode != blockchain_bytecode_without_whisper &&
!try_library_verification(generated_bytecode, blockchain_bytecode_without_whisper) ->
{:error, :generated_bytecode}
has_constructor_with_params?(abi) &&
@ -81,6 +82,18 @@ defmodule Explorer.SmartContract.Verifier do
end
end
# 730000000000000000000000000000000000000000 - default library address that returned by the compiler
defp try_library_verification(
"730000000000000000000000000000000000000000" <> bytecode,
<<_address::binary-size(42)>> <> bytecode
) do
true
end
defp try_library_verification(_, _) do
false
end
@doc """
In order to discover the bytecode we need to remove the `swarm source` from
the hash.

@ -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

@ -0,0 +1,17 @@
defmodule Explorer.Chain.Cache.PendingTransactionsTest do
use Explorer.DataCase
alias Explorer.Chain.Cache.PendingTransactions
describe "update_pending/1" do
test "adds a new pending transaction" do
transaction = insert(:transaction, block_hash: nil, error: nil)
PendingTransactions.update([transaction])
transaction_hash = transaction.hash
assert [%{hash: transaction_hash}] = PendingTransactions.all()
end
end
end

@ -0,0 +1,28 @@
defmodule Explorer.Chain.Cache.UnclesTest do
use Explorer.DataCase
alias Explorer.Chain.Cache.Uncles
alias Explorer.Repo
setup do
Supervisor.terminate_child(Explorer.Supervisor, Uncles.child_id())
Supervisor.restart_child(Explorer.Supervisor, Uncles.child_id())
:ok
end
describe "update_from_second_degree_relations/1" do
test "fetches an uncle from a second_degree_relation and adds it to the cache" do
block = insert(:block)
uncle = insert(:block, consensus: false)
uncle_hash = uncle.hash
second_degree_relation = insert(:block_second_degree_relation, uncle_hash: uncle_hash, nephew: block)
Uncles.update_from_second_degree_relations([second_degree_relation])
assert [%{hash: uncle_hash}] = Uncles.all()
end
end
end

@ -2,7 +2,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do
use Explorer.DataCase
alias Ecto.Multi
alias Explorer.Chain.{Data, Wei, Transaction, InternalTransaction}
alias Explorer.Chain.{Block, Data, Wei, Transaction, InternalTransaction}
alias Explorer.Chain.Import.Runner.InternalTransactions
describe "run/1" do
@ -42,10 +42,78 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsTest do
assert is_nil(Repo.get(Transaction, pending.hash).block_hash)
end
test "removes consensus to blocks where transactions are missing" do
empty_block = insert(:block)
pending = insert(:transaction)
assert is_nil(pending.block_hash)
full_block = insert(:block)
inserted = insert(:transaction) |> with_block(full_block)
assert full_block.hash == inserted.block_hash
index = 0
pending_transaction_changes =
pending.hash
|> make_internal_transaction_changes(index, nil)
|> Map.put(:block_number, empty_block.number)
transaction_changes =
inserted.hash
|> make_internal_transaction_changes(index, nil)
|> Map.put(:block_number, full_block.number)
multi =
Multi.new()
|> Multi.run(:internal_transactions_indexed_at_blocks, fn _, _ -> {:ok, [empty_block.hash, full_block.hash]} end)
assert {:ok, _} = run_internal_transactions([pending_transaction_changes, transaction_changes], multi)
assert from(i in InternalTransaction, where: i.transaction_hash == ^pending.hash) |> Repo.one() |> is_nil()
assert %{consensus: false} = Repo.get(Block, empty_block.hash)
assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash) |> Repo.one() |> is_nil() ==
false
assert %{consensus: true} = Repo.get(Block, full_block.hash)
end
defp run_internal_transactions(changes_list) when is_list(changes_list) do
test "does not remove consensus when block is empty and no transactions are missing" do
empty_block = insert(:block)
full_block = insert(:block)
inserted = insert(:transaction) |> with_block(full_block)
assert full_block.hash == inserted.block_hash
index = 0
transaction_changes =
inserted.hash
|> make_internal_transaction_changes(index, nil)
|> Map.put(:block_number, full_block.number)
multi =
Multi.new()
|> Multi.run(:internal_transactions_indexed_at_blocks, fn _, _ -> {:ok, [empty_block.hash, full_block.hash]} end)
assert {:ok, _} = run_internal_transactions([transaction_changes], multi)
assert %{consensus: true} = Repo.get(Block, empty_block.hash)
assert from(i in InternalTransaction, where: i.transaction_hash == ^inserted.hash) |> Repo.one() |> is_nil() ==
false
assert %{consensus: true} = Repo.get(Block, full_block.hash)
end
end
defp run_internal_transactions(changes_list, multi \\ Multi.new()) when is_list(changes_list) do
multi
|> InternalTransactions.run(changes_list, %{
timeout: :infinity,
timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}

@ -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")

@ -144,6 +144,34 @@ defmodule Explorer.SmartContract.VerifierTest do
assert abi != nil
end
test "verifies a library" do
bytecode =
"0x7349f540c22cba15c47a08c235e20081474201a742301460806040526004361060335760003560e01c8063c2985578146038575b600080fd5b603e60b0565b6040805160208082528351818301528351919283929083019185019080838360005b8381101560765781810151838201526020016060565b50505050905090810190601f16801560a25780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b604080518082019091526003815262666f6f60e81b60208201529056fea265627a7a72315820174b282a3ef3b9778d79fbc2e4c36bc939c54dfaaaa51d3122ee6e648093844c64736f6c634300050b0032"
contract_address = insert(:contract_address, contract_code: bytecode)
code = """
pragma solidity 0.5.11;
library Foo {
function foo() external pure returns (string memory) {
return "foo";
}
}
"""
params = %{
"contract_source_code" => code,
"compiler_version" => "v0.5.11+commit.c082d0b4",
"evm_version" => "default",
"name" => "Foo",
"optimization" => true
}
assert {:ok, %{abi: abi}} = Verifier.evaluate_authenticity(contract_address.hash, params)
assert abi != nil
end
test "verifies smart contract compiled with Solidity 0.5.9 (includes new metadata in bytecode) with constructor args" do
path = File.cwd!() <> "/test/support/fixture/smart_contract/solidity_0.5.9_smart_contract.sol"
contract = File.read!(path)

@ -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

@ -47,6 +47,8 @@ defmodule Explorer.DataCase do
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.PendingTransactions.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.PendingTransactions.child_id())
:ok
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),

@ -13,7 +13,7 @@ defmodule Indexer.Block.Fetcher do
alias Explorer.Chain
alias Explorer.Chain.{Address, Block, Hash, Import, Transaction}
alias Explorer.Chain.Cache.Blocks, as: BlocksCache
alias Explorer.Chain.Cache.{Accounts, BlockNumber, Transactions}
alias Explorer.Chain.Cache.{Accounts, BlockNumber, PendingTransactions, Transactions, Uncles}
alias Indexer.Block.Fetcher.Receipts
alias Indexer.Fetcher.{
@ -175,8 +175,9 @@ defmodule Indexer.Block.Fetcher do
) do
result = {:ok, %{inserted: inserted, errors: blocks_errors}}
update_block_cache(inserted[:blocks])
update_transactions_cache(inserted[:transactions])
update_transactions_cache(inserted[:transactions], inserted[:fork_transactions])
update_addresses_cache(inserted[:addresses])
update_uncles_cache(inserted[:block_second_degree_relations])
result
else
{step, {:error, reason}} -> {:error, {step, reason}}
@ -184,6 +185,8 @@ defmodule Indexer.Block.Fetcher do
end
end
defp update_block_cache([]), do: :ok
defp update_block_cache(blocks) when is_list(blocks) do
{min_block, max_block} = Enum.min_max_by(blocks, & &1.number)
@ -194,12 +197,18 @@ defmodule Indexer.Block.Fetcher do
defp update_block_cache(_), do: :ok
defp update_transactions_cache(transactions) do
defp update_transactions_cache(transactions, forked_transactions) do
Transactions.update(transactions)
PendingTransactions.update_pending(transactions)
PendingTransactions.update_pending(forked_transactions)
end
defp update_addresses_cache(addresses), do: Accounts.drop(addresses)
defp update_uncles_cache(updated_relations) do
Uncles.update_from_second_degree_relations(updated_relations)
end
def import(
%__MODULE__{broadcast: broadcast, callback_module: callback_module} = state,
options

@ -14,7 +14,7 @@ defmodule Indexer.Fetcher.PendingTransaction do
alias Ecto.Changeset
alias Explorer.Chain
alias Explorer.Chain.Cache.Accounts
alias Explorer.Chain.Cache.{Accounts, PendingTransactions}
alias Indexer.Fetcher.PendingTransaction
alias Indexer.Transform.Addresses
@ -151,6 +151,7 @@ defmodule Indexer.Fetcher.PendingTransaction do
}) do
{:ok, imported} ->
Accounts.drop(imported[:addresses])
PendingTransactions.update_pending(imported[:transactions])
:ok
{:error, [%Changeset{} | _] = changesets} ->

@ -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

@ -12,7 +12,7 @@ defmodule Indexer.Fetcher.UncleBlock do
alias Ecto.Changeset
alias EthereumJSONRPC.Blocks
alias Explorer.Chain
alias Explorer.Chain.Cache.Accounts
alias Explorer.Chain.Cache.{Accounts, PendingTransactions, Uncles}
alias Explorer.Chain.Hash
alias Indexer.{Block, BufferedTask, Tracer}
alias Indexer.Fetcher.UncleBlock
@ -129,6 +129,7 @@ defmodule Indexer.Fetcher.UncleBlock do
}) do
{:ok, imported} ->
Accounts.drop(imported[:addresses])
Uncles.update_from_second_degree_relations(imported[:block_second_degree_relations])
retry(errors)
{:error, {:import = step, [%Changeset{} | _] = changesets}} ->
@ -155,7 +156,7 @@ defmodule Indexer.Fetcher.UncleBlock do
@impl Block.Fetcher
def import(_, options) when is_map(options) do
with {:ok, %{block_second_degree_relations: block_second_degree_relations}} = ok <-
with {:ok, %{block_second_degree_relations: block_second_degree_relations} = imported} <-
options
|> Map.drop(@ignored_options)
|> uncle_blocks()
@ -168,8 +169,10 @@ defmodule Indexer.Fetcher.UncleBlock do
# * TokenBalance.async_fetch is not called because it uses block numbers from consensus, not uncles
UncleBlock.async_fetch_blocks(block_second_degree_relations)
PendingTransactions.update_pending(imported[:transactions])
PendingTransactions.update_pending(imported[:fork_transactions])
ok
{:ok, imported}
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,

@ -15,6 +15,7 @@ defmodule Indexer.Temporary.UnclesWithoutIndex do
alias EthereumJSONRPC.Blocks
alias Explorer.{Chain, Repo}
alias Explorer.Chain.Block.SecondDegreeRelation
alias Explorer.Chain.Cache.Uncles
alias Indexer.{BufferedTask, Tracer}
alias Indexer.Fetcher.UncleBlock
@ -99,6 +100,7 @@ defmodule Indexer.Temporary.UnclesWithoutIndex do
case Chain.import(%{block_second_degree_relations: %{params: block_second_degree_relations_params}}) do
{:ok, %{block_second_degree_relations: block_second_degree_relations}} ->
UncleBlock.async_fetch_blocks(block_second_degree_relations)
Uncles.update_from_second_degree_relations(block_second_degree_relations)
retry(errors)

@ -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