diff --git a/CHANGELOG.md b/CHANGELOG.md index 6791e814b7..b26ab858ad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.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 diff --git a/README.md b/README.md index 138ddf18d8..d8cb35d7a0 100644 --- a/README.md +++ b/README.md @@ -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/) | | diff --git a/apps/block_scout_web/assets/css/_layout.scss b/apps/block_scout_web/assets/css/_layout.scss index f1a4e92312..da36762bee 100644 --- a/apps/block_scout_web/assets/css/_layout.scss +++ b/apps/block_scout_web/assets/css/_layout.scss @@ -8,3 +8,7 @@ background-color: #fbfafc; } } + +.logs-hash { + line-height: 24px !important; +} \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/app.scss b/apps/block_scout_web/assets/css/app.scss index 4563ec33d5..a6c5e40c01 100644 --- a/apps/block_scout_web/assets/css/app.scss +++ b/apps/block_scout_web/assets/css/app.scss @@ -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"; diff --git a/apps/block_scout_web/assets/css/components/_address-overview.scss b/apps/block_scout_web/assets/css/components/_address-overview.scss index a936aec99f..f758e4affc 100644 --- a/apps/block_scout_web/assets/css/components/_address-overview.scss +++ b/apps/block_scout_web/assets/css/components/_address-overview.scss @@ -79,4 +79,8 @@ &:last-child { margin-bottom: 0; } +} + +.logs-decoded { + line-height: 25px!important; } \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/components/_btn_contract.scss b/apps/block_scout_web/assets/css/components/_btn_contract.scss new file mode 100644 index 0000000000..e78fbc200a --- /dev/null +++ b/apps/block_scout_web/assets/css/components/_btn_contract.scss @@ -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); +} diff --git a/apps/block_scout_web/assets/css/components/_erc721_token_image_container.scss b/apps/block_scout_web/assets/css/components/_erc721_token_image_container.scss new file mode 100644 index 0000000000..261cab7d78 --- /dev/null +++ b/apps/block_scout_web/assets/css/components/_erc721_token_image_container.scss @@ -0,0 +1,10 @@ +/* ERC721 image block */ +.erc721-image { + display: flex; + justify-content: center; +} + + .erc721-image img { + height: 150px; +} +/* ERC721 image block end */ diff --git a/apps/block_scout_web/assets/css/theme/_dark-theme.scss b/apps/block_scout_web/assets/css/theme/_dark-theme.scss index bd4a97aa11..b6bfa5d202 100644 --- a/apps/block_scout_web/assets/css/theme/_dark-theme.scss +++ b/apps/block_scout_web/assets/css/theme/_dark-theme.scss @@ -234,7 +234,7 @@ $labels-dark: #8a8dba; // header nav, labels } } - .btn-copy-icon, .btn-qr-icon, .btn-address-card-icon { + .btn-copy-icon, .btn-qr-icon, .btn-address-card-icon .btn-contract-icon { border-color: $dark-primary; path { fill: $dark-primary; @@ -504,7 +504,7 @@ $labels-dark: #8a8dba; // header nav, labels border-top-color: darken($labels-dark, 30); border-bottom-color: darken($labels-dark, 30); } - + // Form Controlls .form-control { background-color: $dark-light; @@ -749,7 +749,7 @@ $labels-dark: #8a8dba; // header nav, labels color: #fff; } } - + // alerts .alert-link { color: $labels-dark; @@ -792,35 +792,37 @@ $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; -} - .dark-theme-applied .dropdown-item:hover:before { - content: "|"; - height: 50px; - width: 50%; - opacity: 1; - background: none; - right: 17%; - color: $dark-primary; - position: relative; + background-color: #3f426c!important; } -.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 - { - content: "|"; - height: 50px; - width: 50%; - opacity: 1; - background: none; - left: 24%; - top: 14%; - color: $dark-primary; - } \ No newline at end of file +@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; + width: 50%; + opacity: 1; + background: none; + right: 17%; + color: $dark-primary; + position: relative; + } + .dark-theme-applied .navbar.navbar-primary .navbar-nav .nav-link:hover:before + { + content: "|"; + height: 50px; + width: 50%; + opacity: 1; + background: none; + left: 24%; + top: 14%; + color: $dark-primary; + } + } diff --git a/apps/block_scout_web/assets/css/theme/_ether1_variables.scss b/apps/block_scout_web/assets/css/theme/_ether1_variables.scss index 323ceb22ac..ec5e65bb95 100644 --- a/apps/block_scout_web/assets/css/theme/_ether1_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_ether1_variables.scss @@ -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 { diff --git a/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss b/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss index 6b8d6d7f96..ffcdc4a44b 100644 --- a/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss b/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss index a06e0be93e..564242b150 100644 --- a/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_ethereum_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_goerli_variables.scss b/apps/block_scout_web/assets/css/theme/_goerli_variables.scss index af57f2d1a2..d9d5d2ea17 100644 --- a/apps/block_scout_web/assets/css/theme/_goerli_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_goerli_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_kovan_variables.scss b/apps/block_scout_web/assets/css/theme/_kovan_variables.scss index b47c3de0b2..dc1cceafbd 100644 --- a/apps/block_scout_web/assets/css/theme/_kovan_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_kovan_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_poa_variables.scss b/apps/block_scout_web/assets/css/theme/_poa_variables.scss index 536ecdc105..88dcb6eb06 100644 --- a/apps/block_scout_web/assets/css/theme/_poa_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_poa_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_rinkeby_variables.scss b/apps/block_scout_web/assets/css/theme/_rinkeby_variables.scss index 21388bb315..68631dc349 100644 --- a/apps/block_scout_web/assets/css/theme/_rinkeby_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_rinkeby_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_ropsten_variables.scss b/apps/block_scout_web/assets/css/theme/_ropsten_variables.scss index 21388bb315..68631dc349 100644 --- a/apps/block_scout_web/assets/css/theme/_ropsten_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_ropsten_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_rsk_variables.scss b/apps/block_scout_web/assets/css/theme/_rsk_variables.scss index 8d3bff31d2..1e23b819c4 100644 --- a/apps/block_scout_web/assets/css/theme/_rsk_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_rsk_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_social_variables.scss b/apps/block_scout_web/assets/css/theme/_social_variables.scss index 3d878dcf10..22718bd084 100644 --- a/apps/block_scout_web/assets/css/theme/_social_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_social_variables.scss @@ -5,4 +5,4 @@ $tertiary: #53a9ff; $footer-background-color: $primary; $footer-title-color: #fff; $footer-text-color: #fff; -$footer-item-disc-color: $secondary; +$footer-item-disc-color: $secondary; \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/theme/_sokol_variables.scss b/apps/block_scout_web/assets/css/theme/_sokol_variables.scss index 2860342f1e..836cc8a114 100644 --- a/apps/block_scout_web/assets/css/theme/_sokol_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_sokol_variables.scss @@ -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; diff --git a/apps/block_scout_web/assets/css/theme/_tomochain_variables.scss b/apps/block_scout_web/assets/css/theme/_tomochain_variables.scss index 6f7694de7d..d3c3a166d5 100644 --- a/apps/block_scout_web/assets/css/theme/_tomochain_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_tomochain_variables.scss @@ -1,3 +1,4 @@ $primary: #211841; $secondary: #f16950; $tertiary: #8b84bc; +$btn-contract-color: $primary; diff --git a/apps/block_scout_web/assets/static/images/controller.svg b/apps/block_scout_web/assets/static/images/controller.svg new file mode 100644 index 0000000000..c62d55bebe --- /dev/null +++ b/apps/block_scout_web/assets/static/images/controller.svg @@ -0,0 +1,7 @@ + + + + + + + diff --git a/apps/block_scout_web/config/config.exs b/apps/block_scout_web/config/config.exs index 0126a7d410..1347195b91 100644 --- a/apps/block_scout_web/config/config.exs +++ b/apps/block_scout_web/config/config.exs @@ -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") || "/" ], diff --git a/apps/block_scout_web/config/prod.exs b/apps/block_scout_web/config/prod.exs index c388caab64..00cedfe8c3 100644 --- a/apps/block_scout_web/config/prod.exs +++ b/apps/block_scout_web/config/prod.exs @@ -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") || "/" diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index e2cb1f6db7..d91552229a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -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) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 9236c433a7..eeb5a5c11b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -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 ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex index dd362cd065..9351f9f886 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex @@ -50,7 +50,8 @@ defmodule BlockScoutWeb.BlockTransactionController do View.render_to_string( TransactionView, "_tile.html", - transaction: transaction + transaction: transaction, + conn: conn ) end) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex index 1128f04616..9385ee5616 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex index a98e380e67..3b9830476c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/recent_transactions_controller.ex @@ -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) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex new file mode 100644 index 0000000000..90b2006543 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/metadata_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex new file mode 100644 index 0000000000..4ea0bd2d76 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance/transfer_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex new file mode 100644 index 0000000000..d3c43bda46 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/instance_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex index ddd12eb392..622dc35946 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex @@ -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) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex index f977d091f1..d7bd40cfab 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex @@ -30,7 +30,7 @@ defmodule BlockScoutWeb.Tokens.TransferController do "_token_transfer.html", conn: conn, token: token, - transfer: transfer + token_transfer: transfer ) end) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex index 3f849c0e7c..db0cae65d8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/csp_header.ex b/apps/block_scout_web/lib/block_scout_web/csp_header.ex index 199ee9f189..75cfbf0cb6 100644 --- a/apps/block_scout_web/lib/block_scout_web/csp_header.ex +++ b/apps/block_scout_web/lib/block_scout_web/csp_header.ex @@ -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:;\ " }) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex index 56f1a2bbb2..4945dca50c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex @@ -29,11 +29,6 @@ <%= @tx_count %> <%= gettext "Transactions sent" %> - <% if validator?(@address) do %> - - <%= @validation_count %> - <%= gettext "Validations" %> - <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex index 92a8ebc6cc..945049ace6 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/_logs.html.eex @@ -17,7 +17,7 @@
<%= gettext "Transaction" %>
-

+

<%= link( @log.transaction, to: transaction_path(@conn, :show, @log.transaction), diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index 0463d28a15..2b21120556 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -16,7 +16,7 @@ - Sokol Testnet + <%= subnetwork_title() %> + +
+
<%= format_metadata(@token_instance.instance.metadata) %>
+            
+
+ + + + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex new file mode 100644 index 0000000000..991c8e13f6 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex @@ -0,0 +1,106 @@ +
+
+
+
+
+

+ <%= if token_name?(@token) do %> + <%= @token.name %> + <% else %> + <%= gettext("Token Details") %> + <% end %> + + + + ' + > + + + + + + + + + + + + + + + + + + + + + +

+ +

<%= to_string(@token_id) %>

+ +
+
+ <%= @token.type %> + <%= @total_token_transfers %> <%= gettext "Transfers" %> + <%= if decimals?(@token) do %> + <%= @token.decimals %> <%= gettext "Decimals" %> + <% end %> +
+
+
+
+
+ +
+
+
+
+ /> +
+
+
+
+
+
+ + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex new file mode 100644 index 0000000000..fe0eb5c0e9 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex @@ -0,0 +1,15 @@ +
+ <%= 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 %> +
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex new file mode 100644 index 0000000000..bade3900bc --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex @@ -0,0 +1,39 @@ +
+ <%= render( + OverviewView, + "_details.html", + token: @token, + total_token_transfers: @total_token_transfers, + token_id: @token_instance.token_id, + token_instance: @token_instance, + conn: @conn + ) %> + +
+
+ <%= render OverviewView, "_tabs.html", assigns %> +
+

<%= gettext "Token Transfers" %>

+ + <%= 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 %> + + + + + +
+ <%= render BlockScoutWeb.CommonComponentsView, "_tile-loader.html" %> +
+ + <%= 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 %> + +
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex index 88c36e5158..c3fea04269 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/inventory/_token.html.eex @@ -10,7 +10,7 @@ <%= gettext "Token ID" %>: - <%= @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))) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex index 5b8959f0a3..0a017c7c3d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex @@ -6,29 +6,35 @@
- <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @transfer.transaction_hash %> + <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @token_transfer.transaction_hash %> - <%= 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 %> → - <%= 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 %> - <%= 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 %>
@@ -36,11 +42,11 @@
<%= 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) ) %> - +
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex index 91a7d9973a..85711ca136 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex @@ -38,11 +38,11 @@
<% [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 %>
<%= 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 %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex index 8c88fdf87b..4926406a12 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex @@ -20,6 +20,12 @@ - <%= 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)) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex index 00266d92b3..0f3095f4c4 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex @@ -193,7 +193,12 @@
<%= for transfer <- aggregate_token_transfers(transaction_with_transfers.token_transfers) do %>

- <%= 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)) %>

diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex index 6615ec8259..0d958b938c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex @@ -18,7 +18,7 @@
<%= gettext "Address" %>
-

+

<%= link( @log.address, to: address_path(@conn, :show, @log.address), diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex index 7c3c202f80..793b88bcf0 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex @@ -13,7 +13,13 @@ - <%= 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)) %>

diff --git a/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex index 363a4a0179..a75bd2dcbf 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex @@ -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] diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex index 33569f3a97..0cc90aad69 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/helpers.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex new file mode 100644 index 0000000000..5a19f39684 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/metadata_view.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex new file mode 100644 index 0000000000..f56d10761e --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex new file mode 100644 index 0000000000..2cf314efa1 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/transfer_view.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.Tokens.Instance.TransferView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.Tokens.Instance.OverviewView +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex new file mode 100644 index 0000000000..c18c5b5e1f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.Tokens.InstanceView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex index 1bbd57d041..f5b1701aae 100644 --- a/apps/block_scout_web/lib/block_scout_web/web_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex @@ -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( diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 4d6f2cc136..477e5803b4 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -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 "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 4d6f2cc136..477e5803b4 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -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 "" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs index 6c0bfabfa0..a467c7c73c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs index def232741c..93777e16e3 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs @@ -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() diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs new file mode 100644 index 0000000000..10d8831331 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/instance_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs index 2cbfd2c0a5..817a3137d6 100644 --- a/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/helpers_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/views/tokens/instance/overview_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/tokens/instance/overview_view_test.exs new file mode 100644 index 0000000000..cf310bf266 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/tokens/instance/overview_view_test.exs @@ -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 diff --git a/apps/block_scout_web/test/support/conn_case.ex b/apps/block_scout_web/test/support/conn_case.ex index ed3f094c34..5540346d34 100644 --- a/apps/block_scout_web/test/support/conn_case.ex +++ b/apps/block_scout_web/test/support/conn_case.ex @@ -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 diff --git a/apps/block_scout_web/test/support/feature_case.ex b/apps/block_scout_web/test/support/feature_case.ex index f6b1aedf24..2dd1d0635c 100644 --- a/apps/block_scout_web/test/support/feature_case.ex +++ b/apps/block_scout_web/test/support/feature_case.ex @@ -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) diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 81d2ed2537..fcff7aeea5 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -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" diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index f7cc0614fd..5af1477551 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -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() diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index f89d933d6d..b83b3fe3e3 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -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,20 +1346,43 @@ 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 - case Blocks.take_enough(paging_options.page_size) do - nil -> - elements = fetch_blocks(block_type, paging_options, necessity_by_association) + cond do + block_type == "Block" && !paging_options.key -> + block_from_cache(block_type, paging_options, necessity_by_association) - Blocks.update(elements) + block_type == "Uncle" && !paging_options.key -> + uncles_from_cache(block_type, paging_options, necessity_by_association) - elements + true -> + fetch_blocks(block_type, paging_options, necessity_by_association) + end + end - blocks -> - blocks - end - else - fetch_blocks(block_type, paging_options, necessity_by_association) + 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) + + Blocks.update(elements) + + elements + + blocks -> + blocks + end + 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 diff --git a/apps/explorer/lib/explorer/chain/cache/pending_transactions.ex b/apps/explorer/lib/explorer/chain/cache/pending_transactions.ex new file mode 100644 index 0000000000..686b5e7372 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/pending_transactions.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/cache/uncles.ex b/apps/explorer/lib/explorer/chain/cache/uncles.ex new file mode 100644 index 0000000000..9afe0bf71b --- /dev/null +++ b/apps/explorer/lib/explorer/chain/cache/uncles.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex index 394e21cbaa..ff970faad6 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/blocks.ex @@ -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) diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex index 93e3bdb6b5..b4704d382b 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions_indexed_at_blocks.ex b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions_indexed_at_blocks.ex index 2726670a30..048a94942c 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/internal_transactions_indexed_at_blocks.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/internal_transactions_indexed_at_blocks.ex @@ -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 ) diff --git a/apps/explorer/lib/explorer/chain/token/instance.ex b/apps/explorer/lib/explorer/chain/token/instance.ex new file mode 100644 index 0000000000..0e704650a0 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/token/instance.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 619de87db2..216a01daf0 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -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 diff --git a/apps/explorer/lib/explorer/smart_contract/verifier.ex b/apps/explorer/lib/explorer/smart_contract/verifier.ex index 4c2daa680c..ed1b6532c5 100644 --- a/apps/explorer/lib/explorer/smart_contract/verifier.ex +++ b/apps/explorer/lib/explorer/smart_contract/verifier.ex @@ -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. diff --git a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex new file mode 100644 index 0000000000..a4cf460e74 --- /dev/null +++ b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex @@ -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 diff --git a/apps/explorer/priv/repo/migrations/20190905083522_create_token_instances.exs b/apps/explorer/priv/repo/migrations/20190905083522_create_token_instances.exs new file mode 100644 index 0000000000..7ebf9c12e9 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20190905083522_create_token_instances.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain/cache/pending_transactions_test.exs b/apps/explorer/test/explorer/chain/cache/pending_transactions_test.exs new file mode 100644 index 0000000000..26339bc3d7 --- /dev/null +++ b/apps/explorer/test/explorer/chain/cache/pending_transactions_test.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain/cache/uncles_test.exs b/apps/explorer/test/explorer/chain/cache/uncles_test.exs new file mode 100644 index 0000000000..c8984d34ea --- /dev/null +++ b/apps/explorer/test/explorer/chain/cache/uncles_test.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs index 66d62ae940..700106ffb5 100644 --- a/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/internal_transactions_test.exs @@ -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 + + 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) when is_list(changes_list) do - Multi.new() + 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()} diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 872a3bc5c0..c623ea2d4c 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -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") diff --git a/apps/explorer/test/explorer/smart_contract/verifier_test.exs b/apps/explorer/test/explorer/smart_contract/verifier_test.exs index fba0622964..088820beea 100644 --- a/apps/explorer/test/explorer/smart_contract/verifier_test.exs +++ b/apps/explorer/test/explorer/smart_contract/verifier_test.exs @@ -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) diff --git a/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs b/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs new file mode 100644 index 0000000000..459bb53b02 --- /dev/null +++ b/apps/explorer/test/explorer/token/instance_metadata_retriever_test.exs @@ -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 diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index e12dd24a82..90167e676d 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -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 diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index a6e07683a5..edad876a8b 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -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), diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 1b95d84086..7b51602ddf 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -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 diff --git a/apps/indexer/lib/indexer/fetcher/pending_transaction.ex b/apps/indexer/lib/indexer/fetcher/pending_transaction.ex index 176d8dc9b7..0a8389a991 100644 --- a/apps/indexer/lib/indexer/fetcher/pending_transaction.ex +++ b/apps/indexer/lib/indexer/fetcher/pending_transaction.ex @@ -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} -> diff --git a/apps/indexer/lib/indexer/fetcher/token_instance.ex b/apps/indexer/lib/indexer/fetcher/token_instance.ex new file mode 100644 index 0000000000..32d9ac0064 --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/token_instance.ex @@ -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 diff --git a/apps/indexer/lib/indexer/fetcher/uncle_block.ex b/apps/indexer/lib/indexer/fetcher/uncle_block.ex index e597d16ca2..03c6892964 100644 --- a/apps/indexer/lib/indexer/fetcher/uncle_block.ex +++ b/apps/indexer/lib/indexer/fetcher/uncle_block.ex @@ -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 diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 00fba8d428..a5ffec4833 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -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, diff --git a/apps/indexer/lib/indexer/temporary/uncles_without_index.ex b/apps/indexer/lib/indexer/temporary/uncles_without_index.ex index 948f26b60d..12f133ff1a 100644 --- a/apps/indexer/lib/indexer/temporary/uncles_without_index.ex +++ b/apps/indexer/lib/indexer/temporary/uncles_without_index.ex @@ -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) diff --git a/docs/env-variables.md b/docs/env-variables.md index 503689d091..88f4cfbc07 100644 --- a/docs/env-variables.md +++ b/docs/env-variables.md @@ -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 | | |