Merge branch 'master' into master

pull/1982/head
Victor Baranov 6 years ago committed by GitHub
commit 59d33a42dd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .dialyzer-ignore
  2. 10
      CHANGELOG.md
  3. 179
      README.md
  4. 4
      apps/block_scout_web/assets/css/components/_card.scss
  5. 4
      apps/block_scout_web/assets/css/components/_dropdown.scss
  6. 8
      apps/block_scout_web/lib/block_scout_web/chain.ex
  7. 46
      apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex
  8. 8
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/block_controller.ex
  9. 5
      apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex
  10. 59
      apps/block_scout_web/lib/block_scout_web/etherscan.ex
  11. 7
      apps/block_scout_web/lib/block_scout_web/router.ex
  12. 5
      apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex
  13. 58
      apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex
  14. 82
      apps/block_scout_web/lib/block_scout_web/templates/address_logs/index.html.eex
  15. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  16. 13
      apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
  17. 5
      apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex
  18. 3
      apps/block_scout_web/lib/block_scout_web/views/address_logs_view.ex
  19. 1
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  20. 28
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/block_view.ex
  21. 33
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex
  22. 3
      apps/block_scout_web/lib/block_scout_web/views/api/rpc/transaction_view.ex
  23. 18
      apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
  24. 72
      apps/block_scout_web/priv/gettext/default.pot
  25. 76
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  26. 1
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  27. 57
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs
  28. 3
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/transaction_controller_test.exs
  29. 24
      apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs
  30. 28
      apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs
  31. 10
      apps/explorer/config/config.exs
  32. 2
      apps/explorer/config/test.exs
  33. 3
      apps/explorer/lib/explorer/application.ex
  34. 119
      apps/explorer/lib/explorer/chain.ex
  35. 6
      apps/explorer/lib/explorer/chain/block.ex
  36. 57
      apps/explorer/lib/explorer/chain/block_number_cache.ex
  37. 47
      apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
  38. 50
      apps/explorer/lib/explorer/chain/import/runner/transactions.ex
  39. 2
      apps/explorer/lib/explorer/chain/internal_transaction/result.ex
  40. 5
      apps/explorer/lib/explorer/chain/transaction.ex
  41. 5
      apps/explorer/lib/explorer/paging_options.ex
  42. 124
      apps/explorer/lib/explorer/staking/epoch_counter.ex
  43. 23
      apps/explorer/lib/explorer/staking/pools_reader.ex
  44. 802
      apps/explorer/priv/contracts_abi/pos/staking.json
  45. 12
      apps/explorer/priv/repo/migrations/20190508152922_add_old_block_hash_for_transactions.exs
  46. 11
      apps/explorer/priv/repo/migrations/20190513134025_add_refetch_needed_to_block.exs
  47. 64
      apps/explorer/test/explorer/chain/block_number_cache_test.exs
  48. 47
      apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
  49. 86
      apps/explorer/test/explorer/chain/import/runner/staking_pools_test.exs
  50. 4
      apps/explorer/test/explorer/chain/internal_transaction_test.exs
  51. 100
      apps/explorer/test/explorer/chain_test.exs
  52. 97
      apps/explorer/test/explorer/staking/epoch_counter_test.exs
  53. 21
      apps/explorer/test/explorer/staking/pools_reader_test.exs
  54. 2
      apps/explorer/test/support/data_case.ex
  55. 20
      apps/explorer/test/support/factory.ex
  56. 1
      apps/indexer/README.md
  57. 14
      apps/indexer/lib/indexer/block/fetcher.ex
  58. 1
      apps/indexer/lib/indexer/fetcher/staking_pools.ex
  59. 3
      apps/indexer/lib/indexer/supervisor.ex
  60. 115
      apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
  61. 19
      apps/indexer/test/indexer/fetcher/staking_pools_test.exs

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

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

@ -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`
<br />Linux: Update the database username and password configuration in `apps/explorer/config/dev.secret.exs`
<br />Mac: Remove the `username` and `password` fields from `apps/explorer/config/dev.secret.exs`
<br />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`
<br />_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

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

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

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

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

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

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

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

@ -147,6 +147,13 @@ defmodule BlockScoutWeb.Router do
as: :decompiled_contract
)
resources(
"/logs",
AddressLogsController,
only: [:index],
as: :logs
)
resources(
"/contract_verifications",
AddressContractVerificationController,

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

@ -1,18 +1,25 @@
<% contract_creation_code = contract_creation_code(@address) %>
<section class="container">
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<div class="card-body">
<%= 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 %>
<div class="button button-disabled button-sm float-right ml-3">
<%= gettext("Verify & Publish") %>
</div>
<% 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 %>
<div class="mb-4">
<dl class="row">
@ -56,15 +63,32 @@
<% end %>
<section>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract creation code" %></h3>
<button type="button" class="button button-secondary button-sm" id="button" data-clipboard-text="<%= @address.contract_code %>" aria-label="copy contract creation code">
<%= gettext "Copy Contract Creation Code" %>
</button>
</div>
<div class="tile tile-muted">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= @address.contract_code %></code></pre>
</div>
<%= case contract_creation_code do %>
<% {:selfdestructed, transaction_init} -> %>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract Creation Code" %></h3>
<button type="button" class="button button-secondary button-sm" id="button" data-clipboard-text="<%= transaction_init %>" aria-label="copy contract creation code">
<%= gettext "Copy Contract Creation Code" %>
</button>
</div>
<div class="alert alert-info">
<p><%= gettext "Contracts that self destruct in their constructors have no contract code published and cannot be verified." %></p>
<p><%= gettext "Displaying the init data provided of the creating transaction." %></p>
</div>
<div class="tile tile-muted">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= transaction_init %></code></pre>
</div>
<% {:ok, contract_code} -> %>
<div class="d-flex justify-content-between align-items-baseline">
<h3><%= gettext "Contract Byte Code" %></h3>
<button type="button" class="button button-secondary button-sm" id="button" data-clipboard-text="<%= contract_code %>" aria-label="copy contract creation code">
<%= gettext "Copy Contract Byte Code" %>
</button>
</div>
<div class="tile tile-muted">
<pre class="pre-wrap pre-scrollable"><code class="nohighlight"><%= contract_code %></code></pre>
</div>
<% end %>
</section>
</div>

@ -0,0 +1,82 @@
<section class="container">
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<div class="card-body">
<h2 class="card-title"><%= gettext "Logs" %></h2>
<%= 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 %>
<div data-test="transaction_log" class="tile tile-muted">
<dl class="row">
<dt class="col-md-2"> <%= gettext "Transaction" %> </dt>
<dd class="col-md-10">
<h3 class="">
<%= link(
log.transaction,
to: transaction_path(@conn, :show, log.transaction),
"data-test": "log_address_link",
"data-address-hash": log.transaction
) %>
</h3>
</dd>
<dt class="col-md-2"><%= gettext "Topics" %></dt>
<dd class="col-md-10">
<div class="raw-transaction-log-topics">
<%= unless is_nil(log.first_topic) do %>
<div class="text-dark">
<span class="text-dark">[0]</span>
<%= log.first_topic %>
</div>
<% end %>
<%= unless is_nil(log.second_topic) do %>
<div class="text-dark">
<span class="">[1] </span>
<%= log.second_topic %>
</div>
<% end %>
<%= unless is_nil(log.third_topic) do %>
<div class="text-dark">
<span>[2]</span>
<%= log.third_topic %>
</div>
<% end %>
<%= unless is_nil(log.fourth_topic) do %>
<div class="text-dark">
<span>[3]</span>
<%= log.fourth_topic %>
</div>
<% end %>
</div>
</dd>
<dt class="col-md-2">
<%= gettext "Data" %>
</dt>
<dd class="col-md-10">
<%= unless is_nil(log.data) do %>
<div class="text-dark raw-transaction-log-data">
<%= log.data %>
</div>
<% end %>
</dd>
</dl>
</div>
<% end %>
<% else %>
<div class="tile tile-muted text-center">
<span><%= gettext "There are no logs for this address." %></span>
</div>
<% end %>
</div>
</div>
</section>

@ -1,6 +1,6 @@
<%= if Enum.any?(@token_balances) do %>
<a href="#"
class="text-white text-faded"
<a
href="#"
data-dropdown-toggle
data-toggle="dropdown"
role="button"

@ -1,6 +1,8 @@
defmodule BlockScoutWeb.AddressContractView do
use BlockScoutWeb, :view
alias Explorer.Chain.{Address, Data, InternalTransaction}
def render("scripts.html", %{conn: conn}) do
render_scripts(conn, "address_contract/code_highlighting.js")
end
@ -34,4 +36,15 @@ defmodule BlockScoutWeb.AddressContractView do
{value, String.pad_leading(to_string(line), max_digits, " ")}
end)
end
def contract_creation_code(%Address{
contract_code: %Data{bytes: <<>>},
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

@ -165,7 +165,10 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do
|> String.replace("\e[1m", "<span style=\"font-weight:bold\">")
|> String.replace("»", "&raquo;")
|> String.replace("\e[0m", "</span>")
|> String.split(~r/\<span style=.*?\)"\>|\<\/span\>/, include_captures: true, trim: true)
|> String.split(~r/\<span style=.*?\)"\>|\<span style=\"font-weight:bold\"\>|\<\/span\>/,
include_captures: true,
trim: true
)
|> add_styles_to_every_line()
result

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

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

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

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

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

@ -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, "<a href=\"#{release_link}\" class=\"footer-link\" target=\"_blank\">#{version}</a>"})
end

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

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

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

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

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

@ -56,7 +56,7 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do
result = AddressDecompiledContractView.highlight_decompiled_code(code)
assert result ==
"<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # eveem.org 6 Feb 2019</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Decompiled source of </span>0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Let's make the world open source</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # </span></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # I failed with these:</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> </span><span style=\"color:rgb(111, 110, 111)\"># - </span>unknowne77c646d(?)<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> </span><span style=\"color:rgb(111, 110, 111)\"># - </span>transferFromWithData(address _from, address _to, uint256 _value, bytes _data)<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # All the rest is below.</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># Storage definitions and getters</span></code>\n<code></code>\n<code> <span class=\"hljs-keyword\">def</span> storage:</code>\n<code> allowance is <span class=\"hljs-keyword\">uint</span>256 => <span class=\"hljs-keyword\">uint</span>256 <span style=\"color:rgb(111, 110, 111)\"># mask(256, 0) at storage #2</span></code>\n<code> stor4 is <span class=\"hljs-keyword\">uint</span>256 => <span class=\"hljs-keyword\">uint</span>8 <span style=\"color:rgb(111, 110, 111)\"># mask(8, 0) at storage #4</span></code>\n<code></code>\n<code> <span class=\"hljs-keyword\">def</span> allowance(<span class=\"hljs-keyword\">address</span> _owner, <span class=\"hljs-keyword\">address</span> _spender) <span class=\"hljs-title\">payable</span>: 64</code>\n<code> <span class=\"hljs-keyword\">return</span> allowance[_owner_spender(320 - 1))]</code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Regular functions - see Tutorial for understanding quirks of the code</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># folder failed in this function - may be terribly long, sorry</span></code>\n<code> <span class=\"hljs-keyword\">def</span> unknownc47d033b(?) <span class=\"hljs-title\">payable</span>: not cd[4]:</code>\n<code> <span class=\"hljs-keyword\">revert</span></code>\n<code> else:</code>\n<code> <span class=\"hljs-keyword\">mem</span>[0]cd[4]</code>\n<code> <span class=\"hljs-keyword\">mem</span>[32] = 4</code>\n<code> <span class=\"hljs-keyword\">mem</span>[96] = <span class=\"hljs-keyword\">bool</span>(stor4[cd[4])])</code>\n<code> <span class=\"hljs-keyword\">return</span> <span class=\"hljs-keyword\">bool</span>(stor4[cd[4])])</code>\n<code></code>\n<code> <span class=\"hljs-keyword\">def</span> _fallback() <span class=\"hljs-title\">payable</span>: <span style=\"color:rgb(111, 110, 111)\"># default function</span></code>\n<code> <span class=\"hljs-keyword\">revert</span></code>\n<code></code>\n<code></code>\n"
"<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # eveem.org 6 Feb 2019</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Decompiled source of </span>0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Let's make the world open source</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # </span></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # I failed with these:</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> </span><span style=\"color:rgb(111, 110, 111)\"># - </span>unknowne77c646d(?)<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> </span><span style=\"color:rgb(111, 110, 111)\"># - </span>transferFromWithData(address _from, address _to, uint256 _value, bytes _data)<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # All the rest is below.</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># Storage definitions and getters</span></code>\n<code></code>\n<code> <span class=\"hljs-keyword\">def</span> storage:</code>\n<code> allowance is <span class=\"hljs-keyword\">uint</span>256 => <span class=\"hljs-keyword\">uint</span>256 <span style=\"color:rgb(111, 110, 111)\"># mask(256, 0) at storage #2</span></code>\n<code> stor4 is <span class=\"hljs-keyword\">uint</span>256 => <span class=\"hljs-keyword\">uint</span>8 <span style=\"color:rgb(111, 110, 111)\"># mask(8, 0) at storage #4</span></code>\n<code></code>\n<code> <span class=\"hljs-keyword\">def</span> allowance(<span class=\"hljs-keyword\">address</span> _owner, <span class=\"hljs-keyword\">address</span> _spender) <span class=\"hljs-title\">payable</span>: </code>\n<code> <span class=\"hljs-keyword\">require</span> (calldata.size - 4)<span style=\"font-weight:bold\"> >= </span>64</code>\n<code> <span class=\"hljs-keyword\">return</span> allowance[sha3(((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>_owner), 1), ((320 - 1)<span style=\"font-weight:bold\"> and </span>_spender<span style=\"font-weight:bold\"> and </span>(320 - 1))]</code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Regular functions - see Tutorial for understanding quirks of the code</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code></code>\n<code></code>\n<code> <span style=\"color:rgb(111, 110, 111)\"># folder failed in this function - may be terribly long, sorry</span></code>\n<code> <span class=\"hljs-keyword\">def</span> unknownc47d033b(?) <span class=\"hljs-title\">payable</span>: </code>\n<code> if (calldata.size - 4)<span style=\"font-weight:bold\"> < </span>32:</code>\n<code> <span class=\"hljs-keyword\">revert</span></code>\n<code> else:</code>\n<code> if not (320 - 1)<span style=\"font-weight:bold\"> or </span>not cd[4]:</code>\n<code> <span class=\"hljs-keyword\">revert</span></code>\n<code> else:</code>\n<code> <span class=\"hljs-keyword\">mem</span>[0] = (320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4]</code>\n<code> <span class=\"hljs-keyword\">mem</span>[32] = 4</code>\n<code> <span class=\"hljs-keyword\">mem</span>[96] = <span class=\"hljs-keyword\">bool</span>(stor4[((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4])])</code>\n<code> <span class=\"hljs-keyword\">return</span> <span class=\"hljs-keyword\">bool</span>(stor4[((320 - 1)<span style=\"font-weight:bold\"> and </span>(320 - 1)<span style=\"font-weight:bold\"> and </span>cd[4])])</code>\n<code></code>\n<code> <span class=\"hljs-keyword\">def</span> _fallback() <span class=\"hljs-title\">payable</span>: <span style=\"color:rgb(111, 110, 111)\"># default function</span></code>\n<code> <span class=\"hljs-keyword\">revert</span></code>\n<code></code>\n<code></code>\n"
end
test "adds style span to every line" do
@ -72,6 +72,28 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do
assert AddressDecompiledContractView.highlight_decompiled_code(code) ==
"<code> <span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # eveem.org 6 Feb 2019</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Decompiled source of </span>0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> #</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # Let's make the world open source</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"> # </span></code>\n<code></code>\n<code></code>\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) ==
"<code><span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"># Eveem.org 26 Apr 2019</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"># Decompiled source of </span>0x06012c8cf97bead5deae237070f9587f8e7a266d<span style=\"color:rgb(111, 110, 111)\"></span></code>\n<code><span style=\"color:rgb(111, 110, 111)\">#</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"># Let's make the world open source</span></code>\n<code><span style=\"color:rgb(111, 110, 111)\"># </span></code>\n<code></code>\n<code>const name = <span style=\"font-weight:bold\">'CryptoKitties'</span></code>\n<code>const symbol = <span style=\"font-weight:bold\">'CK'</span></code>\n<code>const GEN0_STARTING_PRICE = <span style=\"font-weight:bold\">10^16</span></code>\n<code>const GEN0_AUCTION_DURATION = <span style=\"font-weight:bold\">(24 * 3600)</span></code>\n<code>const GEN0_CREATION_LIMIT = <span style=\"font-weight:bold\">45000</span></code>\n<code>const PROMO_CREATION_LIMIT = <span style=\"font-weight:bold\">5000</span></code>\n<code></code>\n<code></code>\n<code></code>\n"
end
end
describe "sort_contracts_by_version/1" do

@ -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(<a href="https://github.com/poanetwork/blockscout/releases/tag/v1.3.4-beta" class="footer-link" target="_blank">v1.3.4-beta</a>)}
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(<a href="https://github.com/poanetwork/blockscout/releases/tag/v1.3.4-beta" class="footer-link" target="_blank">v1.3.4-beta</a>)}
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(<a href="https://github.com/poanetwork/blockscout/releases/tag/v1.3.4-beta" class="footer-link" target="_blank">1.3.4</a>)}
~s(<a href="https://github.com/poanetwork/blockscout/releases/tag/v1.3.4-beta" class="footer-link" target="_blank">v1.3.4-beta</a>)}
end
end

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

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

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

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

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

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

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

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

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

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

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

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

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

File diff suppressed because it is too large Load Diff

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save