diff --git a/.dialyzer-ignore b/.dialyzer-ignore index 11bb3be729..53e3554c6e 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -4,4 +4,4 @@ apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex:400: Function timestamp_to_datetime/1 has no local return apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: Function microseconds_time/1 has no local return apps/explorer/lib/explorer/repo/prometheus_logger.ex:8: The call 'Elixir.System':convert_time_unit(__@1::any(),'native','microseconds') breaks the contract (integer(),time_unit() | 'native',time_unit() | 'native') -> integer() -apps/block_scout_web/lib/block_scout_web/views/layout_view.ex:162: The call 'Elixir.Poison.Parser':'parse!'(any(),#{'keys':='atoms!'}) will never return since the success typing is (binary() | maybe_improper_list(binary() | maybe_improper_list(any(),binary() | []) | byte(),binary() | []),[{atom(),_}]) -> 'false' | 'nil' | 'true' | binary() | ['false' | 'nil' | 'true' | binary() | [any()] | number() | map()] | number() | map() and the contract is (iodata(),'Elixir.Keyword':t()) -> t() \ No newline at end of file +apps/block_scout_web/lib/block_scout_web/views/layout_view.ex:174: The call 'Elixir.Poison.Parser':'parse!'(any(),#{'keys':='atoms!'}) will never return since the success typing is (binary() | maybe_improper_list(binary() | maybe_improper_list(any(),binary() | []) | byte(),binary() | []),[{atom(),_}]) -> 'false' | 'nil' | 'true' | binary() | ['false' | 'nil' | 'true' | binary() | [any()] | number() | map()] | number() | map() and the contract is (iodata(),'Elixir.Keyword':t()) -> t() \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 129593bfca..c80371aa74 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,9 +23,15 @@ - [#1920](https://github.com/poanetwork/blockscout/pull/1920) - fix: remove source code fields from list endpoint - [#1876](https://github.com/poanetwork/blockscout/pull/1876) - async calculate a count of blocks - [#1941](https://github.com/poanetwork/blockscout/pull/1941) - feat: add on demand fetching and stale attr to rpc +- [#1957](https://github.com/poanetwork/blockscout/pull/1957) - Calculate stakes ratio before insert pools +- [#1956](https://github.com/poanetwork/blockscout/pull/1956) - add logs tab to address +- [#1933](https://github.com/poanetwork/blockscout/pull/1933) - add eth_BlockNumber json rpc method +- [#1952](https://github.com/poanetwork/blockscout/pull/1952) - feat: exclude empty contracts by default +- [#1954](https://github.com/poanetwork/blockscout/pull/1954) - feat: use creation init on self destruct ### Fixes +- [#1944](https://github.com/poanetwork/blockscout/pull/1944) - fixed styles for token's dropdown. - [#1926](https://github.com/poanetwork/blockscout/pull/1926) - status label alignment - [#1829](https://github.com/poanetwork/blockscout/pull/1829) - Handle nil quantities in block decoding routine - [#1830](https://github.com/poanetwork/blockscout/pull/1830) - Make block size field nullable @@ -44,6 +50,8 @@ - [#1898](https://github.com/poanetwork/blockscout/pull/1898) - check if the constructor has arguments before verifying constructor arguments - [#1915](https://github.com/poanetwork/blockscout/pull/1915) - fallback to 2 latest evm versions - [#1937](https://github.com/poanetwork/blockscout/pull/1937) - Check the presence of overlap[i] object before retrieving properties from it +- [#1960](https://github.com/poanetwork/blockscout/pull/1960) - do not remove bold text in decompiled contacts +- [#1917](https://github.com/poanetwork/blockscout/pull/1917) - Force block refetch if transaction is re-collated in a different block ### Chore @@ -51,6 +59,8 @@ - [#1837](https://github.com/poanetwork/blockscout/pull/1837) - Add -f flag to clear_build.sh script delete static folder - [#1900](https://github.com/poanetwork/blockscout/pull/1900) - SUPPORTED_CHAINS ENV var - [#1892](https://github.com/poanetwork/blockscout/pull/1892) - Remove temporary worker modules +- [#1958](https://github.com/poanetwork/blockscout/pull/1958) - Default value for release link env var +- [#1975](https://github.com/poanetwork/blockscout/pull/1975) - add log index to transaction view ## 1.3.10-beta diff --git a/README.md b/README.md index 9755475dbe..1c86dfc34a 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,9 @@ BlockScout provides a comprehensive, easy-to-use interface for users to view, co Following is an overview of the project and instructions for [getting started](#getting-started). -Visit the [POA BlockScout forum](https://forum.poa.network/c/blockscout) or the [Gitter Channel](https://gitter.im/poanetwork/blockscout) to access additional information or post questions. +Visit the [POA BlockScout forum](https://forum.poa.network/c/blockscout) for additional deployment instructions, FAQs, troubleshooting, and other BlockScout related items. You can also post and answer questions here. + +You can also access the dev chatroom on our [Gitter Channel](https://gitter.im/poanetwork/blockscout). ## About BlockScout @@ -41,31 +43,16 @@ Currently available block explorers (i.e. Etherscan and Etherchain) are closed s ### Supported Projects -#### Hosted Chains - -* [POA Core Network](https://blockscout.com/poa/core) -* [POA Sokol Testnet](https://blockscout.com/poa/sokol) -* [xDai Chain](https://blockscout.com/poa/dai) -* [Ethereum Mainnet](https://blockscout.com/eth/mainnet) -* [Kovan Testnet](https://blockscout.com/eth/kovan) -* [Ropsten Testnet](https://blockscout.com/eth/ropsten) -* [Goerli Testnet](https://blockscout.com/eth/goerli) -* [Rinkeby Testnet](https://blockscout.com/eth/rinkeby) -* [Ethereum Classic](https://blockscout.com/etc/mainnet) -* [Aerum](https://blockscout.com/aerum/mainnet) -* [Callisto](https://blockscout.com/callisto/mainnet) -* [RSK](https://blockscout.com/rsk/mainnet) - -#### Additional Chains Utilizing BlockScout - -* [Oasis Labs](https://blockexplorer.oasiscloud.io/) -* [Fuse Network](https://explorer.fuse.io/) -* [ARTIS](https://explorer.sigma1.artis.network) -* [SafeChain](https://explorer.safechain.io) -* [SpringChain](https://explorer.springrole.com/) -* [PIRL](http://pirl.es/) -* [Petrichor](https://explorer.petrichor-dev.com/) -* [Ether-1](https://blocks.ether1.wattpool.net/) +| **Hosted Mainnets** | **Hosted Testnets** | **Additional Chains using BlockScout** | +|--------------------------------------------------------|-------------------------------------------------------|----------------------------------------------------| +| [Aerum](https://blockscout.com/aerum/mainnet) | [Goerli Testnet](https://blockscout.com/eth/goerli) | [ARTIS](https://explorer.sigma1.artis.network) | +| [Callisto](https://blockscout.com/callisto/mainnet) | [Kovan Testnet](https://blockscout.com/eth/kovan) | [Ether-1](https://blocks.ether1.wattpool.net/) | +| [Ethereum Classic](https://blockscout.com/etc/mainnet) | [POA Sokol Testnet](https://blockscout.com/poa/sokol) | [Fuse Network](https://explorer.fuse.io/) | +| [Ethereum Mainnet](https://blockscout.com/eth/mainnet) | [Rinkeby Testnet](https://blockscout.com/eth/rinkeby) | [Oasis Labs](https://blockexplorer.oasiscloud.io/) | +| [POA Core Network](https://blockscout.com/poa/core) | [Ropsten Testnet](https://blockscout.com/eth/ropsten) | [Petrichor](https://explorer.petrachor.com/) | +| [RSK](https://blockscout.com/rsk/mainnet) | | [PIRL](http://pirl.es/) | +| [xDai Chain](https://blockscout.com/poa/dai) | | [SafeChain](https://explorer.safechain.io) | +| | | [SpringChain](https://explorer.springrole.com/) | ### Visual Interface @@ -74,13 +61,24 @@ Interface for the POA network _updated 02/2019_ ![BlockScout Example](explorer_example_2_2019.gif) -## Getting Started -We use [Terraform](https://www.terraform.io/intro/getting-started/install.html) to build the correct infrastructure to run BlockScout. See [https://github.com/poanetwork/blockscout-terraform](https://github.com/poanetwork/blockscout-terraform) for details. +### Umbrella Project Organization -### Requirements +This repository is an [umbrella project](https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-projects.html). Each directory under `apps/` is a separate [Mix](https://hexdocs.pm/mix/Mix.html) project and [OTP application](https://hexdocs.pm/elixir/Application.html), but the projects can use each other as a dependency in their `mix.exs`. -The [development stack page](https://github.com/poanetwork/blockscout/wiki/Development-Stack) contains more information about these frameworks. +Each OTP application has a restricted domain. + +| Directory | OTP Application | Namespace | Purpose | +|:------------------------|:--------------------|:------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `apps/ethereum_jsonrpc` | `:ethereum_jsonrpc` | `EthereumJSONRPC` | Ethereum JSONRPC client. It is allowed to know `Explorer`'s param format, but it cannot directly depend on `:explorer` | +| `apps/explorer` | `:explorer` | `Explorer` | Storage for the indexed chain. Can read and write to the backing storage. MUST be able to boot in a read-only mode when run independently from `:indexer`, so cannot depend on `:indexer` as that would start `:indexer` indexing. | +| `apps/block_scout_web` | `:block_scout_web` | `BlockScoutWeb` | Phoenix interface to `:explorer`. The minimum interface to allow web access should go in `:block_scout_web`. Any business rules or interface not tied directly to `Phoenix` or `Plug` should go in `:explorer`. MUST be able to boot in a read-only mode when run independently from `:indexer`, so cannot depend on `:indexer` as that would start `:indexer` indexing. | +| `apps/indexer` | `:indexer` | `Indexer` | Uses `:ethereum_jsonrpc` to index chain and batch import data into `:explorer`. Any process, `Task`, or `GenServer` that automatically reads from the chain and writes to `:explorer` should be in `:indexer`. This restricts automatic writes to `:indexer` and read-only mode can be achieved by not running `:indexer`. | + + +## Getting Started + +### Requirements | Dependency | Mac | Linux | |-------------|-----|-------| @@ -96,121 +94,42 @@ The [development stack page](https://github.com/poanetwork/blockscout/wiki/Devel ### Build and Run - 1. Clone the repository. - `git clone https://github.com/poanetwork/blockscout` - - 2. Go to the explorer subdirectory. - `cd blockscout` - - 3. Set up default configurations. - `cp apps/explorer/config/dev.secret.exs.example apps/explorer/config/dev.secret.exs` - `cp apps/block_scout_web/config/dev.secret.exs.example apps/block_scout_web/config/dev.secret.exs` -
Linux: Update the database username and password configuration in `apps/explorer/config/dev.secret.exs` -
Mac: Remove the `username` and `password` fields from `apps/explorer/config/dev.secret.exs` -
Optional: Set up default configuration for testing. - `cp apps/explorer/config/test.secret.exs.example apps/explorer/config/test.secret.exs` - Example usage: Changing the default Postgres port from localhost:15432 if [Boxen](https://github.com/boxen/boxen) is installed. - - 4. Install dependencies. - `mix do deps.get, local.rebar --force, deps.compile, compile` - - 5. Create and migrate database. - `mix ecto.create && mix ecto.migrate` -
_Note:_ If you have run previously, drop the previous database - `mix do ecto.drop, ecto.create, ecto.migrate` - - 6. Install Node.js dependencies. - `cd apps/block_scout_web/assets && npm install; cd -` - `cd apps/explorer && npm install; cd -` +#### Playbook Deployment - 7. Update your JSON RPC Variant in `apps/explorer/config/dev.exs` and `apps/indexer/config/dev.exs`. - For `variant`, enter `ganache`, `geth`, `parity`, or `rsk` +We use [Ansible](https://docs.ansible.com/ansible/latest/index.html) & [Terraform](https://www.terraform.io/intro/getting-started/install.html) to build the correct infrastructure to run BlockScout. See [https://github.com/poanetwork/blockscout-terraform](https://github.com/poanetwork/blockscout-terraform) for details and instructions. - 8. Update your JSON RPC Endpoint in `apps/explorer/config/dev/` and `apps/indexer/config/dev/` - For the `variant` chosen in step 7, enter the correct information for the corresponding JSON RPC Endpoint in `parity.exs`, `geth.exs`, or `ganache.exs` +#### Manual Deployment - 9. Enable HTTPS in development. The Phoenix server only runs with HTTPS. - * `cd apps/block_scout_web` - * `mix phx.gen.cert blockscout blockscout.local; cd -` - * Add blockscout and blockscout.local to your `/etc/hosts` - ``` - 127.0.0.1 localhost blockscout blockscout.local - 255.255.255.255 broadcasthost - ::1 localhost blockscout blockscout.local - ``` - * If using Chrome, Enable `chrome://flags/#allow-insecure-localhost`. +See [Manual BlockScout Deployment](https://forum.poa.network/t/manual-blockscout-deployment/2458) for instructions. - 9. Run the Phoenix Server from the root directory of your application. - `mix phx.server` +#### Environment Variables -Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. +Our forum contains a [full list of BlockScout environment variables](https://forum.poa.network/t/faq-blockscout-environment-variables/1814). -_Additional runtime options:_ +#### Configuring EVM Chains -* Run Phoenix Server with IEx (Interactive Elixer) -`iex -S mix phx.server` +* **CSS:** Update the import instruction in `apps/block_scout_web/assets/css/theme/_variables.scss` to select a preset css file. This is reflected in the `production-${chain}` branch for each instance. For example, in the `production-xdai` branch, it is set to `@import "dai-variables"`. -* Run Phoenix Server with real time indexer -`iex -S mix phx.server` +* **ENV:** Update the [environment variables](https://forum.poa.network/t/faq-blockscout-environment-variables/1814) to match the chain specs. -### Automating Restarts +#### Automating Restarts By default `blockscout` does not restart if it crashes. To enable automated -restarts, set the environment variable `HEART_COMMAND` to whatever you run to -start `blockscout`. You can configure the heart beat timeout, which will change -how long it will wait before considering the application to be unresponsive. At -that point, it will kill the current blockscout and execute `HEART_COMMAND`. -By default a crash dump is not written unless you set `ERL_CRASH_DUMP_SECONDS` -to a positive or negative integer. See the documentation for -[heart](http://erlang.org/doc/man/heart.html) for more information. +restarts, set the environment variable `HEART_COMMAND` to whatever command you run to start `blockscout`. Configure the heart beat timeout to change how long it waits before considering the application unresponsive. At that point, it will kill the current blockscout instance and execute the `HEART_COMMAND`. By default a crash dump is not written unless you set `ERL_CRASH_DUMP_SECONDS` to a positive or negative integer. See the [heart](http://erlang.org/doc/man/heart.html) documentation for more information. -### Configuring Ethereum Classic and other EVM Chains -**Note: Most of these modifications will be consolidated into a single file in the future.** - - 1. Update the import file in `apps/block_scout_web/assets/css/theme/_variables.scss`. There are several preset css files for our supported chains which include Ethereum Classic, Ethereum Mainnet, Ropsten Testnet, Kovan Testnet, POA Core, and POA Sokol. To deploy Ethereum Classic, change the import to `ethereum_classic_variables`. - - 2. Update the logo file in `apps/block_scout_web/config/config.exs`. To deploy Ethereum Classic, change this file to `classic_ethereum_logo.svg`. - - 3. Update the `check_origin` configuration in `apps/block_scout_web/config/prod.exs`. This allows realtime events to occur on your endpoint. - - 4. Update the node configuration. You will need a full tracing node with WebSockets enabled. Make the changes in the following files (dev/prod): - - * `apps/explorer/config/dev/parity.exs` - * `apps/explorer/config/prod/parity.exs` - * `apps/indexer/config/dev/parity.exs` - * `apps/indexer/config/prod/parity.exs` - - 5. Update the dropdown menu in the main navigation `apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex` - - 6. Update the coin in `apps/explorer/config/config.exs`. This will pull relevant information from Coinmarketcap.com. - -### Umbrella Project Organization - -This repository is an [umbrella project](https://elixir-lang.org/getting-started/mix-otp/dependencies-and-umbrella-projects.html). Each directory under `apps/` is a separate [Mix](https://hexdocs.pm/mix/Mix.html) project and [OTP application](https://hexdocs.pm/elixir/Application.html), but the projects can use each other as a dependency in their `mix.exs`. - -Each OTP application has a restricted domain. - -| Directory | OTP Application | Namespace | Purpose | -|:------------------------|:--------------------|:------------------|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `apps/ethereum_jsonrpc` | `:ethereum_jsonrpc` | `EthereumJSONRPC` | Ethereum JSONRPC client. It is allowed to know `Explorer`'s param format, but it cannot directly depend on `:explorer` | -| `apps/explorer` | `:explorer` | `Explorer` | Storage for the indexed chain. Can read and write to the backing storage. MUST be able to boot in a read-only mode when run independently from `:indexer`, so cannot depend on `:indexer` as that would start `:indexer` indexing. | -| `apps/block_scout_web` | `:block_scout_web` | `BlockScoutWeb` | Phoenix interface to `:explorer`. The minimum interface to allow web access should go in `:block_scout_web`. Any business rules or interface not tied directly to `Phoenix` or `Plug` should go in `:explorer`. MUST be able to boot in a read-only mode when run independently from `:indexer`, so cannot depend on `:indexer` as that would start `:indexer` indexing. | -| `apps/indexer` | `:indexer` | `Indexer` | Uses `:ethereum_jsonrpc` to index chain and batch import data into `:explorer`. Any process, `Task`, or `GenServer` that automatically reads from the chain and writes to `:explorer` should be in `:indexer`. This restricts automatic writes to `:indexer` and read-only mode can be achieved by not running `:indexer`. | - - -### CircleCI Updates +#### CircleCI Updates To monitor build status, configure your local [CCMenu](http://ccmenu.org/) with the following url: [`https://circleci.com/gh/poanetwork/blockscout.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604`](https://circleci.com/gh/poanetwork/blockscout.cc.xml?circle-token=f8823a3d0090407c11f87028c73015a331dbf604) -### Testing +## Testing -#### Requirements +### Requirements * PhantomJS (for wallaby) -#### Running the tests +### Running the tests 1. Build the assets. `cd apps/block_scout_web/assets && npm run build; cd -` @@ -237,9 +156,9 @@ To monitor build status, configure your local [CCMenu](http://ccmenu.org/) with 8. Test the JavaScript code. `cd apps/block_scout_web/assets && npm run test; cd -` -##### Parity +#### Parity -###### Mox +##### Mox **This is the default setup. `mix coveralls.html --umbrella` will work on its own, but to be explicit, use the following setup**: @@ -249,7 +168,7 @@ export ETHEREUM_JSONRPC_WEB_SOCKET_CASE=EthereumJSONRPC.WebSocket.Case.Mox mix coveralls.html --umbrella --exclude no_parity ``` -###### HTTP / WebSocket +##### HTTP / WebSocket ```shell export ETHEREUM_JSONRPC_CASE=EthereumJSONRPC.Case.Parity.HTTPWebSocket @@ -262,9 +181,9 @@ mix coveralls.html --umbrella --exclude no_parity | HTTP | `http://localhost:8545` | | WebSocket | `ws://localhost:8546` | -##### Geth +#### Geth -###### Mox +##### Mox ```shell export ETHEREUM_JSONRPC_CASE=EthereumJSONRPC.Case.Geth.Mox @@ -272,7 +191,7 @@ export ETHEREUM_JSONRPC_WEB_SOCKET_CASE=EthereumJSONRPC.WebSocket.Case.Mox mix coveralls.html --umbrella --exclude no_geth ``` -###### HTTP / WebSocket +##### HTTP / WebSocket ```shell export ETHEREUM_JSONRPC_CASE=EthereumJSONRPC.Case.Geth.HTTPWebSocket diff --git a/apps/block_scout_web/assets/css/components/_card.scss b/apps/block_scout_web/assets/css/components/_card.scss index 4c34e4a31b..6e1b6a9725 100644 --- a/apps/block_scout_web/assets/css/components/_card.scss +++ b/apps/block_scout_web/assets/css/components/_card.scss @@ -18,8 +18,8 @@ $card-background-1-text-color: #fff !default; background-color: $card-background-1; color: $card-background-1-text-color; - a, - a:hover { + a:not(.dropdown-item), + a:not(.dropdown-item):hover { color: $card-background-1-text-color; } } diff --git a/apps/block_scout_web/assets/css/components/_dropdown.scss b/apps/block_scout_web/assets/css/components/_dropdown.scss index b603d53fea..c8962c021d 100644 --- a/apps/block_scout_web/assets/css/components/_dropdown.scss +++ b/apps/block_scout_web/assets/css/components/_dropdown.scss @@ -60,9 +60,9 @@ .dropdown-search-icon { color: $gray-300; - left: 0.625rem; + left: 8px; pointer-events: none; - top: 0.5rem; + top: 5px; } .dropdown-search-field { diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index cabac4ae02..9e82c30023 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -205,8 +205,12 @@ defmodule BlockScoutWeb.Chain do %{"block_number" => block_number, "transaction_index" => transaction_index, "index" => index} end - defp paging_params(%Log{index: index}) do - %{"index" => index} + defp paging_params(%Log{index: index} = log) do + if Ecto.assoc_loaded?(log.transaction) do + %{"block_number" => log.transaction.block_number, "transaction_index" => log.transaction.index, "index" => index} + else + %{"index" => index} + end end defp paging_params(%Transaction{block_number: nil, inserted_at: inserted_at, hash: hash}) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex new file mode 100644 index 0000000000..f79d9aa08d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex @@ -0,0 +1,46 @@ +defmodule BlockScoutWeb.AddressLogsController do + @moduledoc """ + Manages events logs tab. + """ + + import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] + import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + alias Explorer.{Chain, Market} + alias Explorer.ExchangeRates.Token + alias Indexer.Fetcher.CoinBalanceOnDemand + + use BlockScoutWeb, :controller + + def index(conn, %{"address_id" => address_hash_string} = params) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.hash_to_address(address_hash) do + logs_plus_one = Chain.address_to_logs(address, paging_options(params)) + {results, next_page} = split_list_by_page(logs_plus_one) + + next_page_url = + case next_page_params(next_page, results, params) do + nil -> + nil + + next_page_params -> + address_logs_path(conn, :index, address, next_page_params) + end + + render( + conn, + "index.html", + address: address, + logs: results, + coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + transaction_count: transaction_count(address), + validation_count: validation_count(address), + next_page_url: next_page_url + ) + else + _ -> + not_found(conn) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex index e6991bf1af..8beb8ea8ea 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex @@ -3,6 +3,7 @@ defmodule BlockScoutWeb.API.RPC.BlockController do alias BlockScoutWeb.Chain, as: ChainWeb alias Explorer.Chain + alias Explorer.Chain.BlockNumberCache def getblockreward(conn, params) do with {:block_param, {:ok, unsafe_block_number}} <- {:block_param, Map.fetch(params, "blockno")}, @@ -23,4 +24,11 @@ defmodule BlockScoutWeb.API.RPC.BlockController do render(conn, :error, error: "Block does not exist") end end + + def eth_block_number(conn, params) do + id = Map.get(params, "id", 1) + max_block_number = BlockNumberCache.max_number() + + render(conn, :eth_block_number, number: max_block_number, id: id) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex index 886bbcd575..23dacd45da 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex @@ -103,6 +103,9 @@ defmodule BlockScoutWeb.API.RPC.ContractController do :not_decompiled -> Chain.list_not_decompiled_contracts(page_size, offset) + :empty -> + Chain.list_empty_contracts(page_size, offset) + _ -> Chain.list_contracts(page_size, offset) end @@ -140,10 +143,12 @@ defmodule BlockScoutWeb.API.RPC.ContractController do defp contracts_filter(2), do: {:ok, :decompiled} defp contracts_filter(3), do: {:ok, :unverified} defp contracts_filter(4), do: {:ok, :not_decompiled} + defp contracts_filter(5), do: {:ok, :empty} defp contracts_filter("verified"), do: {:ok, :verified} defp contracts_filter("decompiled"), do: {:ok, :decompiled} defp contracts_filter("unverified"), do: {:ok, :unverified} defp contracts_filter("not_decompiled"), do: {:ok, :not_decompiled} + defp contracts_filter("empty"), do: {:ok, :empty} defp contracts_filter(filter) when is_bitstring(filter) do case Integer.parse(filter) do diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 3621a12d10..3167689f3c 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -279,6 +279,12 @@ defmodule BlockScoutWeb.Etherscan do "result" => nil } + @block_eth_block_number_example_value %{ + "jsonrpc" => "2.0", + "result" => "767969", + "id" => 1 + } + @contract_listcontracts_example_value %{ "status" => "1", "message" => "OK", @@ -476,11 +482,26 @@ defmodule BlockScoutWeb.Etherscan do enum_interpretation: %{"0" => "error", "1" => "ok"} } + @jsonrpc_version_type %{ + type: "string", + example: ~s("2.0") + } + @message_type %{ type: "string", example: ~s("OK") } + @hex_number_type %{ + type: "string", + example: ~s("767969") + } + + @id_type %{ + type: "string", + example: ~s("1") + } + @wei_type %{ type: "wei", definition: &__MODULE__.wei_type_definition/1, @@ -573,6 +594,11 @@ defmodule BlockScoutWeb.Etherscan do type: "block number", definition: "A nonnegative number used to identify blocks.", example: ~s("0x5c958") + }, + index: %{ + type: "log index", + definition: "A nonnegative number used to identify logs.", + example: ~s("1") } } } @@ -1737,6 +1763,35 @@ defmodule BlockScoutWeb.Etherscan do ] } + @block_eth_block_number_action %{ + name: "eth_block_number", + description: "Mimics Ethereum JSON RPC's eth_blockNumber. Returns the lastest block number", + required_params: [], + optional_params: [ + %{ + key: "id", + placeholder: "request id", + type: "integer", + description: "A nonnegative integer that represents the json rpc request id." + } + ], + responses: [ + %{ + code: "200", + description: "successful request", + example_value: Jason.encode!(@block_eth_block_number_example_value), + model: %{ + name: "Result", + fields: %{ + jsonrpc: @jsonrpc_version_type, + id: @id_type, + result: @hex_number_type + } + } + } + ] + } + @block_getblockreward_action %{ name: "getblockreward", description: "Get block reward by block number.", @@ -1795,7 +1850,7 @@ defmodule BlockScoutWeb.Etherscan do key: "filter", type: "string", description: - "verified|decompiled|unverified|not_decompiled, or 1|2|3|4 respectively. This requests only contracts with that status." + "verified|decompiled|unverified|not_decompiled|empty, or 1|2|3|4|5 respectively. This requests only contracts with that status." }, %{ key: "not_decompiled_with_version", @@ -2171,7 +2226,7 @@ defmodule BlockScoutWeb.Etherscan do @block_module %{ name: "block", - actions: [@block_getblockreward_action] + actions: [@block_getblockreward_action, @block_eth_block_number_action] } @contract_module %{ diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index c0e975793f..b2f639d763 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -147,6 +147,13 @@ defmodule BlockScoutWeb.Router do as: :decompiled_contract ) + resources( + "/logs", + AddressLogsController, + only: [:index], + as: :logs + ) + resources( "/contract_verifications", AddressContractVerificationController, diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex index bbf27c8f96..8ed8d4e028 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex @@ -22,6 +22,11 @@ "data-test": "coin_balance_tab_link", to: address_coin_balance_path(@conn, :index, @address.hash) ) %> + <%= link( + gettext("Logs"), + class: "card-tab #{tab_status("logs", @conn.request_path)}", + to: address_logs_path(@conn, :index, @address.hash) + ) %> <%= if BlockScoutWeb.AddressView.validator?(@validation_count) do %> <%= link( gettext("Blocks Validated"), @@ -55,4 +60,4 @@ class: "card-tab #{tab_status("read_contract", @conn.request_path)}") %> <% end %> - \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex index 560440fb4e..b7b2282682 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex @@ -1,18 +1,25 @@ +<% contract_creation_code = contract_creation_code(@address) %> +
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
- <%= if !BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %> - <%= link( - gettext("Verify & Publish"), - to: address_verify_contract_path(@conn, :new, @address.hash), - class: "button button-primary button-sm float-right ml-3", - "data-test": "verify_and_publish" - ) %> + <%= if match?({:selfdestructed, _}, contract_creation_code) do %> +
+ <%= gettext("Verify & Publish") %> +
+ <% else %> + <%= if !BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %> + <%= link( + gettext("Verify & Publish"), + to: address_verify_contract_path(@conn, :new, @address.hash), + class: "button button-primary button-sm float-right ml-3", + "data-test": "verify_and_publish" + ) %> + <% end %> <% end %> - <%= if BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %>
@@ -56,15 +63,32 @@ <% end %>
-
-

<%= gettext "Contract creation code" %>

- -
-
-
<%= @address.contract_code %>
-
+ <%= case contract_creation_code do %> + <% {:selfdestructed, transaction_init} -> %> +
+

<%= gettext "Contract Creation Code" %>

+ +
+
+

<%= gettext "Contracts that self destruct in their constructors have no contract code published and cannot be verified." %>

+

<%= gettext "Displaying the init data provided of the creating transaction." %>

+
+
+
<%= transaction_init %>
+
+ <% {:ok, contract_code} -> %> +
+

<%= gettext "Contract Byte Code" %>

+ +
+
+
<%= contract_code %>
+
+ <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex new file mode 100644 index 0000000000..22f446924f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex @@ -0,0 +1,82 @@ +
+ <%= render BlockScoutWeb.AddressView, "overview.html", assigns %> +
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> + +
+ +

<%= gettext "Logs" %>

+ + <%= if @next_page_url do %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true, next_page_path: @next_page_url %> + <% end %> + + <%= if !@next_page_url do %> + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: "1", show_pagination_limit: true %> + <% end %> + + <%= if Enum.count(@logs) > 0 do %> + <%= for log <- @logs do %> +
+
+
<%= gettext "Transaction" %>
+
+

+ <%= link( + log.transaction, + to: transaction_path(@conn, :show, log.transaction), + "data-test": "log_address_link", + "data-address-hash": log.transaction + ) %> +

+
+
<%= gettext "Topics" %>
+
+
+ <%= unless is_nil(log.first_topic) do %> +
+ [0] + <%= log.first_topic %> +
+ <% end %> + <%= unless is_nil(log.second_topic) do %> +
+ [1] + <%= log.second_topic %> +
+ <% end %> + <%= unless is_nil(log.third_topic) do %> +
+ [2] + <%= log.third_topic %> +
+ <% end %> + <%= unless is_nil(log.fourth_topic) do %> +
+ [3] + <%= log.fourth_topic %> +
+ <% end %> +
+
+
+ <%= gettext "Data" %> +
+
+ <%= unless is_nil(log.data) do %> +
+ <%= log.data %> +
+ <% end %> +
+
+
+ <% end %> + <% else %> +
+ <%= gettext "There are no logs for this address." %> +
+ <% end %> +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex index 6f1dc762fd..de0215727c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex @@ -1,6 +1,6 @@ <%= if Enum.any?(@token_balances) do %> - >}, + contracts_creation_internal_transaction: %InternalTransaction{init: init} + }) do + {:selfdestructed, init} + end + + def contract_creation_code(%Address{contract_code: contract_code}) do + {:ok, contract_code} + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex index f80a0ded99..b92d356479 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex @@ -165,7 +165,10 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do |> String.replace("\e[1m", "") |> String.replace("ยป", "»") |> String.replace("\e[0m", "") - |> String.split(~r/\|\<\/span\>/, include_captures: true, trim: true) + |> String.split(~r/\|\|\<\/span\>/, + include_captures: true, + trim: true + ) |> add_styles_to_every_line() result diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_logs_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_logs_view.ex new file mode 100644 index 0000000000..7155e65206 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/address_logs_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.AddressLogsView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index 6ce72b90ae..7a6005d526 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -300,6 +300,7 @@ defmodule BlockScoutWeb.AddressView do defp tab_name(["read_contract"]), do: gettext("Read Contract") defp tab_name(["coin_balances"]), do: gettext("Coin Balance History") defp tab_name(["validations"]), do: gettext("Blocks Validated") + defp tab_name(["logs"]), do: gettext("Logs") def short_hash(%Address{hash: hash}) do << diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex index 941a064804..5c98794b2a 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.API.RPC.BlockView do use BlockScoutWeb, :view - alias BlockScoutWeb.API.RPC.RPCView + alias BlockScoutWeb.API.RPC.{EthRPCView, RPCView} alias Explorer.Chain.{Hash, Wei} def render("block_reward.json", %{block: block, reward: reward}) do @@ -22,7 +22,33 @@ defmodule BlockScoutWeb.API.RPC.BlockView do RPCView.render("show.json", data: data) end + def render("eth_block_number.json", %{number: number, id: id}) do + result = encode_quantity(number) + + EthRPCView.render("show.json", %{result: result, id: id}) + end + def render("error.json", %{error: error}) do RPCView.render("error.json", error: error) end + + defp encode_quantity(binary) when is_binary(binary) do + hex_binary = Base.encode16(binary, case: :lower) + + result = String.replace_leading(hex_binary, "0", "") + + final_result = if result == "", do: "0", else: result + + "0x#{final_result}" + end + + defp encode_quantity(value) when is_integer(value) do + value + |> :binary.encode_unsigned() + |> encode_quantity() + end + + defp encode_quantity(value) when is_nil(value) do + nil + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex new file mode 100644 index 0000000000..39eb5ae9d1 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex @@ -0,0 +1,33 @@ +defmodule BlockScoutWeb.API.RPC.EthRPCView do + use BlockScoutWeb, :view + + defstruct [:result, :id, :error] + + def render("show.json", %{result: result, id: id}) do + %__MODULE__{ + result: result, + id: id + } + end + + def render("error.json", %{error: message, id: id}) do + %__MODULE__{ + error: message, + id: id + } + end + + defimpl Poison.Encoder, for: BlockScoutWeb.API.RPC.EthRPCView do + def encode(%BlockScoutWeb.API.RPC.EthRPCView{result: result, id: id, error: error}, _options) when is_nil(error) do + """ + {"jsonrpc":"2.0","result":"#{result}","id":#{id}} + """ + end + + def encode(%BlockScoutWeb.API.RPC.EthRPCView{id: id, error: error}, _options) do + """ + {"jsonrpc":"2.0","error": #{error},"id": #{id}} + """ + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex index 459272df07..e792be8f40 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex @@ -77,7 +77,8 @@ defmodule BlockScoutWeb.API.RPC.TransactionView do %{ "address" => "#{log.address_hash}", "topics" => get_topics(log), - "data" => "#{log.data}" + "data" => "#{log.data}", + "index" => "#{log.index}" } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex index 7a454a0e72..12df6d3f0a 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex @@ -142,10 +142,22 @@ defmodule BlockScoutWeb.LayoutView do end def release_link(version) do - release_link = Application.get_env(:block_scout_web, :release_link) + release_link_env_var = Application.get_env(:block_scout_web, :release_link) - if release_link == "" || release_link == nil do - version + release_link = + cond do + version == "" || version == nil -> + nil + + release_link_env_var == "" || release_link_env_var == nil -> + "https://github.com/poanetwork/blockscout/releases/tag/" <> version + + true -> + release_link_env_var + end + + if release_link == nil do + "" else html_escape({:safe, "#{version}"}) end diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 88638971ea..9d2737ba22 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -187,7 +187,7 @@ msgid "Blocks Indexed" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:27 +#: lib/block_scout_web/templates/address/_tabs.html.eex:32 #: lib/block_scout_web/templates/address/overview.html.eex:95 #: lib/block_scout_web/templates/address_validation/index.html.eex:13 #: lib/block_scout_web/views/address_view.ex:302 @@ -215,7 +215,7 @@ msgid "Close" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:37 +#: lib/block_scout_web/templates/address/_tabs.html.eex:42 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/views/address_view.ex:298 @@ -228,7 +228,7 @@ msgid "Compiler" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:23 +#: lib/block_scout_web/templates/address_contract/index.html.eex:30 msgid "Compiler version" msgstr "" @@ -256,7 +256,7 @@ msgid "Connection Lost, click to load newer validations" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:46 +#: lib/block_scout_web/templates/address_contract/index.html.eex:53 msgid "Contract ABI" msgstr "" @@ -289,17 +289,12 @@ msgid "Contract Name" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:60 -msgid "Contract creation code" -msgstr "" - -#, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:19 +#: lib/block_scout_web/templates/address_contract/index.html.eex:26 msgid "Contract name:" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:34 +#: lib/block_scout_web/templates/address_contract/index.html.eex:41 msgid "Contract source code" msgstr "" @@ -332,6 +327,7 @@ msgid "Curl" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:63 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:18 #: lib/block_scout_web/templates/transaction_log/index.html.eex:67 #: lib/block_scout_web/templates/transaction_log/index.html.eex:133 @@ -503,8 +499,11 @@ msgid "Limit" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address/_tabs.html.eex:26 +#: lib/block_scout_web/templates/address_logs/index.html.eex:8 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 +#: lib/block_scout_web/views/address_view.ex:303 #: lib/block_scout_web/views/transaction_view.ex:340 msgid "Logs" msgstr "" @@ -619,7 +618,7 @@ msgid "Older" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:34 msgid "Optimization enabled" msgstr "" @@ -680,7 +679,7 @@ msgid "Query" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:53 +#: lib/block_scout_web/templates/address/_tabs.html.eex:58 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:25 #: lib/block_scout_web/views/address_view.ex:300 #: lib/block_scout_web/views/tokens/overview_view.ex:37 @@ -872,6 +871,7 @@ msgid "Top Accounts - %{subnetwork} Explorer" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:33 #: lib/block_scout_web/templates/transaction_log/index.html.eex:103 msgid "Topics" msgstr "" @@ -892,6 +892,7 @@ msgid "Total transactions" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:22 #: lib/block_scout_web/views/transaction_view.ex:287 msgid "Transaction" msgstr "" @@ -979,7 +980,8 @@ msgid "Value" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:9 +#: lib/block_scout_web/templates/address_contract/index.html.eex:11 +#: lib/block_scout_web/templates/address_contract/index.html.eex:16 msgid "Verify & Publish" msgstr "" @@ -1044,7 +1046,7 @@ msgid "at" msgstr "" #, elixir-format -#: lib/block_scout_web/views/address_contract_view.ex:20 +#: lib/block_scout_web/views/address_contract_view.ex:22 msgid "false" msgstr "" @@ -1062,7 +1064,7 @@ msgid "string" msgstr "" #, elixir-format -#: lib/block_scout_web/views/address_contract_view.ex:19 +#: lib/block_scout_web/views/address_contract_view.ex:21 msgid "true" msgstr "" @@ -1466,17 +1468,17 @@ msgid "Support" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:48 +#: lib/block_scout_web/templates/address_contract/index.html.eex:55 msgid "Copy ABI" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:62 +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 msgid "Copy Contract Creation Code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:36 +#: lib/block_scout_web/templates/address_contract/index.html.eex:43 msgid "Copy Source Code" msgstr "" @@ -1618,7 +1620,7 @@ msgid "Decompiled Code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:47 +#: lib/block_scout_web/templates/address/_tabs.html.eex:52 msgid "Decompiled code" msgstr "" @@ -1702,3 +1704,33 @@ msgstr "" #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:37 msgid "of" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_contract/index.html.eex:83 +msgid "Contract Byte Code" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_contract/index.html.eex:69 +msgid "Contract Creation Code" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_contract/index.html.eex:75 +msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_contract/index.html.eex:85 +msgid "Copy Contract Byte Code" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 +msgid "Displaying the init data provided of the creating transaction." +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:77 +msgid "There are no logs for this address." +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 8925a95acf..1dd2465781 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 @@ msgid "Blocks Indexed" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:27 +#: lib/block_scout_web/templates/address/_tabs.html.eex:32 #: lib/block_scout_web/templates/address/overview.html.eex:95 #: lib/block_scout_web/templates/address_validation/index.html.eex:13 #: lib/block_scout_web/views/address_view.ex:302 @@ -215,7 +215,7 @@ msgid "Close" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:37 +#: lib/block_scout_web/templates/address/_tabs.html.eex:42 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:165 #: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187 #: lib/block_scout_web/views/address_view.ex:298 @@ -228,7 +228,7 @@ msgid "Compiler" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:23 +#: lib/block_scout_web/templates/address_contract/index.html.eex:30 msgid "Compiler version" msgstr "" @@ -256,7 +256,7 @@ msgid "Connection Lost, click to load newer validations" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:46 +#: lib/block_scout_web/templates/address_contract/index.html.eex:53 msgid "Contract ABI" msgstr "" @@ -289,17 +289,12 @@ msgid "Contract Name" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:60 -msgid "Contract creation code" -msgstr "" - -#, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:19 +#: lib/block_scout_web/templates/address_contract/index.html.eex:26 msgid "Contract name:" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:34 +#: lib/block_scout_web/templates/address_contract/index.html.eex:41 msgid "Contract source code" msgstr "" @@ -334,6 +329,7 @@ msgid "Curl" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:63 #: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:18 #: lib/block_scout_web/templates/transaction_log/index.html.eex:67 #: lib/block_scout_web/templates/transaction_log/index.html.eex:133 @@ -505,8 +501,11 @@ msgid "Limit" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address/_tabs.html.eex:26 +#: lib/block_scout_web/templates/address_logs/index.html.eex:8 #: lib/block_scout_web/templates/transaction/_tabs.html.eex:17 #: lib/block_scout_web/templates/transaction_log/index.html.eex:8 +#: lib/block_scout_web/views/address_view.ex:303 #: lib/block_scout_web/views/transaction_view.ex:340 msgid "Logs" msgstr "" @@ -621,7 +620,7 @@ msgid "Older" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:27 +#: lib/block_scout_web/templates/address_contract/index.html.eex:34 msgid "Optimization enabled" msgstr "" @@ -682,7 +681,7 @@ msgid "Query" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:53 +#: lib/block_scout_web/templates/address/_tabs.html.eex:58 #: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:25 #: lib/block_scout_web/views/address_view.ex:300 #: lib/block_scout_web/views/tokens/overview_view.ex:37 @@ -874,6 +873,7 @@ msgid "Top Accounts - %{subnetwork} Explorer" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:33 #: lib/block_scout_web/templates/transaction_log/index.html.eex:103 msgid "Topics" msgstr "" @@ -894,6 +894,7 @@ msgid "Total transactions" msgstr "" #, elixir-format +#: lib/block_scout_web/templates/address_logs/index.html.eex:22 #: lib/block_scout_web/views/transaction_view.ex:287 msgid "Transaction" msgstr "" @@ -982,7 +983,8 @@ msgid "Value" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:9 +#: lib/block_scout_web/templates/address_contract/index.html.eex:11 +#: lib/block_scout_web/templates/address_contract/index.html.eex:16 msgid "Verify & Publish" msgstr "" @@ -1047,7 +1049,7 @@ msgid "at" msgstr "" #, elixir-format -#: lib/block_scout_web/views/address_contract_view.ex:20 +#: lib/block_scout_web/views/address_contract_view.ex:22 msgid "false" msgstr "" @@ -1065,7 +1067,7 @@ msgid "string" msgstr "" #, elixir-format -#: lib/block_scout_web/views/address_contract_view.ex:19 +#: lib/block_scout_web/views/address_contract_view.ex:21 msgid "true" msgstr "" @@ -1357,7 +1359,7 @@ msgid "Loading balances" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:23 +#: 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 "" @@ -1368,7 +1370,7 @@ msgid "There is no coin history for this address." msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:26 +#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:25 #: lib/block_scout_web/templates/chain/show.html.eex:16 msgid "There was a problem loading the chart." msgstr "" @@ -1509,17 +1511,17 @@ msgid "Support" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:48 +#: lib/block_scout_web/templates/address_contract/index.html.eex:55 msgid "Copy ABI" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:62 +#: lib/block_scout_web/templates/address_contract/index.html.eex:71 msgid "Copy Contract Creation Code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_contract/index.html.eex:36 +#: lib/block_scout_web/templates/address_contract/index.html.eex:43 msgid "Copy Source Code" msgstr "" @@ -1661,7 +1663,7 @@ msgid "Decompiled Code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/_tabs.html.eex:47 +#: lib/block_scout_web/templates/address/_tabs.html.eex:52 msgid "Decompiled code" msgstr "" @@ -1745,3 +1747,33 @@ msgstr "" #: lib/block_scout_web/templates/common_components/_pagination_container.html.eex:37 msgid "of" msgstr "" + +#, elixir-format, fuzzy +#: lib/block_scout_web/templates/address_contract/index.html.eex:83 +msgid "Contract Byte Code" +msgstr "" + +#, elixir-format, fuzzy +#: lib/block_scout_web/templates/address_contract/index.html.eex:69 +msgid "Contract Creation Code" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_contract/index.html.eex:75 +msgid "Contracts that self destruct in their constructors have no contract code published and cannot be verified." +msgstr "" + +#, elixir-format, fuzzy +#: lib/block_scout_web/templates/address_contract/index.html.eex:85 +msgid "Copy Contract Byte Code" +msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_contract/index.html.eex:76 +msgid "Displaying the init data provided of the creating transaction." +msgstr "" + +#, elixir-format, fuzzy +#: lib/block_scout_web/templates/address_logs/index.html.eex:77 +msgid "There are no logs for this address." +msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs index b3b8f0b08b..993b25fcec 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs @@ -25,7 +25,6 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do start_supervised!(AddressesWithBalanceCounter) Application.put_env(:explorer, AverageBlockTime, enabled: true) - BlockNumberCache.setup(cache_period: 0) on_exit(fn -> Application.put_env(:explorer, AverageBlockTime, enabled: false) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index 43169a65d2..399e49bfff 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -100,6 +100,34 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do ] end + test "filtering for only unverified contracts does not show self destructed contracts", %{ + params: params, + conn: conn + } do + address = insert(:contract_address) + insert(:smart_contract) + insert(:contract_address, contract_code: "0x") + + response = + conn + |> get("/api", Map.put(params, "filter", "unverified")) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"] == [ + %{ + "ABI" => "Contract source code not verified", + "Address" => to_string(address.hash), + "CompilerVersion" => "", + "ContractName" => "", + "DecompilerVersion" => "", + "OptimizationUsed" => "" + } + ] + end + test "filtering for only verified contracts shows only verified contracts", %{params: params, conn: conn} do insert(:contract_address) contract = insert(:smart_contract) @@ -222,6 +250,35 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do } ] end + + test "filtering for only not_decompiled (and by extension not verified contracts) does not show empty contracts", %{ + params: params, + conn: conn + } do + insert(:decompiled_smart_contract) + insert(:smart_contract) + insert(:contract_address, contract_code: "0x") + contract_address = insert(:contract_address) + + response = + conn + |> get("/api", Map.put(params, "filter", "not_decompiled")) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"] == [ + %{ + "ABI" => "Contract source code not verified", + "Address" => to_string(contract_address.hash), + "CompilerVersion" => "", + "ContractName" => "", + "DecompilerVersion" => "", + "OptimizationUsed" => "" + } + ] + end end describe "getabi" do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs index 92a2f969ab..9d151e382c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs @@ -460,7 +460,8 @@ defmodule BlockScoutWeb.API.RPC.TransactionControllerTest do %{ "address" => "#{address.hash}", "data" => "#{log.data}", - "topics" => ["first topic", "second topic", nil, nil] + "topics" => ["first topic", "second topic", nil, nil], + "index" => "#{log.index}" } ], "next_page_params" => nil diff --git a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs index 8f1636ba06..334ea36baa 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs @@ -56,7 +56,7 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do result = AddressDecompiledContractView.highlight_decompiled_code(code) assert result == - " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n #\n # I failed with these:\n # - unknowne77c646d(?)\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)\n # All the rest is below.\n #\n\n\n # Storage definitions and getters\n\n def storage:\n allowance is uint256 => uint256 # mask(256, 0) at storage #2\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4\n\n def allowance(address _owner, address _spender) payable: 64\n return allowance[_owner_spender(320 - 1))]\n\n\n #\n # Regular functions - see Tutorial for understanding quirks of the code\n #\n\n\n # folder failed in this function - may be terribly long, sorry\n def unknownc47d033b(?) payable: not cd[4]:\n revert\n else:\n mem[0]cd[4]\n mem[32] = 4\n mem[96] = bool(stor4[cd[4])])\n return bool(stor4[cd[4])])\n\n def _fallback() payable: # default function\n revert\n\n\n" + " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n #\n # I failed with these:\n # - unknowne77c646d(?)\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)\n # All the rest is below.\n #\n\n\n # Storage definitions and getters\n\n def storage:\n allowance is uint256 => uint256 # mask(256, 0) at storage #2\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4\n\n def allowance(address _owner, address _spender) payable: \n require (calldata.size - 4) >= 64\n return allowance[sha3(((320 - 1) and (320 - 1) and _owner), 1), ((320 - 1) and _spender and (320 - 1))]\n\n\n #\n # Regular functions - see Tutorial for understanding quirks of the code\n #\n\n\n # folder failed in this function - may be terribly long, sorry\n def unknownc47d033b(?) payable: \n if (calldata.size - 4) < 32:\n revert\n else:\n if not (320 - 1) or not cd[4]:\n revert\n else:\n mem[0] = (320 - 1) and (320 - 1) and cd[4]\n mem[32] = 4\n mem[96] = bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n return bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n\n def _fallback() payable: # default function\n revert\n\n\n" end test "adds style span to every line" do @@ -72,6 +72,28 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do assert AddressDecompiledContractView.highlight_decompiled_code(code) == " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n\n\n" end + + test "does not remove bold text" do + code = """ + # + # Eveem.org 26 Apr 2019 + # Decompiled source of 0x06012c8cf97bead5deae237070f9587f8e7a266d + # + # Let's make the world open source + #  + + const name = 'CryptoKitties' + const symbol = 'CK' + const GEN0_STARTING_PRICE = 10^16 + const GEN0_AUCTION_DURATION = (24 * 3600) + const GEN0_CREATION_LIMIT = 45000 + const PROMO_CREATION_LIMIT = 5000 + + """ + + assert AddressDecompiledContractView.highlight_decompiled_code(code) == + "#\n# Eveem.org 26 Apr 2019\n# Decompiled source of 0x06012c8cf97bead5deae237070f9587f8e7a266d\n#\n# Let's make the world open source\n# \n\nconst name = 'CryptoKitties'\nconst symbol = 'CK'\nconst GEN0_STARTING_PRICE = 10^16\nconst GEN0_AUCTION_DURATION = (24 * 3600)\nconst GEN0_CREATION_LIMIT = 45000\nconst PROMO_CREATION_LIMIT = 5000\n\n\n\n" + end end describe "sort_contracts_by_version/1" do diff --git a/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs index 5e3302ceec..5787159b14 100644 --- a/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs @@ -62,16 +62,32 @@ defmodule BlockScoutWeb.LayoutViewTest do end describe "release_link/1" do - test "use the version when there is no release_link env configured for it" do + test "set empty string if no blockscout version configured" do + Application.put_env(:block_scout_web, :blockscout_version, nil) + + assert LayoutView.release_link(nil) == "" + end + + test "set empty string if blockscout version is empty string" do + Application.put_env(:block_scout_web, :blockscout_version, "") + + assert LayoutView.release_link("") == "" + end + + test "use the default value when there is no release_link env configured for it" do Application.put_env(:block_scout_web, :release_link, nil) - assert LayoutView.release_link("1.3.4") == "1.3.4" + assert LayoutView.release_link("v1.3.4-beta") == + {:safe, + ~s(v1.3.4-beta)} end - test "use the version when empty release_link env configured for it" do + test "use the default value when empty release_link env configured for it" do Application.put_env(:block_scout_web, :release_link, "") - assert LayoutView.release_link("1.3.4") == "1.3.4" + assert LayoutView.release_link("v1.3.4-beta") == + {:safe, + ~s(v1.3.4-beta)} end test "use the enviroment release link when it's configured" do @@ -81,9 +97,9 @@ defmodule BlockScoutWeb.LayoutViewTest do "https://github.com/poanetwork/blockscout/releases/tag/v1.3.4-beta" ) - assert LayoutView.release_link("1.3.4") == + assert LayoutView.release_link("v1.3.4-beta") == {:safe, - ~s(1.3.4)} + ~s(v1.3.4-beta)} end end diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index e6eb5c4813..0ba1a960c7 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -13,6 +13,8 @@ config :explorer, config :explorer, Explorer.Counters.AverageBlockTime, enabled: true +config :explorer, Explorer.Chain.BlockNumberCache, enabled: true + config :explorer, Explorer.ExchangeRates.Source.CoinMarketCap, pages: String.to_integer(System.get_env("COINMARKETCAP_PAGES") || "10") @@ -58,6 +60,14 @@ config :explorer, Explorer.Staking.PoolsReader, validators_contract_address: System.get_env("POS_VALIDATORS_CONTRACT"), staking_contract_address: System.get_env("POS_STAKING_CONTRACT") +if System.get_env("POS_STAKING_CONTRACT") do + config :explorer, Explorer.Staking.EpochCounter, + enabled: true, + staking_contract_address: System.get_env("POS_STAKING_CONTRACT") +else + config :explorer, Explorer.Staking.EpochCounter, enabled: false +end + if System.get_env("SUPPLY_MODULE") == "TokenBridge" do config :explorer, supply: Explorer.Chain.Supply.TokenBridge end diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs index 0a717d8b27..f27fc3b350 100644 --- a/apps/explorer/config/test.exs +++ b/apps/explorer/config/test.exs @@ -13,6 +13,8 @@ config :explorer, Explorer.Repo, config :explorer, Explorer.ExchangeRates, enabled: false, store: :ets +config :explorer, Explorer.Chain.BlockNumberCache, enabled: false + config :explorer, Explorer.KnownTokens, enabled: false, store: :ets config :explorer, Explorer.Counters.AverageBlockTime, enabled: false diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 417b1eaa8f..d886dd8a0b 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -51,7 +51,8 @@ defmodule Explorer.Application do configure(Explorer.Market.History.Cataloger), configure(Explorer.Counters.AddressesWithBalanceCounter), configure(Explorer.Counters.AverageBlockTime), - configure(Explorer.Validator.MetadataProcessor) + configure(Explorer.Validator.MetadataProcessor), + configure(Explorer.Staking.EpochCounter) ] |> List.flatten() end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 713d449d72..0977df1dd2 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -10,6 +10,7 @@ defmodule Explorer.Chain do limit: 2, order_by: 2, order_by: 3, + offset: 2, preload: 2, select: 2, subquery: 1, @@ -279,6 +280,38 @@ defmodule Explorer.Chain do |> Enum.take(paging_options.page_size) end + @spec address_to_logs(Address.t(), [paging_options]) :: [ + Log.t() + ] + def address_to_logs( + %Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash}, + options \\ [] + ) + when is_list(options) do + paging_options = Keyword.get(options, :paging_options) || %PagingOptions{page_size: 50} + + {block_number, transaction_index, log_index} = paging_options.key || {BlockNumberCache.max_number(), 0, 0} + + query = + from(log in Log, + inner_join: transaction in assoc(log, :transaction), + order_by: [desc: transaction.block_number, desc: transaction.index], + preload: [:transaction], + where: + log.address_hash == ^address_hash and + (transaction.block_number < ^block_number or + (transaction.block_number == ^block_number and transaction.index > ^transaction_index) or + (transaction.block_number == ^block_number and transaction.index == ^transaction_index and + log.index > ^log_index)), + limit: ^paging_options.page_size, + select: log + ) + + query + |> Repo.all() + |> Enum.take(paging_options.page_size) + end + @doc """ Finds all `t:Explorer.Chain.Transaction.t/0`s given the address_hash and the token contract address hash. @@ -2565,7 +2598,7 @@ defmodule Explorer.Chain do join: duplicate in subquery(query), on: duplicate.nonce == pending.nonce, on: duplicate.from_address_hash == pending.from_address_hash, - where: pending.hash in ^hashes + where: pending.hash in ^hashes and is_nil(pending.block_hash) ) Repo.update_all(transactions_to_update, [set: [error: "dropped/replaced", status: :error]], timeout: timeout) @@ -2802,6 +2835,7 @@ defmodule Explorer.Chain do on: smart_contract.address_hash == address.hash, where: not is_nil(address.contract_code), where: is_nil(smart_contract.address_hash), + where: address.contract_code != <<>>, preload: [{:smart_contract, smart_contract}, :decompiled_smart_contracts], order_by: [asc: address.inserted_at], limit: ^limit, @@ -2811,6 +2845,19 @@ defmodule Explorer.Chain do Repo.all(query) end + def list_empty_contracts(limit, offset) do + query = + from(address in Address, + where: address.contract_code == <<>>, + preload: [:smart_contract, :decompiled_smart_contracts], + order_by: [asc: address.inserted_at], + limit: ^limit, + offset: ^offset + ) + + Repo.all(query) + end + def list_not_decompiled_contracts(limit, offset) do query = from( @@ -2820,6 +2867,7 @@ defmodule Explorer.Chain do "NOT EXISTS (SELECT 1 FROM decompiled_smart_contracts WHERE decompiled_smart_contracts.address_hash = ?)", address.hash ), + where: address.contract_code != <<>>, left_join: smart_contract in SmartContract, on: smart_contract.address_hash == address.hash, left_join: decompiled_smart_contract in DecompiledSmartContract, @@ -2856,6 +2904,75 @@ defmodule Explorer.Chain do value end + @doc "Get staking pools from the DB" + @spec staking_pools(filter :: :validator | :active | :inactive, options :: PagingOptions.t()) :: [map()] + def staking_pools(filter, %PagingOptions{page_size: page_size, page_number: page_number} \\ @default_paging_options) do + off = page_size * (page_number - 1) + + Address.Name + |> staking_pool_filter(filter) + |> limit(^page_size) + |> offset(^off) + |> Repo.all() + end + + @doc "Get count of staking pools from the DB" + @spec staking_pools_count(filter :: :validator | :active | :inactive) :: integer + def staking_pools_count(filter) do + Address.Name + |> staking_pool_filter(filter) + |> Repo.aggregate(:count, :address_hash) + end + + defp staking_pool_filter(query, :validator) do + where( + query, + [address], + fragment( + """ + (?->>'is_active')::boolean = true and + (?->>'deleted')::boolean is not true and + (?->>'is_validator')::boolean = true + """, + address.metadata, + address.metadata, + address.metadata + ) + ) + end + + defp staking_pool_filter(query, :active) do + where( + query, + [address], + fragment( + """ + (?->>'is_active')::boolean = true and + (?->>'deleted')::boolean is not true + """, + address.metadata, + address.metadata + ) + ) + end + + defp staking_pool_filter(query, :inactive) do + where( + query, + [address], + fragment( + """ + (?->>'is_active')::boolean = false and + (?->>'deleted')::boolean is not true + """, + address.metadata, + address.metadata + ) + ) + end + + defp staking_pool_filter(query, _), do: query + defp with_decompiled_code_flag(query, hash) do has_decompiled_code_query = from(decompiled_contract in DecompiledSmartContract, diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 58d187d9d8..9cf86b570b 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -10,7 +10,7 @@ defmodule Explorer.Chain.Block do alias Explorer.Chain.{Address, Gas, Hash, Transaction} alias Explorer.Chain.Block.{Reward, SecondDegreeRelation} - @optional_attrs ~w(internal_transactions_indexed_at size)a + @optional_attrs ~w(internal_transactions_indexed_at size refetch_needed)a @required_attrs ~w(consensus difficulty gas_limit gas_used hash miner_hash nonce number parent_hash timestamp total_difficulty)a @@ -63,7 +63,8 @@ defmodule Explorer.Chain.Block do timestamp: DateTime.t(), total_difficulty: difficulty(), transactions: %Ecto.Association.NotLoaded{} | [Transaction.t()], - internal_transactions_indexed_at: DateTime.t() + internal_transactions_indexed_at: DateTime.t(), + refetch_needed: boolean() } @primary_key {:hash, Hash.Full, autogenerate: false} @@ -78,6 +79,7 @@ defmodule Explorer.Chain.Block do field(:timestamp, :utc_datetime_usec) field(:total_difficulty, :decimal) field(:internal_transactions_indexed_at, :utc_datetime_usec) + field(:refetch_needed, :boolean) timestamps() diff --git a/apps/explorer/lib/explorer/chain/block_number_cache.ex b/apps/explorer/lib/explorer/chain/block_number_cache.ex index 28d8d83e9c..2f335c7a2c 100644 --- a/apps/explorer/lib/explorer/chain/block_number_cache.ex +++ b/apps/explorer/lib/explorer/chain/block_number_cache.ex @@ -6,13 +6,10 @@ defmodule Explorer.Chain.BlockNumberCache do alias Explorer.Chain @tab :block_number_cache - # 30 minutes - @cache_period 1_000 * 60 * 30 @key "min_max" - @opts_key "opts" - @spec setup(Keyword.t()) :: :ok - def setup(opts \\ []) do + @spec setup() :: :ok + def setup do if :ets.whereis(@tab) == :undefined do :ets.new(@tab, [ :set, @@ -22,7 +19,6 @@ defmodule Explorer.Chain.BlockNumberCache do ]) end - setup_opts(opts) update_cache() :ok @@ -41,15 +37,11 @@ defmodule Explorer.Chain.BlockNumberCache do end defp value(type) do - initial_cache = {_min, _max, old_current_time} = cached_values() - - {min, max, _current_time} = - if current_time() - old_current_time > cache_period() do - update_cache() - + {min, max} = + if Application.get_env(:explorer, __MODULE__)[:enabled] do cached_values() else - initial_cache + min_and_max_from_db() end case type do @@ -59,18 +51,29 @@ defmodule Explorer.Chain.BlockNumberCache do end end - defp update_cache do - current_time = current_time() - {min, max} = min_and_max_from_db() - tuple = {min, max, current_time} + @spec update(non_neg_integer()) :: boolean() + def update(number) do + {old_min, old_max} = cached_values() - :ets.insert(@tab, {@key, tuple}) + cond do + number > old_max -> + tuple = {old_min, number} + :ets.insert(@tab, {@key, tuple}) + + number < old_min -> + tuple = {number, old_max} + :ets.insert(@tab, {@key, tuple}) + + true -> + false + end end - defp setup_opts(opts) do - cache_period = opts[:cache_period] || @cache_period + defp update_cache do + {min, max} = min_and_max_from_db() + tuple = {min, max} - :ets.insert(@tab, {@opts_key, cache_period}) + :ets.insert(@tab, {@key, tuple}) end defp cached_values do @@ -79,22 +82,10 @@ defmodule Explorer.Chain.BlockNumberCache do cached_values end - defp cache_period do - [{_, cache_period}] = :ets.lookup(@tab, @opts_key) - - cache_period - end - defp min_and_max_from_db do Chain.fetch_min_and_max_block_numbers() rescue _e -> {0, 0} end - - defp current_time do - utc_now = DateTime.utc_now() - - DateTime.to_unix(utc_now, :millisecond) - end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex index aaf5d7242e..b21c0c4441 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex @@ -41,6 +41,9 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do |> Map.put(:timestamps, timestamps) multi + |> Multi.run(:mark_as_deleted, fn repo, _ -> + mark_as_deleted(repo, changes_list, insert_options) + end) |> Multi.run(:insert_staking_pools, fn repo, _ -> insert(repo, changes_list, insert_options) end) @@ -49,6 +52,32 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do @impl Import.Runner def timeout, do: @timeout + defp mark_as_deleted(repo, changes_list, %{timeout: timeout}) when is_list(changes_list) do + addresses = Enum.map(changes_list, & &1.address_hash) + + query = + from( + address_name in Address.Name, + where: + address_name.address_hash not in ^addresses and + fragment("(?->>'is_pool')::boolean = true", address_name.metadata), + update: [ + set: [ + metadata: fragment("? || '{\"deleted\": true}'::jsonb", address_name.metadata) + ] + ] + ) + + try do + {_, result} = repo.update_all(query, [], timeout: timeout) + + {:ok, result} + rescue + postgrex_error in Postgrex.Error -> + {:error, %{exception: postgrex_error}} + end + end + @spec insert(Repo.t(), [map()], %{ optional(:on_conflict) => Import.Runner.on_conflict(), required(:timeout) => timeout, @@ -62,7 +91,7 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do {:ok, _} = Import.insert_changes_list( repo, - changes_list, + stakes_ratio(changes_list), conflict_target: {:unsafe_fragment, "(address_hash) where \"primary\" = true"}, on_conflict: on_conflict, for: Address.Name, @@ -85,4 +114,20 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do ] ) end + + # Calculates staked ratio for each pool + defp stakes_ratio(pools) do + active_pools = Enum.filter(pools, & &1.metadata[:is_active]) + + stakes_total = + Enum.reduce(pools, 0, fn pool, acc -> + acc + pool.metadata[:staked_amount] + end) + + Enum.map(active_pools, fn pool -> + staked_ratio = if stakes_total > 0, do: pool.metadata[:staked_amount] / stakes_total, else: 0 + + put_in(pool, [:metadata, :staked_ratio], staked_ratio) + end) + end end diff --git a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex index 4f1d2d6fe5..1a9fcc497d 100644 --- a/apps/explorer/lib/explorer/chain/import/runner/transactions.ex +++ b/apps/explorer/lib/explorer/chain/import/runner/transactions.ex @@ -8,7 +8,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do import Ecto.Query, only: [from: 2] alias Ecto.{Multi, Repo} - alias Explorer.Chain.{Data, Hash, Import, Transaction} + alias Explorer.Chain.{Block, Data, Hash, Import, Transaction} alias Explorer.Chain.Import.Runner.TokenTransfers @behaviour Import.Runner @@ -42,9 +42,13 @@ defmodule Explorer.Chain.Import.Runner.Transactions do |> Map.put(:timestamps, timestamps) |> Map.put(:token_transfer_transaction_hash_set, token_transfer_transaction_hash_set(options)) - Multi.run(multi, :transactions, fn repo, _ -> + multi + |> Multi.run(:transactions, fn repo, _ -> insert(repo, changes_list, insert_options) end) + |> Multi.run(:recollated_transactions, fn repo, %{transactions: transactions} -> + discard_blocks_for_recollated_transactions(repo, transactions, insert_options) + end) end @impl Import.Runner @@ -87,7 +91,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do on_conflict: on_conflict, for: Transaction, returning: - ~w(block_number index hash internal_transactions_indexed_at block_hash nonce from_address_hash created_contract_address_hash)a, + ~w(block_number index hash internal_transactions_indexed_at block_hash old_block_hash nonce from_address_hash created_contract_address_hash)a, timeout: timeout, timestamps: timestamps ) @@ -99,6 +103,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do update: [ set: [ block_hash: fragment("EXCLUDED.block_hash"), + old_block_hash: transaction.block_hash, block_number: fragment("EXCLUDED.block_number"), created_contract_address_hash: fragment("EXCLUDED.created_contract_address_hash"), cumulative_gas_used: fragment("EXCLUDED.cumulative_gas_used"), @@ -179,4 +184,43 @@ defmodule Explorer.Chain.Import.Runner.Transactions do end defp put_internal_transactions_indexed_at?(_, _), do: false + + defp discard_blocks_for_recollated_transactions(repo, transactions, %{ + timeout: timeout, + timestamps: %{updated_at: updated_at} + }) + when is_list(transactions) do + ordered_block_hashes = + transactions + |> Enum.filter(fn %{block_hash: block_hash, old_block_hash: old_block_hash} -> + not is_nil(old_block_hash) and block_hash != old_block_hash + end) + |> MapSet.new(& &1.old_block_hash) + |> Enum.sort() + + if Enum.empty?(ordered_block_hashes) do + {:ok, []} + else + query = + from( + block in Block, + where: block.hash in ^ordered_block_hashes, + update: [ + set: [ + consensus: false, + updated_at: ^updated_at + ] + ] + ) + + try do + {_, result} = repo.update_all(query, [], timeout: timeout) + + {:ok, result} + rescue + postgrex_error in Postgrex.Error -> + {:error, %{exception: postgrex_error, block_hashes: ordered_block_hashes}} + end + end + end end diff --git a/apps/explorer/lib/explorer/chain/internal_transaction/result.ex b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex index 5b4e3102fc..c4c680e7d4 100644 --- a/apps/explorer/lib/explorer/chain/internal_transaction/result.ex +++ b/apps/explorer/lib/explorer/chain/internal_transaction/result.ex @@ -19,7 +19,7 @@ defmodule Explorer.Chain.InternalTransaction.Result do {key, to_string(hash)} end - defp entry_to_raw({"code", _} = entry), do: entry + defp entry_to_raw({"code", code}), do: {"code", Data.to_string(code)} defp entry_to_raw({key, decimal}) when key in ~w(gasUsed) do integer = diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 1cf9c8f116..de99198181 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -205,6 +205,11 @@ defmodule Explorer.Chain.Transaction do field(:v, :decimal) field(:value, Wei) + # A transient field for deriving old block hash during transaction upserts. + # Used to force refetch of a block in case a transaction is re-collated + # in a different block. See: https://github.com/poanetwork/blockscout/issues/1911 + field(:old_block_hash, Hash.Full) + timestamps() belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full) diff --git a/apps/explorer/lib/explorer/paging_options.ex b/apps/explorer/lib/explorer/paging_options.ex index 1bac2dc71a..0828a0afad 100644 --- a/apps/explorer/lib/explorer/paging_options.ex +++ b/apps/explorer/lib/explorer/paging_options.ex @@ -4,10 +4,11 @@ defmodule Explorer.PagingOptions do number and index. """ - @type t :: %__MODULE__{key: key, page_size: page_size} + @type t :: %__MODULE__{key: key, page_size: page_size, page_number: page_number} @typep key :: any() @typep page_size :: non_neg_integer() + @typep page_number :: pos_integer() - defstruct [:key, :page_size] + defstruct [:key, :page_size, page_number: 1] end diff --git a/apps/explorer/lib/explorer/staking/epoch_counter.ex b/apps/explorer/lib/explorer/staking/epoch_counter.ex new file mode 100644 index 0000000000..c35ec1a6c9 --- /dev/null +++ b/apps/explorer/lib/explorer/staking/epoch_counter.ex @@ -0,0 +1,124 @@ +defmodule Explorer.Staking.EpochCounter do + @moduledoc """ + Fetches current staking epoch number and the epoch end block number. + It subscribes to handle new blocks and conclude whether the epoch is over. + """ + + use GenServer + + alias Explorer.Chain.Events.Subscriber + alias Explorer.SmartContract.Reader + + @table_name __MODULE__ + @epoch_key "epoch_num" + @epoch_end_key "epoch_end_block" + + @doc "Current staking epoch number" + def epoch_number do + if :ets.info(@table_name) != :undefined do + case :ets.lookup(@table_name, @epoch_key) do + [{_, epoch_num}] -> + epoch_num + + _ -> + 0 + end + end + end + + @doc "Block number on which will start new epoch" + def epoch_end_block do + if :ets.info(@table_name) != :undefined do + case :ets.lookup(@table_name, @epoch_end_key) do + [{_, epoch_end}] -> + epoch_end + + _ -> + 0 + end + end + end + + def start_link([]) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + def init([]) do + :ets.new(@table_name, [ + :set, + :named_table, + :public, + write_concurrency: true + ]) + + Subscriber.to(:blocks, :realtime) + {:ok, [], {:continue, :epoch_info}} + end + + def handle_continue(:epoch_info, state) do + fetch_epoch_info() + {:noreply, state} + end + + @doc "Handles new blocks and decides to fetch new epoch info" + def handle_info({:chain_event, :blocks, :realtime, blocks}, state) do + new_block_number = + blocks + |> Enum.map(&Map.get(&1, :number, 0)) + |> Enum.max(fn -> 0 end) + + case :ets.lookup(@table_name, @epoch_end_key) do + [] -> + fetch_epoch_info() + + [{_, epoch_end_block}] when epoch_end_block < new_block_number -> + fetch_epoch_info() + + _ -> + :ok + end + + {:noreply, state} + end + + defp fetch_epoch_info do + with data <- get_epoch_info(), + {:ok, [epoch_num]} <- data["stakingEpoch"], + {:ok, [epoch_end_block]} <- data["stakingEpochEndBlock"] do + :ets.insert(@table_name, {@epoch_key, epoch_num}) + :ets.insert(@table_name, {@epoch_end_key, epoch_end_block}) + end + end + + defp get_epoch_info do + contract_abi = abi("staking.json") + + functions = ["stakingEpoch", "stakingEpochEndBlock"] + + functions + |> Enum.map(fn function -> + %{ + contract_address: staking_address(), + function_name: function, + args: [] + } + end) + |> Reader.query_contracts(contract_abi) + |> Enum.zip(functions) + |> Enum.into(%{}, fn {response, function} -> + {function, response} + end) + end + + defp staking_address do + Application.get_env(:explorer, __MODULE__, [])[:staking_contract_address] + end + + # sobelow_skip ["Traversal"] + defp abi(file_name) do + :explorer + |> Application.app_dir("priv/contracts_abi/pos/#{file_name}") + |> File.read!() + |> Jason.decode!() + end +end diff --git a/apps/explorer/lib/explorer/staking/pools_reader.ex b/apps/explorer/lib/explorer/staking/pools_reader.ex index de03ff10b5..608fea3863 100644 --- a/apps/explorer/lib/explorer/staking/pools_reader.ex +++ b/apps/explorer/lib/explorer/staking/pools_reader.ex @@ -29,6 +29,7 @@ defmodule Explorer.Staking.PoolsReader do {:ok, [delegator_addresses]} <- data["poolDelegators"], delegators_count = Enum.count(delegator_addresses), {:ok, [staked_amount]} <- data["stakeAmountTotalMinusOrderedWithdraw"], + {:ok, [self_staked_amount]} <- data["stakeAmountMinusOrderedWithdraw"], {:ok, [is_validator]} <- data["isValidator"], {:ok, [was_validator_count]} <- data["validatorCounter"], {:ok, [is_banned]} <- data["isValidatorBanned"], @@ -42,6 +43,7 @@ defmodule Explorer.Staking.PoolsReader do is_active: is_active, delegators_count: delegators_count, staked_amount: staked_amount, + self_staked_amount: self_staked_amount, is_validator: is_validator, was_validator_count: was_validator_count, is_banned: is_banned, @@ -77,14 +79,15 @@ defmodule Explorer.Staking.PoolsReader do contract_abi = abi("staking.json") ++ abi("validators.json") methods = [ - {:staking, "isPoolActive", staking_address}, - {:staking, "poolDelegators", staking_address}, - {:staking, "stakeAmountTotalMinusOrderedWithdraw", staking_address}, - {:validators, "isValidator", mining_address}, - {:validators, "validatorCounter", mining_address}, - {:validators, "isValidatorBanned", mining_address}, - {:validators, "bannedUntil", mining_address}, - {:validators, "banCounter", mining_address} + {:staking, "isPoolActive", [staking_address]}, + {:staking, "poolDelegators", [staking_address]}, + {:staking, "stakeAmountTotalMinusOrderedWithdraw", [staking_address]}, + {:staking, "stakeAmountMinusOrderedWithdraw", [staking_address, staking_address]}, + {:validators, "isValidator", [mining_address]}, + {:validators, "validatorCounter", [mining_address]}, + {:validators, "isValidatorBanned", [mining_address]}, + {:validators, "bannedUntil", [mining_address]}, + {:validators, "banCounter", [mining_address]} ] methods @@ -96,11 +99,11 @@ defmodule Explorer.Staking.PoolsReader do end) end - defp format_request({contract_name, function_name, param}) do + defp format_request({contract_name, function_name, params}) do %{ contract_address: contract(contract_name), function_name: function_name, - args: [param] + args: params } end diff --git a/apps/explorer/priv/contracts_abi/pos/staking.json b/apps/explorer/priv/contracts_abi/pos/staking.json index 7bcbcfb18c..33f773ea61 100644 --- a/apps/explorer/priv/contracts_abi/pos/staking.json +++ b/apps/explorer/priv/contracts_abi/pos/staking.json @@ -1,36 +1,17 @@ [ { "constant": true, - "inputs": [], - "name": "STAKE_UNIT", - "outputs": [ + "inputs": [ { - "name": "", - "type": "uint256" - } - ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "MAX_DELEGATORS_PER_POOL", - "outputs": [ + "name": "_poolStakingAddress", + "type": "address" + }, { - "name": "", - "type": "uint256" + "name": "_delegator", + "type": "address" } ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "MAX_CANDIDATES", + "name": "poolDelegatorIndex", "outputs": [ { "name": "", @@ -42,154 +23,55 @@ "type": "function" }, { - "anonymous": false, - "inputs": [ - { - "indexed": true, - "name": "fromPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", - "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "uint256" - } - ], - "name": "Claimed", - "type": "event" - }, - { - "anonymous": false, + "constant": true, "inputs": [ { - "indexed": true, - "name": "toPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", + "name": "_poolStakingAddress", "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "uint256" } ], - "name": "Staked", - "type": "event" - }, - { - "anonymous": false, - "inputs": [ - { - "indexed": false, - "name": "fromPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "toPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", - "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, + "name": "stakeAmountTotalMinusOrderedWithdraw", + "outputs": [ { - "indexed": false, - "name": "amount", + "name": "", "type": "uint256" } ], - "name": "StakeMoved", - "type": "event" + "payable": false, + "stateMutability": "view", + "type": "function" }, { - "anonymous": false, + "constant": false, "inputs": [ { - "indexed": true, - "name": "fromPoolStakingAddress", - "type": "address" - }, - { - "indexed": true, - "name": "staker", + "name": "_erc20TokenContract", "type": "address" - }, - { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", - "type": "int256" } ], - "name": "WithdrawalOrdered", - "type": "event" + "name": "setErc20TokenContract", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" }, { - "anonymous": false, + "constant": false, "inputs": [ { - "indexed": true, - "name": "fromPoolStakingAddress", + "name": "_fromPoolStakingAddress", "type": "address" }, { - "indexed": true, - "name": "staker", + "name": "_toPoolStakingAddress", "type": "address" }, { - "indexed": true, - "name": "stakingEpoch", - "type": "uint256" - }, - { - "indexed": false, - "name": "amount", + "name": "_amount", "type": "uint256" } ], - "name": "Withdrawn", - "type": "event" - }, - { - "constant": false, - "inputs": [ - { - "name": "_unremovableStakingAddress", - "type": "address" - } - ], - "name": "clearUnremovableValidator", + "name": "moveStake", "outputs": [], "payable": false, "stateMutability": "nonpayable", @@ -197,8 +79,13 @@ }, { "constant": false, - "inputs": [], - "name": "incrementStakingEpoch", + "inputs": [ + { + "name": "_minStake", + "type": "uint256" + } + ], + "name": "setDelegatorMinStake", "outputs": [], "payable": false, "stateMutability": "nonpayable", @@ -231,132 +118,75 @@ "constant": false, "inputs": [ { - "name": "_fromPoolStakingAddress", - "type": "address" - }, - { - "name": "_toPoolStakingAddress", - "type": "address" - }, - { - "name": "_amount", + "name": "_minStake", "type": "uint256" } ], - "name": "moveStake", + "name": "setCandidateMinStake", "outputs": [], "payable": false, "stateMutability": "nonpayable", "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_toPoolStakingAddress", + "name": "_poolStakingAddress", "type": "address" - }, - { - "name": "_amount", - "type": "uint256" } ], - "name": "stake", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_fromPoolStakingAddress", - "type": "address" - }, + "name": "stakeAmountTotal", + "outputs": [ { - "name": "_amount", + "name": "", "type": "uint256" } ], - "name": "withdraw", - "outputs": [], "payable": false, - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { "name": "_poolStakingAddress", "type": "address" }, { - "name": "_amount", - "type": "int256" - } - ], - "name": "orderWithdraw", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ - { - "name": "_poolStakingAddress", + "name": "_staker", "type": "address" } ], - "name": "claimOrderedWithdraw", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ + "name": "stakeAmountMinusOrderedWithdraw", + "outputs": [ { - "name": "_erc20TokenContract", - "type": "address" + "name": "", + "type": "uint256" } ], - "name": "setErc20TokenContract", - "outputs": [], "payable": false, - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { - "constant": false, + "constant": true, "inputs": [ { - "name": "_minStake", - "type": "uint256" + "name": "_stakingAddress", + "type": "address" } ], - "name": "setCandidateMinStake", - "outputs": [], - "payable": false, - "stateMutability": "nonpayable", - "type": "function" - }, - { - "constant": false, - "inputs": [ + "name": "poolInactiveIndex", + "outputs": [ { - "name": "_minStake", + "name": "", "type": "uint256" } ], - "name": "setDelegatorMinStake", - "outputs": [], "payable": false, - "stateMutability": "nonpayable", + "stateMutability": "view", "type": "function" }, { @@ -375,12 +205,21 @@ }, { "constant": true, - "inputs": [], - "name": "getPoolsInactive", + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "maxWithdrawAllowed", "outputs": [ { "name": "", - "type": "address[]" + "type": "uint256" } ], "payable": false, @@ -389,30 +228,21 @@ }, { "constant": true, - "inputs": [], - "name": "getPoolsLikelihood", - "outputs": [ + "inputs": [ { - "name": "likelihoods", - "type": "int256[]" + "name": "_poolStakingAddress", + "type": "address" }, { - "name": "sum", - "type": "int256" + "name": "_staker", + "type": "address" } ], - "payable": false, - "stateMutability": "view", - "type": "function" - }, - { - "constant": true, - "inputs": [], - "name": "getPoolsToBeElected", + "name": "stakeAmountByCurrentEpoch", "outputs": [ { "name": "", - "type": "address[]" + "type": "uint256" } ], "payable": false, @@ -422,11 +252,11 @@ { "constant": true, "inputs": [], - "name": "getPoolsToBeRemoved", + "name": "stakingEpoch", "outputs": [ { "name": "", - "type": "address[]" + "type": "uint256" } ], "payable": false, @@ -436,11 +266,11 @@ { "constant": true, "inputs": [], - "name": "areStakeAndWithdrawAllowed", + "name": "getDelegatorMinStake", "outputs": [ { "name": "", - "type": "bool" + "type": "uint256" } ], "payable": false, @@ -477,8 +307,17 @@ }, { "constant": true, - "inputs": [], - "name": "getDelegatorMinStake", + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_staker", + "type": "address" + } + ], + "name": "maxWithdrawOrderAllowed", "outputs": [ { "name": "", @@ -493,15 +332,19 @@ "constant": true, "inputs": [ { - "name": "_stakingAddress", + "name": "_poolStakingAddress", + "type": "address" + }, + { + "name": "_delegator", "type": "address" } ], - "name": "isPoolActive", + "name": "poolDelegatorInactiveIndex", "outputs": [ { "name": "", - "type": "bool" + "type": "uint256" } ], "payable": false, @@ -510,21 +353,49 @@ }, { "constant": true, + "inputs": [], + "name": "getPoolsLikelihood", + "outputs": [ + { + "name": "likelihoods", + "type": "int256[]" + }, + { + "name": "sum", + "type": "int256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, "inputs": [ { - "name": "_poolStakingAddress", + "name": "_unremovableStakingAddress", "type": "address" - }, + } + ], + "name": "clearUnremovableValidator", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [ { - "name": "_staker", + "name": "_poolStakingAddress", "type": "address" } ], - "name": "maxWithdrawAllowed", + "name": "poolDelegators", "outputs": [ { "name": "", - "type": "uint256" + "type": "address[]" } ], "payable": false, @@ -543,7 +414,7 @@ "type": "address" } ], - "name": "maxWithdrawOrderAllowed", + "name": "orderWithdrawEpoch", "outputs": [ { "name": "", @@ -581,6 +452,20 @@ "stateMutability": "pure", "type": "function" }, + { + "constant": true, + "inputs": [], + "name": "getPoolsToBeElected", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, { "constant": true, "inputs": [ @@ -593,7 +478,7 @@ "type": "address" } ], - "name": "orderedWithdrawAmount", + "name": "stakeAmount", "outputs": [ { "name": "", @@ -608,34 +493,80 @@ "constant": true, "inputs": [ { - "name": "_poolStakingAddress", + "name": "_stakingAddress", "type": "address" } ], - "name": "orderedWithdrawAmountTotal", + "name": "isPoolActive", "outputs": [ { "name": "", - "type": "uint256" + "type": "bool" } ], "payable": false, "stateMutability": "view", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "name": "_toPoolStakingAddress", + "type": "address" + }, + { + "name": "_amount", + "type": "uint256" + } + ], + "name": "stake", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": true, + "inputs": [ + { + "name": "_stakingAddress", + "type": "address" + } + ], + "name": "poolIndex", + "outputs": [ + { + "name": "", + "type": "uint256" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": false, "inputs": [ { "name": "_poolStakingAddress", "type": "address" }, { - "name": "_staker", - "type": "address" + "name": "_amount", + "type": "int256" } ], - "name": "orderWithdrawEpoch", + "name": "orderWithdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "STAKE_UNIT", "outputs": [ { "name": "", @@ -646,6 +577,34 @@ "stateMutability": "view", "type": "function" }, + { + "constant": false, + "inputs": [ + { + "name": "_poolStakingAddress", + "type": "address" + } + ], + "name": "claimOrderedWithdraw", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "getPoolsToBeRemoved", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, { "constant": true, "inputs": [ @@ -654,7 +613,7 @@ "type": "address" } ], - "name": "stakeAmountTotal", + "name": "orderedWithdrawAmountTotal", "outputs": [ { "name": "", @@ -665,19 +624,51 @@ "stateMutability": "view", "type": "function" }, + { + "constant": true, + "inputs": [], + "name": "getPoolsInactive", + "outputs": [ + { + "name": "", + "type": "address[]" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "validatorSetContract", + "outputs": [ + { + "name": "", + "type": "address" + } + ], + "payable": false, + "stateMutability": "view", + "type": "function" + }, { "constant": true, "inputs": [ { "name": "_poolStakingAddress", "type": "address" + }, + { + "name": "_staker", + "type": "address" } ], - "name": "poolDelegators", + "name": "orderedWithdrawAmount", "outputs": [ { "name": "", - "type": "address[]" + "type": "uint256" } ], "payable": false, @@ -686,17 +677,8 @@ }, { "constant": true, - "inputs": [ - { - "name": "_poolStakingAddress", - "type": "address" - }, - { - "name": "_delegator", - "type": "address" - } - ], - "name": "poolDelegatorIndex", + "inputs": [], + "name": "MAX_DELEGATORS_PER_POOL", "outputs": [ { "name": "", @@ -711,15 +693,11 @@ "constant": true, "inputs": [ { - "name": "_poolStakingAddress", - "type": "address" - }, - { - "name": "_delegator", + "name": "_stakingAddress", "type": "address" } ], - "name": "poolDelegatorInactiveIndex", + "name": "poolToBeRemovedIndex", "outputs": [ { "name": "", @@ -730,15 +708,19 @@ "stateMutability": "view", "type": "function" }, + { + "constant": false, + "inputs": [], + "name": "incrementStakingEpoch", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, { "constant": true, - "inputs": [ - { - "name": "_stakingAddress", - "type": "address" - } - ], - "name": "poolIndex", + "inputs": [], + "name": "MAX_CANDIDATES", "outputs": [ { "name": "", @@ -750,22 +732,21 @@ "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_stakingAddress", + "name": "_fromPoolStakingAddress", "type": "address" - } - ], - "name": "poolInactiveIndex", - "outputs": [ + }, { - "name": "", + "name": "_amount", "type": "uint256" } ], + "name": "withdraw", + "outputs": [], "payable": false, - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { @@ -788,87 +769,223 @@ "type": "function" }, { - "constant": true, + "anonymous": false, "inputs": [ { - "name": "_stakingAddress", + "indexed": true, + "name": "fromPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" } ], - "name": "poolToBeRemovedIndex", - "outputs": [ + "name": "Claimed", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { - "name": "", + "indexed": true, + "name": "toPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", + "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", "type": "uint256" } ], - "payable": false, - "stateMutability": "view", - "type": "function" + "name": "Staked", + "type": "event" }, { - "constant": true, + "anonymous": false, "inputs": [ { - "name": "_poolStakingAddress", + "indexed": false, + "name": "fromPoolStakingAddress", "type": "address" }, { - "name": "_staker", + "indexed": true, + "name": "toPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" } ], - "name": "stakeAmount", - "outputs": [ + "name": "StakeMoved", + "type": "event" + }, + { + "anonymous": false, + "inputs": [ { - "name": "", + "indexed": true, + "name": "fromPoolStakingAddress", + "type": "address" + }, + { + "indexed": true, + "name": "staker", + "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "int256" } ], - "payable": false, - "stateMutability": "view", - "type": "function" + "name": "WithdrawalOrdered", + "type": "event" }, { - "constant": true, + "anonymous": false, "inputs": [ { - "name": "_poolStakingAddress", + "indexed": true, + "name": "fromPoolStakingAddress", "type": "address" }, { - "name": "_staker", + "indexed": true, + "name": "staker", "type": "address" + }, + { + "indexed": true, + "name": "stakingEpoch", + "type": "uint256" + }, + { + "indexed": false, + "name": "amount", + "type": "uint256" } ], - "name": "stakeAmountByCurrentEpoch", - "outputs": [ + "name": "Withdrawn", + "type": "event" + }, + { + "constant": false, + "inputs": [ { - "name": "", + "name": "_amount", "type": "uint256" + }, + { + "name": "_miningAddress", + "type": "address" } ], + "name": "addPool", + "outputs": [], "payable": false, - "stateMutability": "view", + "stateMutability": "nonpayable", "type": "function" }, { - "constant": true, + "constant": false, "inputs": [ { - "name": "_poolStakingAddress", + "name": "_validatorSetContract", "type": "address" }, { - "name": "_staker", + "name": "_erc20TokenContract", "type": "address" + }, + { + "name": "_initialStakingAddresses", + "type": "address[]" + }, + { + "name": "_delegatorMinStake", + "type": "uint256" + }, + { + "name": "_candidateMinStake", + "type": "uint256" + }, + { + "name": "_stakingEpochDuration", + "type": "uint256" + }, + { + "name": "_stakeWithdrawDisallowPeriod", + "type": "uint256" } ], - "name": "stakeAmountMinusOrderedWithdraw", + "name": "initialize", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": false, + "inputs": [ + { + "name": "_blockNumber", + "type": "uint256" + } + ], + "name": "setStakingEpochStartBlock", + "outputs": [], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "areStakeAndWithdrawAllowed", "outputs": [ { "name": "", - "type": "uint256" + "type": "bool" } ], "payable": false, @@ -877,13 +994,22 @@ }, { "constant": true, - "inputs": [ + "inputs": [], + "name": "stakeWithdrawDisallowPeriod", + "outputs": [ { - "name": "_poolStakingAddress", - "type": "address" + "name": "", + "type": "uint256" } ], - "name": "stakeAmountTotalMinusOrderedWithdraw", + "payable": false, + "stateMutability": "view", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "stakingEpochDuration", "outputs": [ { "name": "", @@ -897,7 +1023,7 @@ { "constant": true, "inputs": [], - "name": "stakingEpoch", + "name": "stakingEpochStartBlock", "outputs": [ { "name": "", @@ -911,11 +1037,11 @@ { "constant": true, "inputs": [], - "name": "validatorSetContract", + "name": "stakingEpochEndBlock", "outputs": [ { "name": "", - "type": "address" + "type": "uint256" } ], "payable": false, diff --git a/apps/explorer/priv/repo/migrations/20190508152922_add_old_block_hash_for_transactions.exs b/apps/explorer/priv/repo/migrations/20190508152922_add_old_block_hash_for_transactions.exs new file mode 100644 index 0000000000..daf04c22f3 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20190508152922_add_old_block_hash_for_transactions.exs @@ -0,0 +1,12 @@ +defmodule Explorer.Repo.Migrations.AddOldBlockHashForTransactions do + use Ecto.Migration + + def change do + alter table(:transactions) do + # A transient field for deriving old block hash during transaction upserts. + # Used to force refetch of a block in case a transaction is re-collated + # in a different block. See: https://github.com/poanetwork/blockscout/issues/1911 + add(:old_block_hash, :bytea, null: true) + end + end +end diff --git a/apps/explorer/priv/repo/migrations/20190513134025_add_refetch_needed_to_block.exs b/apps/explorer/priv/repo/migrations/20190513134025_add_refetch_needed_to_block.exs new file mode 100644 index 0000000000..70ddac3e03 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20190513134025_add_refetch_needed_to_block.exs @@ -0,0 +1,11 @@ +defmodule Explorer.Repo.Migrations.AddRefetchNeededToBlock do + use Ecto.Migration + + def change do + alter table(:blocks) do + add(:refetch_needed, :boolean, default: false) + end + + execute("UPDATE blocks SET refetch_needed = TRUE;", "") + end +end diff --git a/apps/explorer/test/explorer/chain/block_number_cache_test.exs b/apps/explorer/test/explorer/chain/block_number_cache_test.exs index a33263293c..7b501a718b 100644 --- a/apps/explorer/test/explorer/chain/block_number_cache_test.exs +++ b/apps/explorer/test/explorer/chain/block_number_cache_test.exs @@ -3,6 +3,14 @@ defmodule Explorer.Chain.BlockNumberCacheTest do alias Explorer.Chain.BlockNumberCache + setup do + Application.put_env(:explorer, Explorer.Chain.BlockNumberCache, enabled: true) + + on_exit(fn -> + Application.put_env(:explorer, Explorer.Chain.BlockNumberCache, enabled: false) + end) + end + describe "max_number/1" do test "returns max number" do insert(:block, number: 5) @@ -11,33 +19,6 @@ defmodule Explorer.Chain.BlockNumberCacheTest do assert BlockNumberCache.max_number() == 5 end - - test "invalidates cache if period did pass" do - insert(:block, number: 5) - - BlockNumberCache.setup(cache_period: 2_000) - - assert BlockNumberCache.max_number() == 5 - - insert(:block, number: 10) - - Process.sleep(2_000) - - assert BlockNumberCache.max_number() == 10 - assert BlockNumberCache.min_number() == 5 - end - - test "does not invalidate cache if period time did not pass" do - insert(:block, number: 5) - - BlockNumberCache.setup(cache_period: 10_000) - - assert BlockNumberCache.max_number() == 5 - - insert(:block, number: 10) - - assert BlockNumberCache.max_number() == 5 - end end describe "min_number/1" do @@ -48,32 +29,31 @@ defmodule Explorer.Chain.BlockNumberCacheTest do assert BlockNumberCache.max_number() == 2 end + end - test "invalidates cache" do - insert(:block, number: 5) - - BlockNumberCache.setup(cache_period: 2_000) + describe "update/1" do + test "updates max number" do + insert(:block, number: 2) - assert BlockNumberCache.min_number() == 5 + BlockNumberCache.setup() - insert(:block, number: 2) + assert BlockNumberCache.max_number() == 2 - Process.sleep(2_000) + assert BlockNumberCache.update(3) - assert BlockNumberCache.min_number() == 2 - assert BlockNumberCache.max_number() == 5 + assert BlockNumberCache.max_number() == 3 end - test "does not invalidate cache if period time did not pass" do - insert(:block, number: 5) + test "updates min number" do + insert(:block, number: 2) - BlockNumberCache.setup(cache_period: 10_000) + BlockNumberCache.setup() - assert BlockNumberCache.max_number() == 5 + assert BlockNumberCache.min_number() == 2 - insert(:block, number: 2) + assert BlockNumberCache.update(1) - assert BlockNumberCache.max_number() == 5 + assert BlockNumberCache.min_number() == 1 end end end diff --git a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs index ecf1d71dfb..63110cba40 100644 --- a/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/blocks_test.exs @@ -6,7 +6,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do import Explorer.Chain.Import.RunnerCase, only: [insert_address_with_token_balances: 1, update_holder_count!: 2] alias Ecto.Multi - alias Explorer.Chain.Import.Runner.{Blocks, Transaction} + alias Explorer.Chain.Import.Runner.{Blocks, Transactions} alias Explorer.Chain.{Address, Block, Transaction} alias Explorer.Chain alias Explorer.Repo @@ -283,6 +283,29 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do insert_block(new_block1, options) assert Chain.missing_block_number_ranges(range) == [] end + + # Regression test for https://github.com/poanetwork/blockscout/issues/1911 + test "forces block refetch if transaction is re-collated in a different block", + %{consensus_block: %Block{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do + new_block1 = params_for(:block, miner_hash: miner_hash, parent_hash: block_hash, number: block_number + 1) + new_block2 = params_for(:block, miner_hash: miner_hash, parent_hash: new_block1.hash, number: block_number + 2) + + range = block_number..(block_number + 2) + + insert_block(new_block1, options) + insert_block(new_block2, options) + assert Chain.missing_block_number_ranges(range) == [] + + trans_hash = transaction_hash() + + transaction1 = transaction_params_with_block([hash: trans_hash], new_block1) + insert_transaction(transaction1, options) + assert Chain.missing_block_number_ranges(range) == [] + + transaction2 = transaction_params_with_block([hash: trans_hash], new_block2) + insert_transaction(transaction2, options) + assert Chain.missing_block_number_ranges(range) == [(block_number + 1)..(block_number + 1)] + end end defp insert_block(block_params, options) do @@ -293,6 +316,28 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do |> Repo.transaction() end + defp transaction_params_with_block(transaction_params, block_params) do + params_for(:transaction, transaction_params) + |> Map.merge(%{ + block_hash: block_params.hash, + block_number: block_params.number, + cumulative_gas_used: 50_000, + error: nil, + gas_used: 50_000, + index: 0, + from_address_hash: insert(:address).hash + }) + end + + defp insert_transaction(transaction_params, options) do + %Ecto.Changeset{valid?: true, changes: transaction_changes} = + Transaction.changeset(%Transaction{}, transaction_params) + + Multi.new() + |> Transactions.run([transaction_changes], options) + |> Repo.transaction() + end + defp count(schema) do Repo.one!(select(schema, fragment("COUNT(*)"))) end diff --git a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs index d5bc6ecfca..af25368679 100644 --- a/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs +++ b/apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs @@ -1,85 +1,29 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsTest do use Explorer.DataCase + import Explorer.Factory + alias Ecto.Multi alias Explorer.Chain.Import.Runner.StakingPools describe "run/1" do test "insert new pools list" do - pools = [ - %{ - address_hash: %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>> - }, - metadata: %{ - banned_unitil: 0, - delegators_count: 0, - is_active: true, - is_banned: false, - is_validator: true, - mining_address: %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>> - }, - retries_count: 1, - staked_amount: 0, - was_banned_count: 0, - was_validator_count: 1 - }, - name: "anonymous", - primary: true - }, - %{ - address_hash: %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<170, 148, 182, 135, 211, 249, 85, 42, 69, 59, 129, 178, 131, 76, 165, 55, 120, 152, 13, 192>> - }, - metadata: %{ - banned_unitil: 0, - delegators_count: 0, - is_active: true, - is_banned: false, - is_validator: true, - mining_address: %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<117, 223, 66, 56, 58, 254, 107, 245, 25, 74, 168, 250, 14, 155, 61, 95, 158, 134, 148, 65>> - }, - retries_count: 1, - staked_amount: 0, - was_banned_count: 0, - was_validator_count: 1 - }, - name: "anonymous", - primary: true - }, - %{ - address_hash: %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<49, 44, 35, 14, 125, 109, 176, 82, 36, 246, 2, 8, 166, 86, 227, 84, 28, 92, 66, 186>> - }, - metadata: %{ - banned_unitil: 0, - delegators_count: 0, - is_active: true, - is_banned: false, - is_validator: true, - mining_address: %Explorer.Chain.Hash{ - byte_count: 20, - bytes: <<82, 45, 243, 150, 174, 112, 160, 88, 189, 105, 119, 132, 8, 99, 15, 219, 2, 51, 137, 178>> - }, - retries_count: 1, - staked_amount: 0, - was_banned_count: 0, - was_validator_count: 1 - }, - name: "anonymous", - primary: true - } - ] + pools = [pool1, pool2, pool3, pool4] = build_list(4, :staking_pool) assert {:ok, %{insert_staking_pools: list}} = run_changes(pools) assert Enum.count(list) == Enum.count(pools) + + saved_list = + Explorer.Chain.Address.Name + |> Repo.all() + |> Enum.reduce(%{}, fn pool, acc -> + Map.put(acc, pool.address_hash, pool) + end) + + assert saved_list[pool1.address_hash].metadata["staked_ratio"] == 0.25 + assert saved_list[pool2.address_hash].metadata["staked_ratio"] == 0.25 + assert saved_list[pool3.address_hash].metadata["staked_ratio"] == 0.25 + assert saved_list[pool4.address_hash].metadata["staked_ratio"] == 0.25 end end diff --git a/apps/explorer/test/explorer/chain/internal_transaction_test.exs b/apps/explorer/test/explorer/chain/internal_transaction_test.exs index 54ace519bd..fc3977a998 100644 --- a/apps/explorer/test/explorer/chain/internal_transaction_test.exs +++ b/apps/explorer/test/explorer/chain/internal_transaction_test.exs @@ -1,7 +1,7 @@ defmodule Explorer.Chain.InternalTransactionTest do use Explorer.DataCase - alias Explorer.Chain.{InternalTransaction, Wei} + alias Explorer.Chain.{Data, InternalTransaction, Wei} alias Explorer.Factory import EthereumJSONRPC, only: [integer_to_quantity: 1] @@ -173,7 +173,7 @@ defmodule Explorer.Chain.InternalTransactionTest do end test "it correctly formats a create" do - contract_code = Factory.contract_code_info().bytecode + {:ok, contract_code} = Data.cast(Factory.contract_code_info().bytecode) contract_address = Factory.address_hash() from = Factory.address_hash() gas = 50_000 diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index a9b479c3bd..cafd742e2a 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -50,6 +50,51 @@ defmodule Explorer.ChainTest do end end + describe "address_to_logs/2" do + test "fetches logs" do + address = insert(:address) + + transaction1 = + :transaction + |> insert(to_address: address) + |> with_block() + + insert(:log, transaction: transaction1, index: 1, address: address) + + transaction2 = + :transaction + |> insert(from_address: address) + |> with_block() + + insert(:log, transaction: transaction2, index: 2, address: address) + + assert Enum.count(Chain.address_to_logs(address)) == 2 + end + + test "paginates logs" do + address = insert(:address) + + transaction = + :transaction + |> insert(to_address: address) + |> with_block() + + log1 = insert(:log, transaction: transaction, index: 1, address: address) + + 2..51 + |> Enum.map(fn index -> insert(:log, transaction: transaction, index: index, address: address) end) + |> Enum.map(& &1.index) + + paging_options1 = %PagingOptions{page_size: 1} + + [_log] = Chain.address_to_logs(address, paging_options: paging_options1) + + paging_options2 = %PagingOptions{page_size: 60, key: {transaction.block_number, transaction.index, log1.index}} + + assert Enum.count(Chain.address_to_logs(address, paging_options: paging_options2)) == 50 + end + end + describe "address_to_transactions_with_rewards/2" do test "without transactions" do address = insert(:address) @@ -3903,4 +3948,59 @@ defmodule Explorer.ChainTest do refute Chain.contract_address?(to_string(hash), 1, json_rpc_named_arguments) end end + + describe "staking_pools/3" do + test "validators staking pools" do + inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: true}) + insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: false}) + + options = %PagingOptions{page_size: 20, page_number: 1} + + assert [gotten_validator] = Chain.staking_pools(:validator, options) + assert inserted_validator.address_hash == gotten_validator.address_hash + end + + test "active staking pools" do + inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: true}) + insert(:address_name, primary: true, metadata: %{is_active: false}) + + options = %PagingOptions{page_size: 20, page_number: 1} + + assert [gotten_validator] = Chain.staking_pools(:active, options) + assert inserted_validator.address_hash == gotten_validator.address_hash + end + + test "inactive staking pools" do + insert(:address_name, primary: true, metadata: %{is_active: true}) + inserted_validator = insert(:address_name, primary: true, metadata: %{is_active: false}) + + options = %PagingOptions{page_size: 20, page_number: 1} + + assert [gotten_validator] = Chain.staking_pools(:inactive, options) + assert inserted_validator.address_hash == gotten_validator.address_hash + end + end + + describe "staking_pools_count/1" do + test "validators staking pools" do + insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: true}) + insert(:address_name, primary: true, metadata: %{is_active: true, is_validator: false}) + + assert Chain.staking_pools_count(:validator) == 1 + end + + test "active staking pools" do + insert(:address_name, primary: true, metadata: %{is_active: true}) + insert(:address_name, primary: true, metadata: %{is_active: false}) + + assert Chain.staking_pools_count(:active) == 1 + end + + test "inactive staking pools" do + insert(:address_name, primary: true, metadata: %{is_active: true}) + insert(:address_name, primary: true, metadata: %{is_active: false}) + + assert Chain.staking_pools_count(:inactive) == 1 + end + end end diff --git a/apps/explorer/test/explorer/staking/epoch_counter_test.exs b/apps/explorer/test/explorer/staking/epoch_counter_test.exs new file mode 100644 index 0000000000..278377ce05 --- /dev/null +++ b/apps/explorer/test/explorer/staking/epoch_counter_test.exs @@ -0,0 +1,97 @@ +defmodule Explorer.Staking.EpochCounterTest do + use ExUnit.Case, async: false + + import Mox + + alias Explorer.Staking.EpochCounter + alias Explorer.Chain.Events.Publisher + + setup :verify_on_exit! + setup :set_mox_global + + test "when disabled, it returns nil" do + assert EpochCounter.epoch_number() == nil + assert EpochCounter.epoch_end_block() == nil + end + + test "fetch epoch data" do + set_mox(10, 880) + Application.put_env(:explorer, EpochCounter, enabled: true) + start_supervised!(EpochCounter) + + Process.sleep(1_000) + + assert EpochCounter.epoch_number() == 10 + assert EpochCounter.epoch_end_block() == 880 + end + + test "fetch new epoch data" do + set_mox(10, 880) + Application.put_env(:explorer, EpochCounter, enabled: true) + start_supervised!(EpochCounter) + + Process.sleep(1_000) + + assert EpochCounter.epoch_number() == 10 + assert EpochCounter.epoch_end_block() == 880 + + event_type = :blocks + broadcast_type = :realtime + event_data = [%Explorer.Chain.Block{number: 881}] + + set_mox(11, 960) + Publisher.broadcast([{event_type, event_data}], broadcast_type) + + Process.sleep(1_000) + + assert EpochCounter.epoch_number() == 11 + assert EpochCounter.epoch_end_block() == 960 + end + + defp set_mox(epoch_num, end_block_num) do + expect( + EthereumJSONRPC.Mox, + :json_rpc, + fn [ + %{ + id: 0, + jsonrpc: "2.0", + method: "eth_call", + params: _ + }, + %{ + id: 1, + jsonrpc: "2.0", + method: "eth_call", + params: _ + } + ], + _options -> + {:ok, + [ + %{ + id: 0, + jsonrpc: "2.0", + result: encode_num(epoch_num) + }, + %{ + id: 1, + jsonrpc: "2.0", + result: encode_num(end_block_num) + } + ]} + end + ) + end + + defp encode_num(num) do + selector = %ABI.FunctionSelector{function: nil, types: [uint: 32]} + + encoded_num = + [num] + |> ABI.TypeEncoder.encode(selector) + |> Base.encode16(case: :lower) + + "0x" <> encoded_num + end +end diff --git a/apps/explorer/test/explorer/staking/pools_reader_test.exs b/apps/explorer/test/explorer/staking/pools_reader_test.exs index ac6a600722..bb3af9fbcc 100644 --- a/apps/explorer/test/explorer/staking/pools_reader_test.exs +++ b/apps/explorer/test/explorer/staking/pools_reader_test.exs @@ -1,6 +1,5 @@ defmodule Explorer.Token.PoolsReaderTest do use EthereumJSONRPC.Case - use Explorer.DataCase alias Explorer.Staking.PoolsReader @@ -44,6 +43,7 @@ defmodule Explorer.Token.PoolsReaderTest do mining_address: <<187, 202, 168, 212, 130, 137, 187, 31, 252, 249, 128, 141, 154, 164, 177, 210, 21, 5, 76, 120>>, staked_amount: 0, + self_staked_amount: 0, staking_address: <<11, 47, 94, 47, 60, 189, 134, 78, 170, 44, 100, 46, 55, 105, 193, 88, 35, 97, 202, 246>>, was_banned_count: 0, was_validator_count: 2 @@ -162,6 +162,25 @@ defmodule Explorer.Token.PoolsReaderTest do result: "0x0000000000000000000000000000000000000000000000000000000000000000" } + # stakeAmountMinusOrderedWithdraw + %{ + id: id, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: + "0x58daab6a0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf60000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", + to: _ + }, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + # isValidator %{ id: id, diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex index 75d3429e70..68bb21434c 100644 --- a/apps/explorer/test/support/data_case.ex +++ b/apps/explorer/test/support/data_case.ex @@ -39,7 +39,7 @@ defmodule Explorer.DataCase do Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()}) end - Explorer.Chain.BlockNumberCache.setup(cache_period: 0) + Explorer.Chain.BlockNumberCache.setup() :ok end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 4c030a4db1..115b07e066 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -609,4 +609,24 @@ defmodule Explorer.Factory do user: build(:user) } end + + def staking_pool_factory do + %{ + address_hash: address_hash(), + metadata: %{ + banned_unitil: 0, + delegators_count: 0, + is_active: true, + is_banned: false, + is_validator: true, + mining_address: address_hash(), + retries_count: 1, + staked_amount: 25, + was_banned_count: 0, + was_validator_count: 1 + }, + name: "anonymous", + primary: true + } + end end diff --git a/apps/indexer/README.md b/apps/indexer/README.md index 34fb6e6ef3..173df0c6ab 100644 --- a/apps/indexer/README.md +++ b/apps/indexer/README.md @@ -92,6 +92,7 @@ After all deployed instances get all needed data, these fetchers should be depre - `uncataloged_token_transfers`: extracts token transfers from logs, which previously weren't parsed due to unknown format - `uncles_without_index`: adds previously unfetched `index` field for unfetched blocks in `block_second_degree_relations` +- `blocks_transactions_mismatch`: refetches each block once and revokes consensus to those whose transaction number mismatches with the number currently stored. This is meant to force the correction of a race condition that caused successfully fetched transactions to be overwritten by a following non-consensus block: [#1911](https://github.com/poanetwork/blockscout/issues/1911). ## Memory Usage diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index deb0e4554f..06a702aa4b 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -11,7 +11,7 @@ defmodule Indexer.Block.Fetcher do alias EthereumJSONRPC.{Blocks, FetchedBeneficiaries} alias Explorer.Chain - alias Explorer.Chain.{Address, Block, Hash, Import, Transaction} + alias Explorer.Chain.{Address, Block, BlockNumberCache, Hash, Import, Transaction} alias Indexer.Block.Fetcher.Receipts alias Indexer.Fetcher.{ @@ -171,13 +171,23 @@ defmodule Indexer.Block.Fetcher do transactions: %{params: transactions_with_receipts} } ) do - {:ok, %{inserted: inserted, errors: blocks_errors}} + result = {:ok, %{inserted: inserted, errors: blocks_errors}} + update_block_cache(inserted[:blocks]) + result else {step, {:error, reason}} -> {:error, {step, reason}} {:import, {:error, step, failed_value, changes_so_far}} -> {:error, {step, failed_value, changes_so_far}} end end + defp update_block_cache(blocks) do + max_block = Enum.max_by(blocks, fn block -> block.number end) + min_block = Enum.min_by(blocks, fn block -> block.number end) + + BlockNumberCache.update(max_block.number) + BlockNumberCache.update(min_block.number) + end + def import( %__MODULE__{broadcast: broadcast, callback_module: callback_module} = state, options diff --git a/apps/indexer/lib/indexer/fetcher/staking_pools.ex b/apps/indexer/lib/indexer/fetcher/staking_pools.ex index 68794d9ee4..fe4ab84c28 100644 --- a/apps/indexer/lib/indexer/fetcher/staking_pools.ex +++ b/apps/indexer/lib/indexer/fetcher/staking_pools.ex @@ -125,6 +125,7 @@ defmodule Indexer.Fetcher.StakingPools do pool |> Map.delete(:staking_address) |> Map.put(:mining_address, mining_address) + |> Map.put(:is_pool, true) %{ name: "anonymous", diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index 21dc3637ec..b2c3d19664 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -24,6 +24,7 @@ defmodule Indexer.Supervisor do } alias Indexer.Temporary.{ + BlocksTransactionsMismatch, UncatalogedTokenTransfers, UnclesWithoutIndex } @@ -124,6 +125,8 @@ defmodule Indexer.Supervisor do # Temporary workers {UncatalogedTokenTransfers.Supervisor, [[]]}, {UnclesWithoutIndex.Supervisor, + [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}, + {BlocksTransactionsMismatch.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]} ], strategy: :one_for_one diff --git a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex new file mode 100644 index 0000000000..76f4322379 --- /dev/null +++ b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex @@ -0,0 +1,115 @@ +defmodule Indexer.Temporary.BlocksTransactionsMismatch do + @moduledoc """ + Fetches `consensus` `t:Explorer.Chain.Block.t/0` and compares their transaction + number against a node, to revoke `consensus` on mismatch. + + This is meant to fix incorrectly strored transactions that happened as a result + of a race condition due to the asynchronicity of indexer's components. + """ + + use Indexer.Fetcher + + require Logger + + import Ecto.Query + + alias Ecto.Multi + alias EthereumJSONRPC.Blocks + alias Explorer.Chain.Block + alias Explorer.Repo + alias Indexer.BufferedTask + + @behaviour BufferedTask + + @defaults [ + flush_interval: :timer.seconds(3), + max_batch_size: 10, + max_concurrency: 4, + task_supervisor: Indexer.Temporary.BlocksTransactionsMismatch.TaskSupervisor, + metadata: [fetcher: :blocks_transactions_mismatch] + ] + + @doc false + def child_spec([init_options, gen_server_options]) when is_list(init_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_options = + @defaults + |> Keyword.merge(mergeable_init_options) + |> Keyword.put(:state, state) + + Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__) + end + + @impl BufferedTask + def init(initial, reducer, _) do + query = + from(block in Block, + join: transactions in assoc(block, :transactions), + where: block.consensus and block.refetch_needed, + group_by: block.hash, + select: {block, count(transactions.hash)} + ) + + {:ok, final} = Repo.stream_reduce(query, initial, &reducer.(&1, &2)) + + final + end + + @impl BufferedTask + def run(blocks_data, json_rpc_named_arguments) do + hashes = Enum.map(blocks_data, fn {block, _trans_num} -> block.hash end) + + Logger.debug("fetching") + + case EthereumJSONRPC.fetch_blocks_by_hash(hashes, json_rpc_named_arguments) do + {:ok, blocks} -> + run_blocks(blocks, blocks_data) + + {:error, reason} -> + Logger.error(fn -> ["failed to fetch: ", inspect(reason)] end) + {:retry, blocks_data} + end + end + + defp run_blocks(%Blocks{blocks_params: []}, blocks_data), do: {:retry, blocks_data} + + defp run_blocks( + %Blocks{transactions_params: transactions_params}, + blocks_data + ) do + found_blocks_map = + transactions_params + |> Enum.group_by(&Map.fetch!(&1, :block_hash)) + |> Map.new(fn {block_hash, trans_lst} -> {block_hash, Enum.count(trans_lst)} end) + + {found_blocks_data, missing_blocks_data} = + Enum.split_with(blocks_data, fn {block, _trans_num} -> + Map.has_key?(found_blocks_map, to_string(block.hash)) + end) + + {:ok, _} = + found_blocks_data + |> Enum.reduce(Multi.new(), fn {block, trans_num}, multi -> + changes = %{ + refetch_needed: false, + consensus: found_blocks_map[to_string(block.hash)] == trans_num + } + + Multi.update(multi, block.hash, Block.changeset(block, changes)) + end) + |> Repo.transaction() + + if Enum.empty?(missing_blocks_data) do + :ok + else + {:retry, missing_blocks_data} + end + end +end diff --git a/apps/indexer/test/indexer/fetcher/staking_pools_test.exs b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs index 8f985537bf..13e2c0d7ee 100644 --- a/apps/indexer/test/indexer/fetcher/staking_pools_test.exs +++ b/apps/indexer/test/indexer/fetcher/staking_pools_test.exs @@ -129,6 +129,25 @@ defmodule Indexer.Fetcher.StakingPoolsTest do result: "0x0000000000000000000000000000000000000000000000000000000000000000" } + # stakeAmountMinusOrderedWithdraw + %{ + id: id, + jsonrpc: "2.0", + method: "eth_call", + params: [ + %{ + data: + "0x58daab6a0000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf60000000000000000000000000b2f5e2f3cbd864eaa2c642e3769c1582361caf6", + to: _ + }, + "latest" + ] + } -> + %{ + id: id, + result: "0x0000000000000000000000000000000000000000000000000000000000000000" + } + # isValidator %{ id: id,