diff --git a/CHANGELOG.md b/CHANGELOG.md index a847de3b1d..f77cb5439a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,17 @@ ## Current ### Features +- [#2109](https://github.com/poanetwork/blockscout/pull/2109) - use bigger updates instead of `Multi` transactions in BlocksTransactionsMismatch - [#2075](https://github.com/poanetwork/blockscout/pull/2075) - add blocks cache +- [#2151](https://github.com/poanetwork/blockscout/pull/2151) - hide dropdown menu then other networks list is empty ### Fixes +- [#2162](https://github.com/poanetwork/blockscout/pull/2162) - contract creation tile color changed +- [#2144](https://github.com/poanetwork/blockscout/pull/2144) - 'page not found' images path fixed for goerli +- [#2142](https://github.com/poanetwork/blockscout/pull/2142) - Removed posdao theme and logo, added 'page not found' image for goerli +- [#2138](https://github.com/poanetwork/blockscout/pull/2138) - badge colors issue, api titles issue +- [#2129](https://github.com/poanetwork/blockscout/pull/2129) - Fix for width of explorer elements +- [#2121](https://github.com/poanetwork/blockscout/pull/2121) - Binding of 404 page - [#2120](https://github.com/poanetwork/blockscout/pull/2120) - footer links and socials focus color issue - [#2113](https://github.com/poanetwork/blockscout/pull/2113) - renewed logos for rsk, dai, blockscout; themes color changes for lukso; error images for lukso - [#2112](https://github.com/poanetwork/blockscout/pull/2112) - themes color improvements, dropdown color issue @@ -12,7 +20,17 @@ - [#2090](https://github.com/poanetwork/blockscout/pull/2090) - updated some ETC theme colors - [#2096](https://github.com/poanetwork/blockscout/pull/2096) - RSK theme fixes - [#2093](https://github.com/poanetwork/blockscout/pull/2093) - detect token transfer type for deprecated erc721 spec -- [#2108](https://github.com/poanetwork/blockscout/pull/2108) - fixe uncle fetching without full transactions +- [#2111](https://github.com/poanetwork/blockscout/pull/2111) - improve address transaction controller +- [#2108](https://github.com/poanetwork/blockscout/pull/2108) - fix uncle fetching without full transactions +- [#2128](https://github.com/poanetwork/blockscout/pull/2128) - add new function clause for uncle errors +- [#2123](https://github.com/poanetwork/blockscout/pull/2123) - fix coins percentage view +- [#2119](https://github.com/poanetwork/blockscout/pull/2119) - fix map logging +- [#2130](https://github.com/poanetwork/blockscout/pull/2130) - fix navigation +- [#2149](https://github.com/poanetwork/blockscout/pull/2149) - remove pending transaction count + +### Chore +- [#2127](https://github.com/poanetwork/blockscout/pull/2127) - use previouse chromedriver version +- [#2118](https://github.com/poanetwork/blockscout/pull/2118) - show only the last decompiled contract ### Chore @@ -41,6 +59,7 @@ - [#2037](https://github.com/poanetwork/blockscout/pull/2037) - add address logs search functionality - [#2012](https://github.com/poanetwork/blockscout/pull/2012) - make all pages pagination async - [#2064](https://github.com/poanetwork/blockscout/pull/2064) - feat: add fields to tx apis, small cleanups +- [#2100](https://github.com/poanetwork/blockscout/pull/2100) - feat: eth_get_balance rpc endpoint ### Fixes - [#2099](https://github.com/poanetwork/blockscout/pull/2099) - logs search input width diff --git a/README.md b/README.md index f53a43e897..e0e9ece96b 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ Currently available block explorers (i.e. Etherscan and Etherchain) are closed s | [xDai Chain](https://blockscout.com/poa/dai) | | [SafeChain](https://explorer.safechain.io) | | | | [SpringChain](https://explorer.springrole.com/) | | | | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | +| | | [Loom](http://plasma-blockexplorer.dappchains.com/) | +| | | [Tenda](https://tenda.network) | ### Visual Interface diff --git a/apps/block_scout_web/assets/css/components/_api.scss b/apps/block_scout_web/assets/css/components/_api.scss index 6fc2bc9a97..820e1e8f01 100644 --- a/apps/block_scout_web/assets/css/components/_api.scss +++ b/apps/block_scout_web/assets/css/components/_api.scss @@ -1,7 +1,7 @@ -$api-text-monospace-color: $primary !default; +$api-text-monospace-color: $secondary !default; $api-text-monospace-background: rgba($api-text-monospace-color, 0.1) !default; $api-anchors-list-background-color: #f6f7f9 !default; -$api-doc-list-item-title-color: $primary !default; +$api-doc-list-item-title-color: #333 !default; $api-doc-list-item-view-more-color: $api-doc-list-item-title-color !default; .api-text-monospace { @@ -89,8 +89,8 @@ $api-doc-list-item-view-more-color: $api-doc-list-item-title-color !default; .api-doc-list-item-title { color: $api-doc-list-item-title-color; - font-size: 17px; - font-weight: 700; + font-size: 15px; + font-weight: 400; line-height: 1.2; margin: 0 0 15px; } diff --git a/apps/block_scout_web/assets/css/components/_badge.scss b/apps/block_scout_web/assets/css/components/_badge.scss index 2891a6e4ff..9fad8b41dd 100644 --- a/apps/block_scout_web/assets/css/components/_badge.scss +++ b/apps/block_scout_web/assets/css/components/_badge.scss @@ -2,8 +2,8 @@ $badge-success-color: #15bba6 !default; $badge-success-background-color: rgba($badge-success-color, 0.1) !default; $badge-danger-color: #ed9966 !default; $badge-danger-background-color: rgba($badge-danger-color, 0.1) !default; -$badge-neutral-color: #333 !default; -$badge-neutral-background-color: #e9e9e9 !default; +$badge-neutral-color: $secondary !default; +$badge-neutral-background-color: rgba($secondary, .1) !default; .badge { color: $white; diff --git a/apps/block_scout_web/assets/css/components/_tile.scss b/apps/block_scout_web/assets/css/components/_tile.scss index d5658647c5..f86ba1a64f 100644 --- a/apps/block_scout_web/assets/css/components/_tile.scss +++ b/apps/block_scout_web/assets/css/components/_tile.scss @@ -4,7 +4,7 @@ $tile-type-reorg-color: $purple !default; $tile-type-emission-reward-color: $lilac !default; $tile-type-transaction-color: $blue !default; $tile-type-contract-call-color: $green !default; -$tile-type-contract-creation-color: $pink !default; +$tile-type-contract-creation-color: $dark-purple !default; $tile-type-token-transfer-color: $orange !default; $tile-type-unique-token-color: $orange !default; $tile-type-unique-token-image-color: $orange !default; diff --git a/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss b/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss index a3ea681eda..ce8cbdb926 100644 --- a/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss +++ b/apps/block_scout_web/assets/css/components/_verify_other_explorers.scss @@ -18,9 +18,13 @@ line-height: 1.25; display: inline-flex; margin-bottom: 12px; - @media (min-width: 1200px) { + width: 100%; + @media (min-width: 768px) { margin-right: 10px; + } + @media (min-width: 1200px) { margin-bottom: 0; + width: auto; } } } @@ -31,6 +35,8 @@ flex-grow: 2; @media (min-width: 768px) { flex-direction: row; + position: relative; + padding-right: 44px; } } @@ -41,6 +47,7 @@ flex-grow: 2; @media (min-width: 768px) { margin-top: 0; + max-width: 188px; } @media (min-width: 1200px) { min-width: 145px; @@ -76,7 +83,7 @@ } .exp-content { - padding: 6px 9px 4px 9px; + padding: 6px 9px 5px 9px; h3, div { font-size: 10px; line-height: 1; @@ -117,7 +124,7 @@ display: inline-flex; align-items: center; justify-content: center; - border: 1px solid $secondary; + border: 1px solid $btn-line-color; border-radius: 2px; margin-top: 10px; transition: .1s ease-in; @@ -125,12 +132,15 @@ @media (min-width: 768px) { margin-left: 10px; margin-top: 0; + position: absolute; + top: 0; + right: 0; } svg path { - fill: $secondary; + fill: $btn-line-color; } &:hover { - background-color: $secondary; + background-color: $btn-line-color; svg path { fill: #fff; } diff --git a/apps/block_scout_web/assets/css/theme/_base_variables.scss b/apps/block_scout_web/assets/css/theme/_base_variables.scss index 3e97eb61b3..9f1b82c887 100644 --- a/apps/block_scout_web/assets/css/theme/_base_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_base_variables.scss @@ -47,6 +47,7 @@ $yellow: #ffc107 !default; $green: #20b760 !default; $teal: #009097 !default; $cyan: #90e1d8 !default; +$dark-purple: #923dc3; $colors: () !default; $colors: map-merge( diff --git a/apps/block_scout_web/assets/css/theme/_dai_variables.scss b/apps/block_scout_web/assets/css/theme/_dai_variables.scss index 1e19866bbe..50cbcfa290 100644 --- a/apps/block_scout_web/assets/css/theme/_dai_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_dai_variables.scss @@ -57,4 +57,9 @@ $card-tab-active: $secondary; $dashboard-banner-gradient-end ); } -} \ No newline at end of file +} + +// Badges +$badge-neutral-color: #20446e; +$badge-neutral-background-color: rgba(#20446e, .1); +$api-text-monospace-color: #20446e; \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss b/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss index 9bec5a1243..68feab4dfc 100644 --- a/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_ethereum_classic_variables.scss @@ -70,3 +70,8 @@ $card-tab-active: $tertiary; filter: brightness(0) invert(1); } } + +// Badges +$badge-neutral-color: $tertiary; +$badge-neutral-background-color: rgba($tertiary, .1); +$api-text-monospace-color: $tertiary; \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/theme/_goerli_variables.scss b/apps/block_scout_web/assets/css/theme/_goerli_variables.scss index 3ab3e4d089..7900dd4c3b 100644 --- a/apps/block_scout_web/assets/css/theme/_goerli_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_goerli_variables.scss @@ -73,3 +73,8 @@ $card-tab-active: $sub-accent-color; ); } } + +// Badges +$badge-neutral-color: $sub-accent-color; +$badge-neutral-background-color: rgba($sub-accent-color, .1); +$api-text-monospace-color: $sub-accent-color; diff --git a/apps/block_scout_web/assets/css/theme/_kovan_variables.scss b/apps/block_scout_web/assets/css/theme/_kovan_variables.scss index 821232bbd5..351f046726 100644 --- a/apps/block_scout_web/assets/css/theme/_kovan_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_kovan_variables.scss @@ -67,4 +67,11 @@ $card-tab-active: $tertiary; $dashboard-banner-gradient-end ); } -} \ No newline at end of file +} + +// Badges +$badge-success-color: #15bba6; +$badge-success-background-color: rgba(#15bba6, .1); +$badge-neutral-color: $tertiary; +$badge-neutral-background-color: rgba($tertiary, .1); +$api-text-monospace-color: $tertiary; \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/theme/_lukso_variables.scss b/apps/block_scout_web/assets/css/theme/_lukso_variables.scss index cfa415b7ae..65f9a73b5f 100644 --- a/apps/block_scout_web/assets/css/theme/_lukso_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_lukso_variables.scss @@ -146,3 +146,8 @@ $dashboard-banner-network-plain-container-height: 150px; } } } + +// Badges +$badge-neutral-color: $tertiary; +$badge-neutral-background-color: rgba($tertiary, .1); +$api-text-monospace-color: $tertiary; diff --git a/apps/block_scout_web/assets/css/theme/_neutral_variables.scss b/apps/block_scout_web/assets/css/theme/_neutral_variables.scss index 8b6676a856..8a0a62d26e 100644 --- a/apps/block_scout_web/assets/css/theme/_neutral_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_neutral_variables.scss @@ -59,4 +59,9 @@ $card-tab-active: $primary; $dashboard-banner-gradient-end ); } -} \ No newline at end of file +} + +// Badges +$badge-neutral-color: $primary; +$badge-neutral-background-color: rgba($primary, .1); +$api-text-monospace-color: $primary; \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/theme/_poa_variables.scss b/apps/block_scout_web/assets/css/theme/_poa_variables.scss index 8b6676a856..8a0a62d26e 100644 --- a/apps/block_scout_web/assets/css/theme/_poa_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_poa_variables.scss @@ -59,4 +59,9 @@ $card-tab-active: $primary; $dashboard-banner-gradient-end ); } -} \ No newline at end of file +} + +// Badges +$badge-neutral-color: $primary; +$badge-neutral-background-color: rgba($primary, .1); +$api-text-monospace-color: $primary; \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/theme/_posdao_variables.scss b/apps/block_scout_web/assets/css/theme/_posdao_variables.scss deleted file mode 100644 index 9d92ca2adc..0000000000 --- a/apps/block_scout_web/assets/css/theme/_posdao_variables.scss +++ /dev/null @@ -1,29 +0,0 @@ -$primary: #15bba6; -$secondary: #17314f; -$tertiary: #00ff00; - -$header-links-color-active: #333; -$dashboard-banner-gradient-start: $secondary; -$dashboard-banner-gradient-end: #1e4168; - -$dashboard-line-color-market: $primary; - -$tile-type-block-border-color: $secondary; -$tile-type-block-color: #333; - -$footer-background-color: #173250; -$footer-text-color: #909dac; - -$navbar-logo-height: auto; -$navbar-logo-width: 100px; - -$footer-logo-height: auto; -$footer-logo-width: 100px; - -$card-background-1: $secondary; -$card-background-1-text-color: #fff; - -$btn-copy-color: $secondary; -$btn-qr-color: $secondary; - -$btn-dropdown-line-color: $secondary; \ No newline at end of file diff --git a/apps/block_scout_web/assets/css/theme/_rsk_variables.scss b/apps/block_scout_web/assets/css/theme/_rsk_variables.scss index e7536db13c..76029e1a34 100644 --- a/apps/block_scout_web/assets/css/theme/_rsk_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_rsk_variables.scss @@ -61,3 +61,7 @@ $card-tab-active: $secondary; filter: brightness(0) invert(1); } } + +// Badges +$badge-neutral-color: #1a323b; +$badge-neutral-background-color: rgba(#1a323b, .1); diff --git a/apps/block_scout_web/assets/css/theme/_sokol_variables.scss b/apps/block_scout_web/assets/css/theme/_sokol_variables.scss index 2d80e64f03..71822bdc5a 100644 --- a/apps/block_scout_web/assets/css/theme/_sokol_variables.scss +++ b/apps/block_scout_web/assets/css/theme/_sokol_variables.scss @@ -65,3 +65,7 @@ $card-tab-active: $sub-accent-color; ); } } + +// Badges +$badge-neutral-color: $tertiary; +$badge-neutral-background-color: rgba($tertiary, .1); \ No newline at end of file diff --git a/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found.png b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found.png new file mode 100644 index 0000000000..8758824715 Binary files /dev/null and b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found.png differ diff --git a/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found@2x.png b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found@2x.png new file mode 100644 index 0000000000..19557af509 Binary files /dev/null and b/apps/block_scout_web/assets/static/images/errors-img/goerli-page-not-found@2x.png differ diff --git a/apps/block_scout_web/assets/static/images/goerli_logo.png b/apps/block_scout_web/assets/static/images/goerli_logo.png deleted file mode 100644 index 910a7b2362..0000000000 Binary files a/apps/block_scout_web/assets/static/images/goerli_logo.png and /dev/null differ diff --git a/apps/block_scout_web/assets/static/images/posdao_logo.png b/apps/block_scout_web/assets/static/images/posdao_logo.png deleted file mode 100755 index 535b0ab6ac..0000000000 Binary files a/apps/block_scout_web/assets/static/images/posdao_logo.png and /dev/null differ diff --git a/apps/block_scout_web/assets/static/images/posdao_logo_footer.png b/apps/block_scout_web/assets/static/images/posdao_logo_footer.png deleted file mode 100755 index 1b49d8eff2..0000000000 Binary files a/apps/block_scout_web/assets/static/images/posdao_logo_footer.png and /dev/null differ diff --git a/apps/block_scout_web/assets/static/images/posdao_logo_footer.svg b/apps/block_scout_web/assets/static/images/posdao_logo_footer.svg deleted file mode 100644 index 9a26bec27f..0000000000 --- a/apps/block_scout_web/assets/static/images/posdao_logo_footer.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/apps/block_scout_web/lib/block_scout_web/controller.ex b/apps/block_scout_web/lib/block_scout_web/controller.ex index fe2851c197..cf545f5260 100644 --- a/apps/block_scout_web/lib/block_scout_web/controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controller.ex @@ -12,8 +12,9 @@ defmodule BlockScoutWeb.Controller do def not_found(conn) do conn |> put_status(:not_found) - |> put_view(BlockScoutWeb.ErrorView) - |> render("404.html") + |> put_view(BlockScoutWeb.PageNotFoundView) + |> render(:index) + |> halt() end def unprocessable_entity(conn) do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index da22b8c042..6742d01b60 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -28,7 +28,7 @@ defmodule BlockScoutWeb.AddressTransactionController do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do + {:ok, address} <- Chain.hash_to_address(address_hash, [:names], false) do options = @transaction_necessity_by_association |> put_in([:necessity_by_association, :block], :required) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex index be10089bad..3150d3bd60 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/address_controller.ex @@ -20,6 +20,35 @@ defmodule BlockScoutWeb.API.RPC.AddressController do |> render(:listaccounts, %{accounts: accounts}) end + def eth_get_balance(conn, params) do + with {:address_param, {:ok, address_param}} <- fetch_address(params), + {:block_param, {:ok, block}} <- {:block_param, fetch_block_param(params)}, + {:format, {:ok, address_hash}} <- to_address_hash(address_param), + {:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address_hash, block)} do + render(conn, :eth_get_balance, %{balance: Wei.hex_format(balance)}) + else + {:address_param, :error} -> + conn + |> put_status(400) + |> render(:eth_get_balance_error, %{message: "Query parameter 'address' is required"}) + + {:format, :error} -> + conn + |> put_status(400) + |> render(:eth_get_balance_error, %{error: "Invalid address hash"}) + + {:block_param, :error} -> + conn + |> put_status(400) + |> render(:eth_get_balance_error, %{error: "Invalid block"}) + + {:balance, {:error, :not_found}} -> + conn + |> put_status(404) + |> render(:eth_get_balance_error, %{error: "Balance not found"}) + end + end + def balance(conn, params, template \\ :balance) do with {:address_param, {:ok, address_param}} <- fetch_address(params), {:format, {:ok, address_hashes}} <- to_address_hashes(address_param) do @@ -217,6 +246,20 @@ defmodule BlockScoutWeb.API.RPC.AddressController do {:required_params, result} end + defp fetch_block_param(%{"block" => "latest"}), do: {:ok, :latest} + defp fetch_block_param(%{"block" => "earliest"}), do: {:ok, :earliest} + defp fetch_block_param(%{"block" => "pending"}), do: {:ok, :pending} + + defp fetch_block_param(%{"block" => string_integer}) when is_bitstring(string_integer) do + case Integer.parse(string_integer) do + {integer, ""} -> {:ok, integer} + _ -> :error + end + end + + defp fetch_block_param(%{"block" => _block}), do: :error + defp fetch_block_param(_), do: {:ok, :latest} + defp to_valid_format(params, :tokenbalance) do result = with {:ok, contract_address_hash} <- to_address_hash(params, "contractaddress"), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex new file mode 100644 index 0000000000..693772ed8c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/eth_controller.ex @@ -0,0 +1,118 @@ +defmodule BlockScoutWeb.API.RPC.EthController do + use BlockScoutWeb, :controller + + alias Explorer.Chain + alias Explorer.Chain.Wei + + def eth_request(%{body_params: %{"_json" => requests}} = conn, _) when is_list(requests) do + responses = responses(requests) + + conn + |> put_status(200) + |> render("responses.json", %{responses: responses}) + end + + def eth_request(%{body_params: %{"_json" => request}} = conn, _) do + [response] = responses([request]) + + conn + |> put_status(200) + |> render("response.json", %{response: response}) + end + + def eth_request(conn, request) do + # In the case that the JSON body is sent up w/o a json content type, + # Phoenix encodes it as a single key value pair, with the value being + # nil and the body being the key (as in a CURL request w/ no content type header) + decoded_request = + with [{single_key, nil}] <- Map.to_list(request), + {:ok, decoded} <- Jason.decode(single_key) do + decoded + else + _ -> request + end + + [response] = responses([decoded_request]) + + conn + |> put_status(200) + |> render("response.json", %{response: response}) + end + + defp responses(requests) do + Enum.map(requests, fn request -> + with {:id, {:ok, id}} <- {:id, Map.fetch(request, "id")}, + {:request, {:ok, result}} <- {:request, do_eth_request(request)} do + format_success(result, id) + else + {:id, :error} -> format_error("id is a required field", 0) + {:request, {:error, message}} -> format_error(message, Map.get(request, "id")) + end + end) + end + + defp format_success(result, id) do + %{result: result, id: id} + end + + defp format_error(message, id) do + %{error: message, id: id} + end + + defp do_eth_request(%{"jsonrpc" => rpc_version}) when rpc_version != "2.0" do + {:error, "invalid rpc version"} + end + + defp do_eth_request(%{"jsonrpc" => "2.0", "method" => method, "params" => params}) + when is_list(params) do + with {:ok, action} <- get_action(method), + true <- :erlang.function_exported(__MODULE__, action, Enum.count(params)) do + apply(__MODULE__, action, params) + else + _ -> + {:error, "Action not found."} + end + end + + defp do_eth_request(%{"params" => _params, "method" => _}) do + {:error, "Invalid params. Params must be a list."} + end + + defp do_eth_request(_) do + {:error, "Method, params, and jsonrpc, are all required parameters."} + end + + def eth_get_balance(address_param, block_param \\ nil) do + with {:address, {:ok, address}} <- {:address, Chain.string_to_address_hash(address_param)}, + {:block, {:ok, block}} <- {:block, block_param(block_param)}, + {:balance, {:ok, balance}} <- {:balance, Chain.get_balance_as_of_block(address, block)} do + {:ok, Wei.hex_format(balance)} + else + {:address, :error} -> + {:error, "Query parameter 'address' is invalid"} + + {:block, :error} -> + {:error, "Query parameter 'block' is invalid"} + + {:balance, {:error, :not_found}} -> + {:error, "Balance not found"} + end + end + + defp get_action("eth_getBalance"), do: {:ok, :eth_get_balance} + defp get_action(_), do: :error + + defp block_param("latest"), do: {:ok, :latest} + defp block_param("earliest"), do: {:ok, :earliest} + defp block_param("pending"), do: {:ok, :pending} + + defp block_param(string_integer) when is_bitstring(string_integer) do + case Integer.parse(string_integer) do + {integer, ""} -> {:ok, integer} + _ -> :error + end + end + + defp block_param(nil), do: {:ok, :latest} + defp block_param(_), do: :error +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/page_not_found_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/page_not_found_controller.ex new file mode 100644 index 0000000000..e453e5463b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/page_not_found_controller.ex @@ -0,0 +1,8 @@ +defmodule BlockScoutWeb.PageNotFoundController do + use BlockScoutWeb, :controller + + def index(conn, _params) do + conn + |> render("index.html") + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex index 95ffe8aa22..f0f9626f89 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/pending_transaction_controller.ex @@ -51,10 +51,7 @@ defmodule BlockScoutWeb.PendingTransactionController do end def index(conn, _params) do - render(conn, "index.html", - current_path: current_path(conn), - pending_transaction_count: Chain.pending_transaction_count() - ) + render(conn, "index.html", current_path: current_path(conn)) end defp get_pending_transactions_and_next_page(options) do diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 74a9fa761c..2191a86fa2 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -100,6 +100,12 @@ defmodule BlockScoutWeb.Etherscan do "result" => [] } + @account_eth_get_balance_example_value %{ + "jsonrpc" => "2.0", + "result" => "0x0234c8a3397aab58", + "id" => 1 + } + @account_tokentx_example_value %{ "status" => "1", "message" => "OK", @@ -1028,6 +1034,49 @@ defmodule BlockScoutWeb.Etherscan do } } + @account_eth_get_balance_action %{ + name: "eth_get_balance", + description: + "Mimics Ethereum JSON RPC's eth_getBalance. Returns the balance as of the provided block (defaults to latest)", + required_params: [ + %{ + key: "address", + placeholder: "addressHash", + type: "string", + description: "The address of the account." + } + ], + optional_params: [ + %{ + key: "block", + placeholder: "block", + type: "string", + description: """ + Either the block number as a string, or one of latest, earliest or pending + + latest will be the latest balance in a *consensus* block. + earliest will be the first recorded balance for the address. + pending will be the latest balance in consensus *or* nonconcensus blocks. + """ + } + ], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@account_eth_get_balance_example_value), + model: %{ + name: "Result", + fields: %{ + jsonrpc: @jsonrpc_version_type, + id: @id_type, + result: @hex_number_type + } + } + } + ] + } + @account_balance_action %{ name: "balance", description: """ @@ -2203,6 +2252,7 @@ defmodule BlockScoutWeb.Etherscan do @account_module %{ name: "account", actions: [ + @account_eth_get_balance_action, @account_balance_action, @account_balancemulti_action, @account_txlist_action, diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index aa19725811..786c77cdff 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -32,6 +32,8 @@ defmodule BlockScoutWeb.Router do alias BlockScoutWeb.API.RPC + post("/eth_rpc", EthController, :eth_request) + forward("/", RPCTranslator, %{ "block" => RPC.BlockController, "account" => RPC.AddressController, @@ -245,5 +247,7 @@ defmodule BlockScoutWeb.Router do get("/chain_blocks", ChainController, :chain_blocks, as: :chain_blocks) get("/api_docs", APIDocsController, :index) + + get("/:page", PageNotFoundController, :index) end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex index 270450c2c0..c2e62e2004 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex @@ -39,7 +39,7 @@ - <% if @total_supply do %> + <%= if @total_supply do %> (<%= balance_percentage(@address, @total_supply) %>) <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex index 173744603c..6cb4ccf6ab 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_decompiled_contract/index.html.eex @@ -2,7 +2,8 @@ <%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> - <%= for contract <- sort_contracts_by_version(@address.decompiled_smart_contracts) do %> + <% contract = last_decompiled_contract_version(@address.decompiled_smart_contracts) %> + <%= if contract do %>

<%= gettext "Decompiler version" %>

@@ -21,6 +22,10 @@
+ <% else %> +
+ <%= gettext "There is no decompilded contracts for this address." %> +
<% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex index 2f9f470eed..f9992c6d86 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/common_components/_pagination_container.html.eex @@ -1,4 +1,4 @@ -
disabled<% end %>> +
<%= if false do %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index de88489330..73a92c7cfc 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -77,7 +77,7 @@
-

Lorem Ipsum Dolor

-

Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua

+

Page not found

+

The requested path was not found on BlockScout.

Back Home
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex index b92d356479..438f344199 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex @@ -230,10 +230,8 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do end) end - def sort_contracts_by_version(decompiled_contracts) do - decompiled_contracts - |> Enum.sort_by(& &1.decompiler_version) - |> Enum.reverse() + def last_decompiled_contract_version(decompiled_contracts) do + Enum.max_by(decompiled_contracts, & &1.decompiler_version) end defp add_line_numbers(code) do diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex index 7aaf26dba0..248dc942c7 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/address_view.ex @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.API.RPC.AddressView do use BlockScoutWeb, :view - alias BlockScoutWeb.API.RPC.RPCView + alias BlockScoutWeb.API.RPC.{EthRPCView, RPCView} def render("listaccounts.json", %{accounts: accounts}) do accounts = Enum.map(accounts, &prepare_account/1) @@ -51,6 +51,10 @@ defmodule BlockScoutWeb.API.RPC.AddressView do RPCView.render("show.json", data: data) end + def render("eth_get_balance_error.json", %{error: message}) do + EthRPCView.render("error.json", %{error: message, id: 0}) + end + def render("error.json", assigns) do RPCView.render("error.json", assigns) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex index 39eb5ae9d1..5dda92d3d5 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_rpc_view.ex @@ -17,16 +17,48 @@ defmodule BlockScoutWeb.API.RPC.EthRPCView do } end + def render("response.json", %{response: %{error: error, id: id}}) do + %__MODULE__{ + error: error, + id: id + } + end + + def render("response.json", %{response: %{result: result, id: id}}) do + %__MODULE__{ + result: result, + id: id + } + end + + def render("responses.json", %{responses: responses}) do + Enum.map(responses, fn + %{error: error, id: id} -> + %__MODULE__{ + error: error, + id: id + } + + %{result: result, id: id} -> + %__MODULE__{ + result: result, + id: id + } + end) + 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 + result = Poison.encode!(result) + """ - {"jsonrpc":"2.0","result":"#{result}","id":#{id}} + {"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}} + {"jsonrpc":"2.0","error": "#{error}","id": #{id}} """ end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex new file mode 100644 index 0000000000..739f3ac8a7 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/eth_view.ex @@ -0,0 +1,13 @@ +defmodule BlockScoutWeb.API.RPC.EthView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.RPC.EthRPCView + + def render("responses.json", %{responses: responses}) do + EthRPCView.render("responses.json", %{responses: responses}) + end + + def render("response.json", %{response: response}) do + EthRPCView.render("response.json", %{response: response}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex b/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex new file mode 100644 index 0000000000..b5a18f0434 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/page_not_found.ex @@ -0,0 +1,5 @@ +defmodule BlockScoutWeb.PageNotFoundView do + use BlockScoutWeb, :view + + @dialyzer :no_match +end diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index e9763e2287..6a5a9d2e36 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -1490,7 +1490,7 @@ msgid "EVM Version" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:16 +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:17 msgid "Copy Decompiled Contract Code" msgstr "" @@ -1505,12 +1505,12 @@ msgid "Decompiled code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:14 +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:15 msgid "Decompiled contract code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:7 +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:8 msgid "Decompiler version" msgstr "" @@ -1697,3 +1697,8 @@ msgstr "" #: lib/block_scout_web/templates/transaction/overview.html.eex:178 msgid " Token Transfer" msgstr "" + +#, elixir-format +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27 +msgid "There is no decompilded contracts for this address." +msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 654a848278..49ccd3f61a 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -1490,7 +1490,7 @@ msgid "EVM Version" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:16 +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:17 msgid "Copy Decompiled Contract Code" msgstr "" @@ -1505,12 +1505,12 @@ msgid "Decompiled code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:14 +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:15 msgid "Decompiled contract code" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:7 +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:8 msgid "Decompiler version" msgstr "" @@ -1693,7 +1693,12 @@ msgstr "" msgid "New Smart Contract Verification" msgstr "" -#, elixir-format, fuzzy +#, elixir-format #: lib/block_scout_web/templates/transaction/overview.html.eex:178 msgid " Token Transfer" msgstr "" + +#, elixir-format, fuzzy +#: lib/block_scout_web/templates/address_decompiled_contract/index.html.eex:27 +msgid "There is no decompilded contracts for this address." +msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs new file mode 100644 index 0000000000..b26becee2f --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs @@ -0,0 +1,199 @@ +defmodule BlockScoutWeb.API.RPC.EthControllerTest do + use BlockScoutWeb.ConnCase, async: false + + alias Explorer.Counters.{AddressesWithBalanceCounter, AverageBlockTime} + alias Indexer.Fetcher.CoinBalanceOnDemand + + setup do + mocked_json_rpc_named_arguments = [ + transport: EthereumJSONRPC.Mox, + transport_options: [] + ] + + start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor}) + start_supervised!(AverageBlockTime) + start_supervised!({CoinBalanceOnDemand, [mocked_json_rpc_named_arguments, [name: CoinBalanceOnDemand]]}) + start_supervised!(AddressesWithBalanceCounter) + + Application.put_env(:explorer, AverageBlockTime, enabled: true) + + on_exit(fn -> + Application.put_env(:explorer, AverageBlockTime, enabled: false) + end) + + :ok + end + + defp params(api_params, params), do: Map.put(api_params, "params", params) + + describe "eth_get_balance" do + setup do + %{ + api_params: %{ + "method" => "eth_getBalance", + "jsonrpc" => "2.0", + "id" => 0 + } + } + end + + test "with an invalid address", %{conn: conn, api_params: api_params} do + assert response = + conn + |> post("/api/eth_rpc", params(api_params, ["badHash"])) + |> json_response(200) + + assert %{"error" => "Query parameter 'address' is invalid"} = response + end + + test "with a valid address that has no balance", %{conn: conn, api_params: api_params} do + address = insert(:address) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash)])) + |> json_response(200) + + assert %{"error" => "Balance not found"} = response + end + + test "with a valid address that has a balance", %{conn: conn, api_params: api_params} do + block = insert(:block) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash)])) + |> json_response(200) + + assert %{"result" => "0x1"} = response + end + + test "with a valid address that has no earliest balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 1) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"])) + |> json_response(200) + + assert response["error"] == "Balance not found" + end + + test "with a valid address that has an earliest balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 0) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "earliest"])) + |> json_response(200) + + assert response["result"] == "0x1" + end + + test "with a valid address and no pending balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 1, consensus: true) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"])) + |> json_response(200) + + assert response["error"] == "Balance not found" + end + + test "with a valid address and a pending balance", %{conn: conn, api_params: api_params} do + block = insert(:block, number: 1, consensus: false) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"])) + |> json_response(200) + + assert response["result"] == "0x1" + end + + test "with a valid address and a pending balance after a consensus block", %{conn: conn, api_params: api_params} do + insert(:block, number: 1, consensus: true) + block = insert(:block, number: 2, consensus: false) + address = insert(:address) + + insert(:fetched_balance, block_number: block.number, address_hash: address.hash, value: 1) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "pending"])) + |> json_response(200) + + assert response["result"] == "0x1" + end + + test "with a block provided", %{conn: conn, api_params: api_params} do + address = insert(:address) + + insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1) + insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2) + insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"])) + |> json_response(200) + + assert response["result"] == "0x2" + end + + test "with a block provided and no balance", %{conn: conn, api_params: api_params} do + address = insert(:address) + + insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) + + assert response = + conn + |> post("/api/eth_rpc", params(api_params, [to_string(address.hash), "2"])) + |> json_response(200) + + assert response["error"] == "Balance not found" + end + + test "with a batch of requests", %{conn: conn} do + address = insert(:address) + + insert(:fetched_balance, block_number: 1, address_hash: address.hash, value: 1) + insert(:fetched_balance, block_number: 2, address_hash: address.hash, value: 2) + insert(:fetched_balance, block_number: 3, address_hash: address.hash, value: 3) + + params = [ + %{"id" => 0, "params" => [to_string(address.hash), "1"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}, + %{"id" => 1, "params" => [to_string(address.hash), "2"], "jsonrpc" => "2.0", "method" => "eth_getBalance"}, + %{"id" => 2, "params" => [to_string(address.hash), "3"], "jsonrpc" => "2.0", "method" => "eth_getBalance"} + ] + + assert response = + conn + |> put_req_header("content-type", "application/json") + |> post("/api/eth_rpc", Jason.encode!(params)) + |> json_response(200) + + assert [ + %{"id" => 0, "result" => "0x1"}, + %{"id" => 1, "result" => "0x2"}, + %{"id" => 2, "result" => "0x3"} + ] = response + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs index d535b309f8..d632154294 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/pending_transaction_controller_test.exs @@ -37,15 +37,6 @@ defmodule BlockScoutWeb.PendingTransactionControllerTest do refute hd(json_response(conn, 200)["items"]) =~ to_string(dropped_replaced.hash) end - test "returns a count of pending transactions", %{conn: conn} do - insert(:transaction) - - conn = get(conn, pending_transaction_path(BlockScoutWeb.Endpoint, :index)) - - assert html_response(conn, 200) - assert 1 == conn.assigns.pending_transaction_count - end - test "works when there are no transactions", %{conn: conn} do conn = get(conn, pending_transaction_path(conn, :index)) diff --git a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs index 334ea36baa..abac26beae 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs @@ -96,15 +96,15 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do end end - describe "sort_contracts_by_version/1" do - test "sorts contracts in lexicographical order" do + describe "last_decompiled_contract_version/1" do + test "returns last version" do contract2 = insert(:decompiled_smart_contract, decompiler_version: "v2") contract1 = insert(:decompiled_smart_contract, decompiler_version: "v1") contract3 = insert(:decompiled_smart_contract, decompiler_version: "v3") - result = AddressDecompiledContractView.sort_contracts_by_version([contract2, contract1, contract3]) + result = AddressDecompiledContractView.last_decompiled_contract_version([contract2, contract1, contract3]) - assert result == [contract3, contract2, contract1] + assert result == contract3 end end end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 4edc94887a..9c203ad3e4 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -227,60 +227,31 @@ defmodule Explorer.Chain do transaction_hashes_from_token_transfers = TokenTransfer.where_any_address_fields_match(direction, address_hash, paging_options) - token_transfers_query = - transaction_hashes_from_token_transfers - |> Transaction.where_transaction_hashes_match() - |> join_associations(necessity_by_association) - |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) - |> Transaction.preload_token_transfers(address_hash) - - base_query = + transactions_list = paging_options |> fetch_transactions() + |> Transaction.where_transaction_matches(transaction_hashes_from_token_transfers, direction, address_hash) |> join_associations(necessity_by_association) |> Transaction.preload_token_transfers(address_hash) + |> Repo.all() - from_address_query = - base_query - |> where([t], t.from_address_hash == ^address_hash) - - to_address_query = - base_query - |> where([t], t.to_address_hash == ^address_hash) - - created_contract_query = - base_query - |> where([t], t.created_contract_address_hash == ^address_hash) - - queries = - [token_transfers_query] ++ - case direction do - :from -> [from_address_query] - :to -> [to_address_query, created_contract_query] - _ -> [from_address_query, to_address_query, created_contract_query] + if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] do + address_hash + |> Reward.fetch_emission_rewards_tuples(paging_options) + |> Enum.concat(transactions_list) + |> Enum.sort_by(fn item -> + case item do + {%Reward{} = emission_reward, _} -> + {-emission_reward.block.number, 1} + + item -> + {-item.block_number, -item.index} end - - rewards_list = - if Application.get_env(:block_scout_web, BlockScoutWeb.Chain)[:has_emission_funds] do - Reward.fetch_emission_rewards_tuples(address_hash, paging_options) - else - [] - end - - queries - |> Stream.flat_map(&Repo.all/1) - |> Stream.uniq_by(& &1.hash) - |> Stream.concat(rewards_list) - |> Enum.sort_by(fn item -> - case item do - {%Reward{} = emission_reward, _} -> - {-emission_reward.block.number, 1} - - item -> - {-item.block_number, -item.index} - end - end) - |> Enum.take(paging_options.page_size) + end) + |> Enum.take(paging_options.page_size) + else + transactions_list + end end @spec address_to_logs(Address.t(), Keyword.t()) :: [ @@ -703,25 +674,32 @@ defmodule Explorer.Chain do iex> Explorer.Chain.hash_to_address(hash) {:error, :not_found} + Optionally accepts: + - a list of bindings to preload, just like `Ecto.Query.preload/3` + - a boolean to also fetch the `has_decompiled_code?` virtual field or not + """ - @spec hash_to_address(Hash.Address.t()) :: {:ok, Address.t()} | {:error, :not_found} - def hash_to_address(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do - query = - from( - address in Address, - preload: [ + @spec hash_to_address(Hash.Address.t(), [Macro.t()], boolean()) :: {:ok, Address.t()} | {:error, :not_found} + def hash_to_address( + %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, + preloads \\ [ :contracts_creation_internal_transaction, :names, :smart_contract, :token, :contracts_creation_transaction ], + query_decompiled_code_flag \\ true + ) do + query = + from( + address in Address, + preload: ^preloads, where: address.hash == ^hash ) - query_with_decompiled_flag = with_decompiled_code_flag(query, hash) - - query_with_decompiled_flag + query + |> with_decompiled_code_flag(hash, query_decompiled_code_flag) |> Repo.one() |> case do nil -> {:error, :not_found} @@ -824,6 +802,86 @@ defmodule Explorer.Chain do Repo.all(query) end + @doc """ + Returns the balance of the given address and block combination. + + Returns `{:error, :not_found}` if there is no address by that hash present. + Returns `{:error, :no_balance}` if there is no balance for that address at that block. + """ + @spec get_balance_as_of_block(Hash.Address.t(), integer | :earliest | :latest | :pending) :: + {:ok, Wei.t()} | {:error, :no_balance} | {:error, :not_found} + def get_balance_as_of_block(address, block) when is_integer(block) do + coin_balance_query = + from(coin_balance in CoinBalance, + where: coin_balance.address_hash == ^address, + where: not is_nil(coin_balance.value), + where: coin_balance.block_number <= ^block, + order_by: [desc: coin_balance.block_number], + limit: 1, + select: coin_balance.value + ) + + case Repo.one(coin_balance_query) do + nil -> {:error, :not_found} + coin_balance -> {:ok, coin_balance} + end + end + + def get_balance_as_of_block(address, :latest) do + case max_consensus_block_number() do + {:ok, latest_block_number} -> + get_balance_as_of_block(address, latest_block_number) + + {:error, :not_found} -> + {:error, :not_found} + end + end + + def get_balance_as_of_block(address, :earliest) do + query = + from(coin_balance in CoinBalance, + where: coin_balance.address_hash == ^address, + where: not is_nil(coin_balance.value), + where: coin_balance.block_number == 0, + limit: 1, + select: coin_balance.value + ) + + case Repo.one(query) do + nil -> {:error, :not_found} + coin_balance -> {:ok, coin_balance} + end + end + + def get_balance_as_of_block(address, :pending) do + query = + case max_consensus_block_number() do + {:ok, latest_block_number} -> + from(coin_balance in CoinBalance, + where: coin_balance.address_hash == ^address, + where: not is_nil(coin_balance.value), + where: coin_balance.block_number > ^latest_block_number, + order_by: [desc: coin_balance.block_number], + limit: 1, + select: coin_balance.value + ) + + {:error, :not_found} -> + from(coin_balance in CoinBalance, + where: coin_balance.address_hash == ^address, + where: not is_nil(coin_balance.value), + order_by: [desc: coin_balance.block_number], + limit: 1, + select: coin_balance.value + ) + end + + case Repo.one(query) do + nil -> {:error, :not_found} + coin_balance -> {:ok, coin_balance} + end + end + @spec list_ordered_addresses(non_neg_integer(), non_neg_integer()) :: [Address.t()] def list_ordered_addresses(offset, limit) do query = @@ -3081,7 +3139,11 @@ defmodule Explorer.Chain do defp staking_pool_filter(query, _), do: query - defp with_decompiled_code_flag(query, hash) do + defp with_decompiled_code_flag(query, hash, use_option \\ true) + + defp with_decompiled_code_flag(query, _hash, false), do: query + + defp with_decompiled_code_flag(query, hash, true) do has_decompiled_code_query = from(decompiled_contract in DecompiledSmartContract, where: decompiled_contract.address_hash == ^hash, diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index de99198181..19f04dee2d 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -5,7 +5,7 @@ defmodule Explorer.Chain.Transaction do require Logger - import Ecto.Query, only: [from: 2, order_by: 3, preload: 3, subquery: 1, where: 3] + import Ecto.Query, only: [from: 2, preload: 3, subquery: 1, where: 3] alias ABI.FunctionSelector @@ -549,15 +549,40 @@ defmodule Explorer.Chain.Transaction do end @doc """ - Builds a query that will check for transactions within the hashes params. + Modifies a query to filter for transactions whose hash is in a list or that are + linked to the given address_hash through a direction. Be careful to not pass a large list, because this will lead to performance problems. """ - def where_transaction_hashes_match(transaction_hashes) do - Transaction - |> where([t], t.hash == fragment("ANY (?)", ^transaction_hashes)) - |> order_by([transaction], desc: transaction.block_number, desc: transaction.index) + def where_transaction_matches(query, transaction_hashes, :from, address_hash) do + where( + query, + [t], + t.hash in ^transaction_hashes or + t.from_address_hash == ^address_hash + ) + end + + def where_transaction_matches(query, transaction_hashes, :to, address_hash) do + where( + query, + [t], + t.hash in ^transaction_hashes or + t.to_address_hash == ^address_hash or + t.created_contract_address_hash == ^address_hash + ) + end + + def where_transaction_matches(query, transaction_hashes, _direction, address_hash) do + where( + query, + [t], + t.hash in ^transaction_hashes or + t.from_address_hash == ^address_hash or + t.to_address_hash == ^address_hash or + t.created_contract_address_hash == ^address_hash + ) end @collated_fields ~w(block_number cumulative_gas_used gas_used index)a diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex index 4b12d3deb0..c1c434aa44 100644 --- a/apps/explorer/lib/explorer/chain/wei.ex +++ b/apps/explorer/lib/explorer/chain/wei.ex @@ -113,6 +113,17 @@ defmodule Explorer.Chain.Wei do @wei_per_ether Decimal.new(1_000_000_000_000_000_000) @wei_per_gwei Decimal.new(1_000_000_000) + @spec hex_format(Wei.t()) :: String.t() + def hex_format(%Wei{value: decimal}) do + hex = + decimal + |> Decimal.to_integer() + |> Integer.to_string(16) + |> String.downcase() + + "0x" <> hex + end + @doc """ Sums two Wei values. diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index 8778677f33..8edd87738d 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -140,7 +140,7 @@ defmodule Indexer.Block.Realtime.Fetcher do %__MODULE__{state | subscription: subscription} {:error, reason} -> - Logger.debug(fn -> ["Could not connect to websocket: ", reason, ". Continuing with polling."] end) + Logger.debug(fn -> ["Could not connect to websocket: #{inspect(reason)}. Continuing with polling."] end) state end end @@ -201,6 +201,12 @@ defmodule Indexer.Block.Realtime.Fetcher do end end + def import(_, _) do + Logger.warn("Empty parameters were provided for realtime fetcher") + + {:ok, []} + end + defp start_fetch_and_import(number, block_fetcher, previous_number, max_number_seen) do start_at = determine_start_at(number, previous_number, max_number_seen) diff --git a/apps/indexer/lib/indexer/fetcher/uncle_block.ex b/apps/indexer/lib/indexer/fetcher/uncle_block.ex index ba36ffb2f9..213604a73e 100644 --- a/apps/indexer/lib/indexer/fetcher/uncle_block.ex +++ b/apps/indexer/lib/indexer/fetcher/uncle_block.ex @@ -104,18 +104,18 @@ defmodule Indexer.Fetcher.UncleBlock do {nephew_hash_bytes, index} end - defp run_blocks(%Blocks{blocks_params: []}, _, original_entries), do: {:retry, original_entries} - - defp run_blocks( - %Blocks{ - blocks_params: blocks_params, - transactions_params: transactions_params, - block_second_degree_relations_params: block_second_degree_relations_params, - errors: errors - }, - block_fetcher, - original_entries - ) do + def run_blocks(%Blocks{blocks_params: []}, _, original_entries), do: {:retry, original_entries} + + def run_blocks( + %Blocks{ + blocks_params: blocks_params, + transactions_params: transactions_params, + block_second_degree_relations_params: block_second_degree_relations_params, + errors: errors + }, + block_fetcher, + original_entries + ) do addresses_params = Addresses.extract_addresses(%{blocks: blocks_params, transactions: transactions_params}) case Block.Fetcher.import(block_fetcher, %{ @@ -235,7 +235,17 @@ defmodule Indexer.Fetcher.UncleBlock do Enum.map(errors, &error_to_entry/1) end - defp error_to_entry(%{data: %{hash: hash}}) when is_binary(hash), do: hash + defp error_to_entry(%{data: %{hash: hash, index: index}}) when is_binary(hash) do + {:ok, %Hash{bytes: nephew_hash_bytes}} = Hash.Full.cast(hash) + + {nephew_hash_bytes, index} + end + + defp error_to_entry(%{data: %{nephew_hash: hash, index: index}}) when is_binary(hash) do + {:ok, %Hash{bytes: nephew_hash_bytes}} = Hash.Full.cast(hash) + + {nephew_hash_bytes, index} + end defp errors_to_iodata(errors) when is_list(errors) do errors_to_iodata(errors, []) @@ -251,4 +261,9 @@ defmodule Indexer.Fetcher.UncleBlock do when is_integer(code) and is_binary(message) and is_binary(hash) do [hash, ": (", to_string(code), ") ", message, ?\n] end + + defp error_to_iodata(%{code: code, message: message, data: %{nephew_hash: hash}}) + when is_integer(code) and is_binary(message) and is_binary(hash) do + [hash, ": (", to_string(code), ") ", message, ?\n] + end end diff --git a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex index 49b33b7c44..486789f675 100644 --- a/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex +++ b/apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex @@ -13,7 +13,6 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do import Ecto.Query - alias Ecto.Multi alias EthereumJSONRPC.Blocks alias Explorer.Chain.Block alias Explorer.Repo @@ -23,13 +22,14 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do @defaults [ flush_interval: :timer.seconds(3), - max_batch_size: 10, + max_batch_size: 50, max_concurrency: 1, task_supervisor: Indexer.Temporary.BlocksTransactionsMismatch.TaskSupervisor, metadata: [fetcher: :blocks_transactions_mismatch] ] @doc false + # credo:disable-for-next-line Credo.Check.Design.DuplicatedCode 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) @@ -99,17 +99,26 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do 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)) + {matching_blocks_data, unmatching_blocks_data} = + Enum.split_with(found_blocks_data, fn {block, trans_num} -> + found_blocks_map[to_string(block.hash)] == trans_num end) - |> Repo.transaction() + + unless Enum.empty?(matching_blocks_data) do + hashes = Enum.map(matching_blocks_data, fn {block, _trans_num} -> block.hash end) + + Block + |> where([block], block.hash in ^hashes) + |> Repo.update_all(set: [refetch_needed: false]) + end + + unless Enum.empty?(unmatching_blocks_data) do + hashes = Enum.map(unmatching_blocks_data, fn {block, _trans_num} -> block.hash end) + + Block + |> where([block], block.hash in ^hashes) + |> Repo.update_all(set: [refetch_needed: false, consensus: false]) + end if Enum.empty?(missing_blocks_data) do :ok diff --git a/apps/indexer/test/indexer/fetcher/uncle_block_test.exs b/apps/indexer/test/indexer/fetcher/uncle_block_test.exs index 224349d781..f3350b8de7 100644 --- a/apps/indexer/test/indexer/fetcher/uncle_block_test.exs +++ b/apps/indexer/test/indexer/fetcher/uncle_block_test.exs @@ -6,7 +6,9 @@ defmodule Indexer.Fetcher.UncleBlockTest do import EthereumJSONRPC, only: [integer_to_quantity: 1] + alias EthereumJSONRPC.Blocks alias Explorer.Chain + alias Explorer.Chain.Hash alias Indexer.Block alias Indexer.Fetcher.UncleBlock @@ -138,6 +140,74 @@ defmodule Indexer.Fetcher.UncleBlockTest do end end + describe "run/2" do + test "retries failed request", %{json_rpc_named_arguments: json_rpc_named_arguments} do + %Hash{bytes: block_hash_bytes} = block_hash() + entries = [{block_hash_bytes, 0}] + + EthereumJSONRPC.Mox + |> expect(:json_rpc, fn [ + %{ + id: id, + method: "eth_getUncleByBlockHashAndIndex" + } + ], + _ -> + {:ok, + [ + %{ + id: id, + error: %{ + code: 404, + data: %{index: 0, nephew_hash: "0xa0814f0478fe90c82852f812fd74c96df148654c326d2600d836e6908ebb62b4"}, + message: "Not Found" + } + } + ]} + end) + + assert {:retry, ^entries} = + UncleBlock.run(entries, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}) + end + end + + describe "run_blocks/2" do + test "converts errors to entries for retry", %{json_rpc_named_arguments: json_rpc_named_arguments} do + miner_hash = + address_hash() + |> to_string() + + block_number = 1 + + index = 0 + + hash = "0xa0814f0478fe90c82852f812fd74c96df148654c326d2600d836e6908ebb62b4" + + params = %Blocks{ + errors: [ + %{ + code: 404, + data: %{index: index, nephew_hash: hash}, + message: "Not Found" + } + ], + blocks_params: [%{miner_hash: miner_hash, number: block_number}] + } + + assert {:retry, [{bin_hash, ^index}]} = + UncleBlock.run_blocks( + params, + %Block.Fetcher{ + json_rpc_named_arguments: json_rpc_named_arguments, + callback_module: Indexer.Block.Realtime.Fetcher + }, + [] + ) + + assert Hash.Full.cast(bin_hash) == Hash.Full.cast(hash) + end + end + defp wait(producer) do producer.() rescue diff --git a/bin/install_chrome_headless.sh b/bin/install_chrome_headless.sh index 9721e84ea3..d27bb6f9d8 100755 --- a/bin/install_chrome_headless.sh +++ b/bin/install_chrome_headless.sh @@ -1,6 +1,6 @@ export DISPLAY=:99.0 sh -e /etc/init.d/xvfb start -export CHROMEDRIVER_VERSION=`curl -s http://chromedriver.storage.googleapis.com/LATEST_RELEASE` +export CHROMEDRIVER_VERSION=74.0.3729.6 curl -L -O "http://chromedriver.storage.googleapis.com/${CHROMEDRIVER_VERSION}/chromedriver_linux64.zip" unzip chromedriver_linux64.zip sudo chmod +x chromedriver