From 43604709ad8496fbbee75e35a2562e45c5d01e49 Mon Sep 17 00:00:00 2001 From: sl1depengwyn Date: Sat, 7 Jan 2023 10:13:57 +0500 Subject: [PATCH] Add withdrawals to eth json rpc app Add withdrawals to explorer app Add withdrawals to indexer Add withdrawal tabs to address and block pages Add withdrawals test to web Add withdrawals test to eth json rpc Add withdrawals test to explorer Add withdrawal test to indexer Update CHANGELOG.md Update gettext [no ci] Update apps/indexer/config/prod.exs [no ci] Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> Fix review Update CHANGELOG.md Update CHANGELOG.md Add withdrawal list page; Add withdrawal to APIv2 Add timestamp Fix nikitosing review --- CHANGELOG.md | 1 + .../assets/js/pages/address.js | 1 + .../lib/block_scout_web/api_router.ex | 6 + .../lib/block_scout_web/chain.ex | 7 +- .../address_withdrawal_controller.ex | 121 +++++++++++++ .../controllers/api/v2/address_controller.ex | 19 ++- .../controllers/api/v2/block_controller.ex | 21 ++- .../api/v2/withdrawal_controller.ex | 25 +++ .../block_transaction_controller.ex | 6 +- .../block_withdrawal_controller.ex | 106 ++++++++++++ .../controllers/withdrawal_controller.ex | 39 +++++ .../templates/address/_tabs.html.eex | 8 + .../address_withdrawal/_metatags.html.eex | 1 + .../address_withdrawal/_withdrawal.html.eex | 25 +++ .../address_withdrawal/index.html.eex | 66 ++++++++ .../templates/block/_number_link.html.eex | 4 + .../templates/block/_tabs.html.eex | 19 +++ .../block_transaction/index.html.eex | 12 +- .../block_withdrawal/_metatags.html.eex | 1 + .../block_withdrawal/_withdrawal.html.eex | 23 +++ .../templates/block_withdrawal/index.html.eex | 54 ++++++ .../templates/layout/_topnav.html.eex | 3 + .../templates/layout/app.html.eex | 5 +- .../templates/withdrawal/_metatags.html.eex | 8 + .../templates/withdrawal/_withdrawal.html.eex | 34 ++++ .../templates/withdrawal/index.html.eex | 59 +++++++ .../views/address_withdrawal_view.ex | 3 + .../views/api/v2/address_view.ex | 3 +- .../views/api/v2/block_view.ex | 6 +- .../views/api/v2/withdrawal_view.ex | 41 +++++ .../views/block_withdrawal_view.ex | 3 + .../block_scout_web/views/withdrawal_view.ex | 3 + .../lib/block_scout_web/web_router.ex | 11 ++ .../address_withdrawal_controller_test.exs | 123 ++++++++++++++ .../api/v2/address_controller_test.exs | 39 ++++- .../api/v2/block_controller_test.exs | 77 ++++++++- .../api/v2/withdrawal_controller_test.exs | 55 ++++++ .../block_withdrawal_controller_test.exs | 139 +++++++++++++++ .../withdrawal_controller_test.exs | 60 +++++++ apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex | 11 ++ .../lib/ethereum_jsonrpc/block.ex | 160 +++++++++++++++--- .../lib/ethereum_jsonrpc/blocks.ex | 118 ++++++++++++- .../lib/ethereum_jsonrpc/withdrawal.ex | 101 +++++++++++ .../lib/ethereum_jsonrpc/withdrawals.ex | 67 ++++++++ .../test/ethereum_jsonrpc/block_test.exs | 13 +- .../test/ethereum_jsonrpc/withdrawal_test.exs | 5 + .../ethereum_jsonrpc/withdrawals_test.exs | 5 + apps/explorer/lib/explorer/chain.ex | 65 ++++++- apps/explorer/lib/explorer/chain/address.ex | 4 +- apps/explorer/lib/explorer/chain/block.ex | 4 +- .../chain/import/runner/withdrawals.ex | 106 ++++++++++++ .../chain/import/stage/block_referencing.ex | 3 +- .../explorer/lib/explorer/chain/withdrawal.ex | 113 +++++++++++++ apps/explorer/lib/explorer/helper.ex | 3 + .../20221223214711_create_withdrawals.exs | 19 +++ .../test/explorer/chain/withdrawal_test.exs | 82 +++++++++ apps/explorer/test/support/factory.ex | 26 ++- apps/indexer/README.md | 2 + apps/indexer/config/dev.exs | 5 + apps/indexer/config/prod.exs | 6 + apps/indexer/lib/indexer/block/fetcher.ex | 10 +- .../indexer/lib/indexer/fetcher/withdrawal.ex | 159 +++++++++++++++++ apps/indexer/lib/indexer/supervisor.ex | 6 +- .../transform/address_coin_balances.ex | 7 + .../lib/indexer/transform/addresses.ex | 12 ++ .../test/indexer/fetcher/withdrawal_test.exs | 152 +++++++++++++++++ .../fetcher/withdrawal_supervisor_case.ex | 17 ++ config/config.exs | 1 + config/runtime.exs | 5 + docker-compose/envs/common-blockscout.env | 2 + docker/Makefile | 5 + 71 files changed, 2474 insertions(+), 57 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/address_withdrawal_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_metatags.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_withdrawal.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/index.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/block/_number_link.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_metatags.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_withdrawal.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/index.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_metatags.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_withdrawal.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/withdrawal/index.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/address_withdrawal_view.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/v2/withdrawal_view.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/block_withdrawal_view.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/withdrawal_view.ex create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs create mode 100644 apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawal.ex create mode 100644 apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawals.ex create mode 100644 apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawal_test.exs create mode 100644 apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawals_test.exs create mode 100644 apps/explorer/lib/explorer/chain/import/runner/withdrawals.ex create mode 100644 apps/explorer/lib/explorer/chain/withdrawal.ex create mode 100644 apps/explorer/priv/repo/migrations/20221223214711_create_withdrawals.exs create mode 100644 apps/explorer/test/explorer/chain/withdrawal_test.exs create mode 100644 apps/indexer/lib/indexer/fetcher/withdrawal.ex create mode 100644 apps/indexer/test/indexer/fetcher/withdrawal_test.exs create mode 100644 apps/indexer/test/support/indexer/fetcher/withdrawal_supervisor_case.ex diff --git a/CHANGELOG.md b/CHANGELOG.md index 741a994f5d..f65c6c050e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- [#6694](https://github.com/blockscout/blockscout/pull/6694) - Add withdrawals support (EIP-4895) - [#7355](https://github.com/blockscout/blockscout/pull/7355) - Add endpoint for token info import - [#7393](https://github.com/blockscout/blockscout/pull/7393) - Realtime fetcher max gap diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index 53feee1d9f..9edee8b5cd 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -251,6 +251,7 @@ if ($addressDetailsPage.length) { const shouldScroll = pathParts.includes('transactions') || pathParts.includes('token-transfers') || pathParts.includes('tokens') || + pathParts.includes('withdrawals') || pathParts.includes('internal-transactions') || pathParts.includes('coin-balances') || pathParts.includes('logs') || diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index 7b45d1a070..4c04d7dd9d 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -141,6 +141,7 @@ defmodule BlockScoutWeb.ApiRouter do get("/", V2.BlockController, :blocks) get("/:block_hash_or_number", V2.BlockController, :block) get("/:block_hash_or_number/transactions", V2.BlockController, :transactions) + get("/:block_hash_or_number/withdrawals", V2.BlockController, :withdrawals) end scope "/addresses" do @@ -156,6 +157,7 @@ defmodule BlockScoutWeb.ApiRouter do get("/:address_hash/blocks-validated", V2.AddressController, :blocks_validated) get("/:address_hash/coin-balance-history", V2.AddressController, :coin_balance_history) get("/:address_hash/coin-balance-history-by-day", V2.AddressController, :coin_balance_history_by_day) + get("/:address_hash/withdrawals", V2.AddressController, :withdrawals) end scope "/tokens" do @@ -186,6 +188,10 @@ defmodule BlockScoutWeb.ApiRouter do get("/market", V2.StatsController, :market_chart) end end + + scope "/withdrawals" do + get("/", V2.WithdrawalController, :withdrawals_list) + end end scope "/v1", as: :api_v1 do diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index d4e30d03ba..cc83b040a7 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -30,7 +30,8 @@ defmodule BlockScoutWeb.Chain do Token.Instance, TokenTransfer, Transaction, - Wei + Wei, + Withdrawal } alias Explorer.PagingOptions @@ -437,6 +438,10 @@ defmodule BlockScoutWeb.Chain do %{"smart_contract_id" => smart_contract.id} end + defp paging_params(%Withdrawal{index: index}) do + %{"index" => index} + end + # clause for search results pagination defp paging_params(%{ address_hash: address_hash, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_withdrawal_controller.ex new file mode 100644 index 0000000000..7fcb05d5b4 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_withdrawal_controller.ex @@ -0,0 +1,121 @@ +defmodule BlockScoutWeb.AddressWithdrawalController do + @moduledoc """ + Display all the withdrawals that terminate at this Address. + """ + + use BlockScoutWeb, :controller + + import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + + import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2] + + alias BlockScoutWeb.{AccessHelper, AddressWithdrawalView, Controller} + alias Explorer.{Chain, Market} + + alias Explorer.Chain.Wei + + alias Explorer.ExchangeRates.Token + alias Indexer.Fetcher.CoinBalanceOnDemand + alias Phoenix.View + + def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do + address_options = [necessity_by_association: %{:names => :optional, :smart_contract => :optional}] + + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.hash_to_address(address_hash, address_options, false), + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do + options = paging_options(params) + + withdrawals_plus_one = Chain.address_hash_to_withdrawals(address_hash, options) + {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) + + next_page_url = + case next_page_params(next_page, withdrawals, params) do + nil -> + nil + + next_page_params -> + address_withdrawal_path( + conn, + :index, + address, + Map.delete(next_page_params, "type") + ) + end + + items_json = + for withdrawal <- withdrawals do + View.render_to_string(AddressWithdrawalView, "_withdrawal.html", withdrawal: withdrawal) + end + + json(conn, %{items: items_json, next_page_path: next_page_url}) + else + :error -> + unprocessable_entity(conn) + + {:restricted_access, _} -> + not_found(conn) + + {:error, :not_found} -> + case Chain.Hash.Address.validate(address_hash_string) do + {:ok, _} -> + json(conn, %{items: [], next_page_path: ""}) + + _ -> + not_found(conn) + end + end + end + + 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), + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params) do + render( + conn, + "index.html", + address: address, + coin_balance_status: CoinBalanceOnDemand.trigger_fetch(address), + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), + current_path: Controller.current_full_path(conn), + tags: get_address_tags(address_hash, current_user(conn)) + ) + else + :error -> + unprocessable_entity(conn) + + {:restricted_access, _} -> + not_found(conn) + + {:error, :not_found} -> + case Chain.Hash.Address.validate(address_hash_string) do + {:ok, _} -> + {:ok, address_hash} = Chain.string_to_address_hash(address_hash_string) + + address = %Chain.Address{ + hash: address_hash, + smart_contract: nil, + token: nil, + fetched_coin_balance: %Wei{value: Decimal.new(0)} + } + + render( + conn, + "index.html", + address: address, + coin_balance_status: nil, + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + counters_path: address_path(conn, :address_counters, %{"id" => address_hash_string}), + current_path: Controller.current_full_path(conn), + tags: get_address_tags(address_hash, current_user(conn)) + ) + + _ -> + not_found(conn) + end + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index ab29da2107..9b999f9234 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -14,7 +14,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1] alias BlockScoutWeb.AccessHelper - alias BlockScoutWeb.API.V2.{BlockView, TransactionView} + alias BlockScoutWeb.API.V2.{BlockView, TransactionView, WithdrawalView} alias Explorer.ExchangeRates.Token alias Explorer.{Chain, Market} alias Indexer.Fetcher.{CoinBalanceOnDemand, TokenBalanceOnDemand} @@ -370,6 +370,23 @@ defmodule BlockScoutWeb.API.V2.AddressController do end end + def withdrawals(conn, %{"address_hash" => address_hash_string} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:not_found, {:ok, _address}} <- {:not_found, Chain.hash_to_address(address_hash, @api_true, false)} do + options = @api_true |> Keyword.merge(paging_options(params)) + withdrawals_plus_one = address_hash |> Chain.address_hash_to_withdrawals(options) + {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) + + next_page_params = next_page |> next_page_params(withdrawals, params) |> delete_parameters_from_next_page_params() + + conn + |> put_status(200) + |> put_view(WithdrawalView) + |> render(:withdrawals, %{withdrawals: withdrawals, next_page_params: next_page_params}) + end + end + def addresses_list(conn, params) do {addresses, next_page} = params diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index 78f0dc47d3..11492b2420 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -12,7 +12,7 @@ defmodule BlockScoutWeb.API.V2.BlockController do import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1, select_block_type: 1] - alias BlockScoutWeb.API.V2.TransactionView + alias BlockScoutWeb.API.V2.{TransactionView, WithdrawalView} alias Explorer.Chain @transaction_necessity_by_association [ @@ -106,4 +106,23 @@ defmodule BlockScoutWeb.API.V2.BlockController do |> render(:transactions, %{transactions: transactions, next_page_params: next_page_params}) end end + + def withdrawals(conn, %{"block_hash_or_number" => block_hash_or_number} = params) do + with {:ok, type, value} <- parse_block_hash_or_number_param(block_hash_or_number), + {:ok, block} <- fetch_block(type, value, @api_true) do + full_options = + [necessity_by_association: %{address: :optional}, api?: true] + |> Keyword.merge(paging_options(params)) + + withdrawals_plus_one = Chain.block_to_withdrawals(block.hash, full_options) + {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) + + next_page_params = next_page |> next_page_params(withdrawals, params) |> delete_parameters_from_next_page_params() + + conn + |> put_status(200) + |> put_view(WithdrawalView) + |> render(:withdrawals, %{withdrawals: withdrawals, next_page_params: next_page_params}) + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex new file mode 100644 index 0000000000..983a0e18cc --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/withdrawal_controller.ex @@ -0,0 +1,25 @@ +defmodule BlockScoutWeb.API.V2.WithdrawalController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Chain, + only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] + + alias Explorer.Chain + + def withdrawals_list(conn, params) do + full_options = + [necessity_by_association: %{address: :optional, block: :optional}, api?: true] + |> Keyword.merge(paging_options(params)) + + withdrawals_plus_one = Chain.list_withdrawals(full_options) + {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) + + next_page_params = next_page |> next_page_params(withdrawals, params) |> delete_parameters_from_next_page_params() + + conn + |> put_status(200) + |> render(:withdrawals, %{withdrawals: withdrawals, next_page_params: next_page_params}) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex index dcd28110dd..74ef3f6b2f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex @@ -139,7 +139,7 @@ defmodule BlockScoutWeb.BlockTransactionController do end end - defp param_block_hash_or_number_to_block(param, options) do + def param_block_hash_or_number_to_block(param, options) do case parse_block_hash_or_number_param(param) do {:ok, :number, number} -> number_to_block(number, options) @@ -152,9 +152,9 @@ defmodule BlockScoutWeb.BlockTransactionController do end end - defp block_above_tip("0x" <> _), do: {:error, :hash} + def block_above_tip("0x" <> _), do: {:error, :hash} - defp block_above_tip(block_hash_or_number) when is_binary(block_hash_or_number) do + def block_above_tip(block_hash_or_number) when is_binary(block_hash_or_number) do case Chain.max_consensus_block_number() do {:ok, max_consensus_block_number} -> {block_number, _} = Integer.parse(block_hash_or_number) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex new file mode 100644 index 0000000000..c6044d84ca --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_withdrawal_controller.ex @@ -0,0 +1,106 @@ +defmodule BlockScoutWeb.BlockWithdrawalController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Chain, + only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + + import BlockScoutWeb.BlockTransactionController, only: [param_block_hash_or_number_to_block: 2, block_above_tip: 1] + + alias BlockScoutWeb.{BlockTransactionView, BlockWithdrawalView, Controller} + alias Explorer.Chain + alias Phoenix.View + + def index(conn, %{"block_hash_or_number" => formatted_block_hash_or_number, "type" => "JSON"} = params) do + case param_block_hash_or_number_to_block(formatted_block_hash_or_number, []) do + {:ok, block} -> + full_options = + [necessity_by_association: %{address: :optional}] + |> Keyword.merge(paging_options(params)) + + withdrawals_plus_one = Chain.block_to_withdrawals(block.hash, full_options) + + {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) + + next_page_path = + case next_page_params(next_page, withdrawals, params) do + nil -> + nil + + next_page_params -> + block_withdrawal_path( + conn, + :index, + block, + Map.delete(next_page_params, "type") + ) + end + + items = + for withdrawal <- withdrawals do + View.render_to_string(BlockWithdrawalView, "_withdrawal.html", withdrawal: withdrawal) + end + + json( + conn, + %{ + items: items, + next_page_path: next_page_path + } + ) + + {:error, {:invalid, :hash}} -> + not_found(conn) + + {:error, {:invalid, :number}} -> + not_found(conn) + + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> render( + BlockTransactionView, + "404.html", + block: nil, + block_above_tip: block_above_tip(formatted_block_hash_or_number) + ) + end + end + + def index(conn, %{"block_hash_or_number" => formatted_block_hash_or_number}) do + case param_block_hash_or_number_to_block(formatted_block_hash_or_number, + necessity_by_association: %{ + [miner: :names] => :required, + :uncles => :optional, + :nephews => :optional, + :rewards => :optional + } + ) do + {:ok, block} -> + block_transaction_count = Chain.block_to_transaction_count(block.hash) + + render( + conn, + "index.html", + block: block, + block_transaction_count: block_transaction_count, + current_path: Controller.current_full_path(conn) + ) + + {:error, {:invalid, :hash}} -> + not_found(conn) + + {:error, {:invalid, :number}} -> + not_found(conn) + + {:error, :not_found} -> + conn + |> put_status(:not_found) + |> render( + BlockTransactionView, + "404.html", + block: nil, + block_above_tip: block_above_tip(formatted_block_hash_or_number) + ) + end + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex new file mode 100644 index 0000000000..e3405c69eb --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/withdrawal_controller.ex @@ -0,0 +1,39 @@ +defmodule BlockScoutWeb.WithdrawalController do + use BlockScoutWeb, :controller + + import BlockScoutWeb.Chain, + only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1, fetch_page_number: 1] + + alias BlockScoutWeb.{Controller, WithdrawalView} + alias Explorer.Chain + alias Phoenix.View + + def index(conn, %{"type" => "JSON"} = params) do + full_options = + [necessity_by_association: %{address: :optional, block: :optional}] + |> Keyword.merge(paging_options(params)) + + withdrawals_plus_one = Chain.list_withdrawals(full_options) + {withdrawals, next_page} = split_list_by_page(withdrawals_plus_one) + + items = + for withdrawal <- withdrawals do + View.render_to_string(WithdrawalView, "_withdrawal.html", withdrawal: withdrawal) + end + + next_page_path = + case next_page_params(next_page, withdrawals, params) do + nil -> nil + next_page_params -> withdrawal_path(conn, :index, Map.delete(next_page_params, "type")) + end + + json(conn, %{items: items, next_page_path: next_page_path}) + end + + def index(conn, params) do + render(conn, "index.html", + current_path: Controller.current_full_path(conn), + page_number: params |> fetch_page_number() |> Integer.to_string() + ) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex index 2a29f0de4c..de29c7e7fd 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tabs.html.eex @@ -24,6 +24,14 @@ "data-test": "tokens_tab_link" ) %> <% end %> + <%= if Chain.check_if_withdrawals_at_address(@address.hash) do %> + <%= link( + gettext("Withdrawals"), + class: "card-tab #{tab_status("withdrawals", @conn.request_path)}", + to: AccessHelper.get_path(@conn, :address_withdrawal_path, :index, @address.hash), + "data-test": "withdrawals_tab_link" + ) %> + <% end %> <%= link( gettext("Internal Transactions"), class: "card-tab #{tab_status("internal-transactions", @conn.request_path)}", diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_metatags.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_metatags.html.eex new file mode 100644 index 0000000000..3ef2a67ca2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.AddressView, "_metatags.html", conn: @conn, address: @address %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_withdrawal.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_withdrawal.html.eex new file mode 100644 index 0000000000..1bae3593d8 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/_withdrawal.html.eex @@ -0,0 +1,25 @@ + + + + <%= @withdrawal.index %> + + + + <%= @withdrawal.validator_index %> + + + + <%= render BlockScoutWeb.BlockView, + "_number_link.html", + block: @withdrawal.block + %> + + + + + + + + <%= format_wei_value(@withdrawal.amount, :ether) %> + + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/index.html.eex new file mode 100644 index 0000000000..d07c664e6b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_withdrawal/index.html.eex @@ -0,0 +1,66 @@ +
+ + <% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> + + <%= render BlockScoutWeb.AddressView, "overview.html", address: @address, is_proxy: is_proxy, conn: @conn, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status, counters_path: @counters_path, tags: @tags %> + +
+
+ <%= render BlockScoutWeb.AddressView, "_tabs.html", address: @address, is_proxy: is_proxy, conn: @conn %> +
+
+ +

<%= gettext "Withdrawals" %>

+
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+
+ + + +
+
+ + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 5 %> + +
+
<%= gettext "Index" %>
+
+
<%= gettext "Validator index" %>
+
+
<%= gettext "Block" %>
+
+
<%= gettext "Age" %>
+
+
<%= gettext "Amount" %>
+
+
+
+ +
+
+ <%= gettext "There are no withdrawals for this address." %> +
+
+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
+
+ +
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_number_link.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_number_link.html.eex new file mode 100644 index 0000000000..6046e0eddf --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_number_link.html.eex @@ -0,0 +1,4 @@ +<%= link( + to_string(@block.number), + to: block_path(BlockScoutWeb.Endpoint, :show, @block) +) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex new file mode 100644 index 0000000000..885aa9b98a --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_tabs.html.eex @@ -0,0 +1,19 @@ +
+ <%= + link( + gettext("Transactions"), + class: "card-tab #{tab_status("transactions", @conn.request_path)}", + to: block_transaction_path(@conn, :index, @conn.params["block_hash_or_number"]) + ) + %> + + <%= if Chain.check_if_withdrawals_in_block(@block.hash) do %> + <%= + link( + gettext("Withdrawals"), + class: "card-tab #{tab_status("withdrawals", @conn.request_path)}", + to: block_withdrawal_path(@conn, :index, @conn.params["block_hash_or_number"]) + ) + %> + <% end %> +
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex index c3f9ab4e25..d4c91eff4a 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex @@ -4,16 +4,8 @@
-
- <%= - link( - gettext("Transactions"), - class: "card-tab #{tab_status("transactions", @conn.request_path)}", - to: block_transaction_path(@conn, :index, @conn.params["block_hash_or_number"]) - ) - %> -
- + <%= render BlockScoutWeb.BlockView, "_tabs.html", assigns %> +
<%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_metatags.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_metatags.html.eex new file mode 100644 index 0000000000..bff7f9460c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_metatags.html.eex @@ -0,0 +1 @@ +<%= render BlockScoutWeb.BlockView, "_metatags.html", conn: @conn, block: @block %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_withdrawal.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_withdrawal.html.eex new file mode 100644 index 0000000000..9506edc331 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/_withdrawal.html.eex @@ -0,0 +1,23 @@ + + + + <%= @withdrawal.index %> + + + + <%= @withdrawal.validator_index %> + + + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @withdrawal.address, + contract: BlockScoutWeb.AddressView.contract?(@withdrawal.address), + use_custom_tooltip: false + %> + + + + <%= format_wei_value(@withdrawal.amount, :ether) %> + + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/index.html.eex new file mode 100644 index 0000000000..43c4515848 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/block_withdrawal/index.html.eex @@ -0,0 +1,54 @@ +
+ + <%= render BlockScoutWeb.BlockView, "overview.html", assigns %> + +
+
+ <%= render BlockScoutWeb.BlockView, "_tabs.html", assigns %> + +
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + + + +
+
+ + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 4 %> + +
+
<%= gettext "Index" %>
+
+
<%= gettext "Validator index" %>
+
+
<%= gettext "To" %>
+
+
<%= gettext "Amount" %>
+
+
+
+ +
+
+ <%= gettext "There are no withdrawals for this block." %> +
+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: "1", show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> + +
+ +
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index 2d8c4feb42..720184dc2b 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 @@ -67,6 +67,9 @@ <% end %> <%= link(gettext("Verified contracts"), to: verified_contracts_path(@conn, :index), + class: "dropdown-item border-bottom #{tab_status("reorgs", @conn.request_path)}")%> + <%= link(gettext("Withdrawals"), + to: withdrawal_path(@conn, :index), class: "dropdown-item #{tab_status("reorgs", @conn.request_path)}")%>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex index 7e561dfe2f..81725695df 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex @@ -103,6 +103,7 @@ @view_module != Elixir.BlockScoutWeb.ChainView && @view_module != Elixir.BlockScoutWeb.BlockView && @view_module != Elixir.BlockScoutWeb.BlockTransactionView && + @view_module != Elixir.BlockScoutWeb.BlockWithdrawalView && @view_module != Elixir.BlockScoutWeb.AddressView && @view_module != Elixir.BlockScoutWeb.TokensView && @view_module != Elixir.BlockScoutWeb.TransactionView && @@ -115,6 +116,7 @@ @view_module != Elixir.BlockScoutWeb.AddressTransactionView && @view_module != Elixir.BlockScoutWeb.AddressTokenTransferView && @view_module != Elixir.BlockScoutWeb.AddressTokenView && + @view_module != Elixir.BlockScoutWeb.AddressWithdrawalView && @view_module != Elixir.BlockScoutWeb.AddressInternalTransactionView && @view_module != Elixir.BlockScoutWeb.AddressCoinBalanceView && @view_module != Elixir.BlockScoutWeb.AddressLogsView && @@ -142,7 +144,8 @@ @view_module != Elixir.BlockScoutWeb.SearchView && @view_module != Elixir.BlockScoutWeb.AddressContractVerificationViaStandardJsonInputView && @view_module != Elixir.BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView && - @view_module != Elixir.BlockScoutWeb.StakesView + @view_module != Elixir.BlockScoutWeb.StakesView && + @view_module != Elixir.BlockScoutWeb.WithdrawalView ) do %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_metatags.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_metatags.html.eex new file mode 100644 index 0000000000..b17983990b --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_metatags.html.eex @@ -0,0 +1,8 @@ + + <%= gettext( + "Beacon chain withdrawals - %{subnetwork} Explorer", + subnetwork: BlockScoutWeb.LayoutView.subnetwork_title() + ) %> + +"> +"> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_withdrawal.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_withdrawal.html.eex new file mode 100644 index 0000000000..e90e6c99d2 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/_withdrawal.html.eex @@ -0,0 +1,34 @@ + + + + <%= @withdrawal.index %> + + + + <%= @withdrawal.validator_index %> + + + + <%= render BlockScoutWeb.BlockView, + "_number_link.html", + block: @withdrawal.block + %> + + + + <%= render BlockScoutWeb.AddressView, + "_link.html", + address: @withdrawal.address, + contract: BlockScoutWeb.AddressView.contract?(@withdrawal.address), + use_custom_tooltip: false + %> + + + + + + + + <%= format_wei_value(@withdrawal.amount, :ether) %> + + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/index.html.eex new file mode 100644 index 0000000000..9b075854aa --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/withdrawal/index.html.eex @@ -0,0 +1,59 @@ +
+ <%= render BlockScoutWeb.Advertisement.TextAdView, "index.html", conn: @conn %> +
+
+

<%= gettext "Withdrawals" %>

+ +
+ <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "top", cur_page_number: @page_number, show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ + + +
+
+ + + + + + + + + + + + + <%= render BlockScoutWeb.CommonComponentsView, "_table-loader.html", columns_num: 6 %> + +
+
<%= gettext "Index" %>
+
+
<%= gettext "Validator index" %>
+
+
<%= gettext "Block" %>
+
+
<%= gettext "To" %>
+
+
<%= gettext "Age" %>
+
+
<%= gettext "Amount" %>
+
+
+
+ +
+
+
+ + <%= gettext "There are no withdrawals." %> + +
+
+ + <%= render BlockScoutWeb.CommonComponentsView, "_pagination_container.html", position: "bottom", cur_page_number: @page_number, show_pagination_limit: true, data_next_page_button: true, data_prev_page_button: true %> +
+ +
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_withdrawal_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_withdrawal_view.ex new file mode 100644 index 0000000000..9ff659e847 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/address_withdrawal_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.AddressWithdrawalView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 49fe37b23c..4ebb5e53ca 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -107,7 +107,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do "has_logs" => Chain.check_if_logs_at_address(address.hash, @api_true), "has_tokens" => Chain.check_if_tokens_at_address(address.hash, @api_true), "has_token_transfers" => Chain.check_if_token_transfers_at_address(address.hash, @api_true), - "watchlist_address_id" => Chain.select_watchlist_address_id(get_watchlist_id(conn), address.hash) + "watchlist_address_id" => Chain.select_watchlist_address_id(get_watchlist_id(conn), address.hash), + "has_beacon_chain_withdrawals" => Chain.check_if_withdrawals_at_address(address.hash, @api_true) }) end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex index cc2067913a..3a9e914400 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/block_view.ex @@ -7,6 +7,8 @@ defmodule BlockScoutWeb.API.V2.BlockView do alias Explorer.Chain.Block alias Explorer.Counters.BlockPriorityFeeCounter + @api_true [api?: true] + def render("message.json", assigns) do ApiView.render("message.json", assigns) end @@ -58,7 +60,9 @@ defmodule BlockScoutWeb.API.V2.BlockView do "gas_used_percentage" => gas_used_percentage(block), "burnt_fees_percentage" => burnt_fees_percentage(burned_fee, tx_fees), "type" => block |> BlockView.block_type() |> String.downcase(), - "tx_fees" => tx_fees + "tx_fees" => tx_fees, + "has_beacon_chain_withdrawals" => + if(single_block?, do: Chain.check_if_withdrawals_in_block(block.hash, @api_true), else: nil) } end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/withdrawal_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/withdrawal_view.ex new file mode 100644 index 0000000000..299d462972 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/withdrawal_view.ex @@ -0,0 +1,41 @@ +defmodule BlockScoutWeb.API.V2.WithdrawalView do + use BlockScoutWeb, :view + + alias BlockScoutWeb.API.V2.Helper + alias Explorer.Chain.Withdrawal + + def render("withdrawals.json", %{withdrawals: withdrawals, next_page_params: next_page_params}) do + %{"items" => Enum.map(withdrawals, &prepare_withdrawal(&1)), "next_page_params" => next_page_params} + end + + @spec prepare_withdrawal(Withdrawal.t()) :: map() + def prepare_withdrawal(%Withdrawal{block: %Ecto.Association.NotLoaded{}} = withdrawal) do + %{ + "index" => withdrawal.index, + "validator_index" => withdrawal.validator_index, + "receiver" => Helper.address_with_info(withdrawal.address, withdrawal.address_hash), + "amount" => withdrawal.amount + } + end + + def prepare_withdrawal(%Withdrawal{address: %Ecto.Association.NotLoaded{}} = withdrawal) do + %{ + "index" => withdrawal.index, + "validator_index" => withdrawal.validator_index, + "block_number" => withdrawal.block.number, + "amount" => withdrawal.amount, + "timestamp" => withdrawal.block.timestamp + } + end + + def prepare_withdrawal(%Withdrawal{} = withdrawal) do + %{ + "index" => withdrawal.index, + "validator_index" => withdrawal.validator_index, + "block_number" => withdrawal.block.number, + "receiver" => Helper.address_with_info(withdrawal.address, withdrawal.address_hash), + "amount" => withdrawal.amount, + "timestamp" => withdrawal.block.timestamp + } + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/block_withdrawal_view.ex b/apps/block_scout_web/lib/block_scout_web/views/block_withdrawal_view.ex new file mode 100644 index 0000000000..5fa812f198 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/block_withdrawal_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.BlockWithdrawalView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/withdrawal_view.ex b/apps/block_scout_web/lib/block_scout_web/views/withdrawal_view.ex new file mode 100644 index 0000000000..bbe7be4fcf --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/withdrawal_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.WithdrawalView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex index 20a92d8682..4b3f7a20ae 100644 --- a/apps/block_scout_web/lib/block_scout_web/web_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex @@ -104,6 +104,7 @@ defmodule BlockScoutWeb.WebRouter do resources "/block", BlockController, only: [:show], param: "hash_or_number" do resources("/transactions", BlockTransactionController, only: [:index], as: :transaction) + resources("/withdrawals", BlockWithdrawalController, only: [:index], as: :withdrawal) end resources("/blocks", BlockController, as: :blocks, only: [:index]) @@ -113,6 +114,7 @@ defmodule BlockScoutWeb.WebRouter do only: [:show], param: "hash_or_number" do resources("/transactions", BlockTransactionController, only: [:index], as: :transaction) + resources("/withdrawals", BlockWithdrawalController, only: [:index], as: :withdrawal) end get("/reorgs", BlockController, :reorg, as: :reorg) @@ -125,6 +127,8 @@ defmodule BlockScoutWeb.WebRouter do resources("/verified-contracts", VerifiedContractsController, only: [:index]) + resources("/withdrawals", WithdrawalController, only: [:index]) + get("/txs", TransactionController, :index) resources "/tx", TransactionController, only: [:show] do @@ -274,6 +278,13 @@ defmodule BlockScoutWeb.WebRouter do as: :token_transfers ) + resources( + "/withdrawals", + AddressWithdrawalController, + only: [:index], + as: :withdrawal + ) + resources("/tokens", AddressTokenController, only: [:index], as: :token) do resources( "/token-transfers", diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs new file mode 100644 index 0000000000..7a259f62e4 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_withdrawal_controller_test.exs @@ -0,0 +1,123 @@ +defmodule BlockScoutWeb.AddressWithdrawalControllerTest do + use BlockScoutWeb.ConnCase, async: true + use ExUnit.Case, async: false + + import BlockScoutWeb.WebRouter.Helpers, only: [address_withdrawal_path: 3, address_withdrawal_path: 4] + import BlockScoutWeb.WeiHelper, only: [format_wei_value: 2] + import Mox + + alias Explorer.Chain.{Address, Transaction} + alias Explorer.ExchangeRates.Token + + setup :verify_on_exit! + + describe "GET index/2" do + setup :set_mox_global + + setup do + configuration = Application.get_env(:explorer, :checksum_function) + Application.put_env(:explorer, :checksum_function, :eth) + + on_exit(fn -> + Application.put_env(:explorer, :checksum_function, configuration) + end) + end + + test "with invalid address hash", %{conn: conn} do + conn = get(conn, address_withdrawal_path(conn, :index, "invalid_address")) + + assert html_response(conn, 422) + end + + test "with valid address hash without address in the DB", %{conn: conn} do + conn = + get( + conn, + address_withdrawal_path(conn, :index, Address.checksum("0x8bf38d4764929064f2d4d3a56520a76ab3df415b"), %{ + "type" => "JSON" + }) + ) + + assert json_response(conn, 200) + tiles = json_response(conn, 200)["items"] + assert tiles |> length() == 0 + end + + test "returns withdrawals for the address", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(30, :withdrawal)) + + # to check that we can correctly render adress overview + get(conn, address_withdrawal_path(conn, :index, Address.checksum(address))) + + conn = get(conn, address_withdrawal_path(conn, :index, Address.checksum(address), %{"type" => "JSON"})) + + tiles = json_response(conn, 200)["items"] + indexes = Enum.map(address.withdrawals, &to_string(&1.index)) + + assert Enum.all?(indexes, fn index -> + Enum.any?(tiles, &String.contains?(&1, index)) + end) + end + + test "includes USD exchange rate value for address in assigns", %{conn: conn} do + address = insert(:address) + + conn = get(conn, address_withdrawal_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash))) + + assert %Token{} = conn.assigns.exchange_rate + end + + test "returns next page of results based on last seen withdrawal", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(60, :withdrawal)) + + {first_page, second_page} = + address.withdrawals + |> Enum.sort(&(&1.index >= &2.index)) + |> Enum.split(51) + + conn = + get(conn, address_withdrawal_path(BlockScoutWeb.Endpoint, :index, Address.checksum(address.hash)), %{ + "index" => first_page |> List.last() |> (& &1.index).() |> Integer.to_string(), + "type" => "JSON" + }) + + tiles = json_response(conn, 200)["items"] + + assert Enum.all?(second_page, fn withdrawal -> + Enum.any?(tiles, fn tile -> + # more strict check since simple index could occur in the tile accidentally + String.contains?(tile, to_string(withdrawal.index)) and + String.contains?(tile, to_string(withdrawal.validator_index)) and + String.contains?(tile, to_string(withdrawal.block.number)) and + String.contains?(tile, format_wei_value(withdrawal.amount, :ether)) + end) + end) + + refute Enum.any?(first_page, fn withdrawal -> + Enum.any?(tiles, fn tile -> + # more strict check since simple index could occur in the tile accidentally + String.contains?(tile, to_string(withdrawal.index)) and + String.contains?(tile, to_string(withdrawal.validator_index)) and + String.contains?(tile, to_string(withdrawal.block.number)) and + String.contains?(tile, format_wei_value(withdrawal.amount, :ether)) + end) + end) + end + + test "next_page_params exist if not on last page", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(51, :withdrawal)) + + conn = get(conn, address_withdrawal_path(conn, :index, Address.checksum(address.hash), %{"type" => "JSON"})) + + assert json_response(conn, 200)["next_page_path"] + end + + test "next_page_params are empty if on last page", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(1, :withdrawal)) + + conn = get(conn, address_withdrawal_path(conn, :index, Address.checksum(address.hash), %{"type" => "JSON"})) + + refute json_response(conn, 200)["next_page_path"] + end + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs index 69f393a183..b414a917bc 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs @@ -12,7 +12,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do Log, Token, TokenTransfer, - Transaction + Transaction, + Withdrawal } alias Explorer.Account.WatchlistAddress @@ -65,7 +66,8 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do "has_logs" => false, "has_tokens" => false, "has_token_transfers" => false, - "watchlist_address_id" => nil + "watchlist_address_id" => nil, + "has_beacon_chain_withdrawals" => false } request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}") @@ -1590,6 +1592,35 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do end end + describe "/addresses/{address_hash}/withdrawals" do + test "get empty list on non existing address", %{conn: conn} do + address = build(:address) + + request = get(conn, "/api/v2/addresses/#{address.hash}/withdrawals") + + assert %{"message" => "Not found"} = json_response(request, 404) + end + + test "get 422 on invalid address", %{conn: conn} do + request = get(conn, "/api/v2/addresses/0x/withdrawals") + + assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422) + end + + test "get withdrawals", %{conn: conn} do + address = insert(:address, withdrawals: insert_list(51, :withdrawal)) + + request = get(conn, "/api/v2/addresses/#{address.hash}/withdrawals") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/withdrawals", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, address.withdrawals) + end + end + describe "/addresses" do test "get empty list", %{conn: conn} do request = get(conn, "/api/v2/addresses") @@ -1699,6 +1730,10 @@ defmodule BlockScoutWeb.API.V2.AddressControllerTest do assert to_string(log.transaction_hash) == json["tx_hash"] end + defp compare_item(%Withdrawal{} = withdrawal, json) do + assert withdrawal.index == json["index"] + end + defp check_paginated_response(first_page_resp, second_page_resp, list) do assert Enum.count(first_page_resp["items"]) == 50 assert first_page_resp["next_page_params"] != nil diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs index f4bff6e168..9e115db4bd 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs @@ -1,7 +1,7 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do use BlockScoutWeb.ConnCase - alias Explorer.Chain.{Address, Block, Transaction} + alias Explorer.Chain.{Address, Block, Transaction, Withdrawal} setup do Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id()) @@ -311,6 +311,77 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do end end + describe "/blocks/{block_hash_or_number}/withdrawals" do + test "return 422 on invalid parameter", %{conn: conn} do + request_1 = get(conn, "/api/v2/blocks/0x123123/withdrawals") + assert %{"message" => "Invalid hash"} = json_response(request_1, 422) + + request_2 = get(conn, "/api/v2/blocks/123qwe/withdrawals") + assert %{"message" => "Invalid number"} = json_response(request_2, 422) + end + + test "return 404 on non existing block", %{conn: conn} do + block = build(:block) + + request_1 = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert %{"message" => "Not found"} = json_response(request_1, 404) + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert %{"message" => "Not found"} = json_response(request_2, 404) + end + + test "get empty list", %{conn: conn} do + block = insert(:block) + + request = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + + request = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get withdrawals", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(3, :withdrawal)) + + [withdrawal | _] = Enum.reverse(block.withdrawals) + + request = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 3 + assert response["next_page_params"] == nil + compare_item(withdrawal, Enum.at(response["items"], 0)) + + request = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert response_1 = json_response(request, 200) + assert response_1 == response + end + + test "get withdrawals with working next_page_params", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(51, :withdrawal)) + + request = get(conn, "/api/v2/blocks/#{block.number}/withdrawals") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/blocks/#{block.number}/withdrawals", response["next_page_params"]) + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, block.withdrawals) + + request_1 = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals") + assert response_1 = json_response(request_1, 200) + + assert response_1 == response + + request_2 = get(conn, "/api/v2/blocks/#{block.hash}/withdrawals", response_1["next_page_params"]) + assert response_2 = json_response(request_2, 200) + assert response_2 == response_2nd_page + end + end + defp compare_item(%Block{} = block, json) do assert to_string(block.hash) == json["hash"] assert block.number == json["height"] @@ -324,6 +395,10 @@ defmodule BlockScoutWeb.API.V2.BlockControllerTest do assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"] end + defp compare_item(%Withdrawal{} = withdrawal, json) do + assert withdrawal.index == json["index"] + end + defp check_paginated_response(first_page_resp, second_page_resp, list) do assert Enum.count(first_page_resp["items"]) == 50 assert first_page_resp["next_page_params"] != nil diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs new file mode 100644 index 0000000000..dbd829f27c --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/withdrawal_controller_test.exs @@ -0,0 +1,55 @@ +defmodule BlockScoutWeb.API.V2.WithdrawalControllerTest do + use BlockScoutWeb.ConnCase + + alias Explorer.Chain.Withdrawal + + describe "/withdrawals" do + test "empty lists", %{conn: conn} do + request = get(conn, "/api/v2/blocks") + assert response = json_response(request, 200) + assert response["items"] == [] + assert response["next_page_params"] == nil + end + + test "get withdrawal", %{conn: conn} do + block = insert(:withdrawal) + + request = get(conn, "/api/v2/withdrawals") + + assert response = json_response(request, 200) + assert Enum.count(response["items"]) == 1 + assert response["next_page_params"] == nil + compare_item(block, Enum.at(response["items"], 0)) + end + + test "can paginate", %{conn: conn} do + withdrawals = + 51 + |> insert_list(:withdrawal) + + request = get(conn, "/api/v2/withdrawals") + assert response = json_response(request, 200) + + request_2nd_page = get(conn, "/api/v2/withdrawals", response["next_page_params"]) + + assert response_2nd_page = json_response(request_2nd_page, 200) + + check_paginated_response(response, response_2nd_page, withdrawals) + end + end + + defp compare_item(%Withdrawal{} = withdrawal, json) do + assert withdrawal.index == json["index"] + end + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs new file mode 100644 index 0000000000..61d3d76c46 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_withdrawal_controller_test.exs @@ -0,0 +1,139 @@ +defmodule BlockScoutWeb.BlockWithdrawalControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.WebRouter.Helpers, only: [block_withdrawal_path: 3] + + describe "GET index/2" do + test "with invalid block number", %{conn: conn} do + conn = get(conn, block_withdrawal_path(conn, :index, "unknown")) + + assert html_response(conn, 404) + end + + test "with valid block number below the tip", %{conn: conn} do + insert(:block, number: 666) + + conn = get(conn, block_withdrawal_path(conn, :index, "1")) + + assert html_response(conn, 404) =~ "This block has not been processed yet." + end + + test "with valid block number above the tip", %{conn: conn} do + block = insert(:block) + + conn = get(conn, block_withdrawal_path(conn, :index, block.number + 1)) + + assert_block_above_tip(conn) + end + + test "returns withdrawals for the block", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(3, :withdrawal)) + + # to check that we can render a block overview + get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block)) + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.count(items) == 3 + end + + test "non-consensus block number without consensus blocks is treated as consensus number above tip", %{conn: conn} do + block = insert(:block, consensus: false) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: block.hash) + + conn = get(conn, block_withdrawal_path(conn, :index, block.number)) + + assert_block_above_tip(conn) + end + + test "non-consensus block number above consensus block number is treated as consensus number above tip", %{ + conn: conn + } do + consensus_block = insert(:block, consensus: true, number: 1) + block = insert(:block, consensus: false, number: consensus_block.number + 1) + + transaction = insert(:transaction) + insert(:transaction_fork, hash: transaction.hash, uncle_hash: block.hash) + + conn = get(conn, block_withdrawal_path(conn, :index, block.number)) + + assert_block_above_tip(conn) + end + + test "does not return transactions for invalid block hash", %{conn: conn} do + conn = get(conn, block_withdrawal_path(conn, :index, "0x0")) + + assert html_response(conn, 404) + end + + test "with valid not-indexed hash", %{conn: conn} do + conn = get(conn, block_withdrawal_path(conn, :index, block_hash())) + + assert html_response(conn, 404) =~ "Block not found, please try again later." + end + + test "does not return unrelated transactions", %{conn: conn} do + insert(:withdrawal) + block = insert(:block) + + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + assert json_response(conn, 200) + + {:ok, %{"items" => items}} = + conn.resp_body + |> Poison.decode() + + assert Enum.empty?(items) + end + + test "next_page_path exists if not on last page", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(60, :withdrawal)) + + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + assert next_page_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + block = insert(:block, withdrawals: insert_list(1, :withdrawal)) + + conn = get(conn, block_withdrawal_path(BlockScoutWeb.Endpoint, :index, block), %{type: "JSON"}) + + {:ok, %{"next_page_path" => next_page_path}} = + conn.resp_body + |> Poison.decode() + + refute next_page_path + end + + test "displays miner primary address name", %{conn: conn} do + miner_name = "POA Miner Pool" + %{address: miner_address} = insert(:address_name, name: miner_name, primary: true) + + block = insert(:block, miner: miner_address, miner_hash: nil) + + conn = get(conn, block_withdrawal_path(conn, :index, block)) + assert html_response(conn, 200) =~ miner_name + end + end + + defp assert_block_above_tip(conn) do + assert conn + |> html_response(404) + |> Floki.find(~S|.error-descr|) + |> Floki.text() + |> String.trim() == "Easy Cowboy! This block does not exist yet!" + end +end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs new file mode 100644 index 0000000000..d62fb54191 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/controllers/withdrawal_controller_test.exs @@ -0,0 +1,60 @@ +defmodule BlockScoutWeb.WithdrawalControllerTest do + use BlockScoutWeb.ConnCase + + import BlockScoutWeb.WebRouter.Helpers, only: [withdrawal_path: 2, withdrawal_path: 3] + + alias Explorer.Chain.Withdrawal + + describe "GET index/2" do + test "returns all withdrawals", %{conn: conn} do + insert_list(4, :withdrawal) + + conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 4 + end + + test "returns next page of results based on last withdrawal", %{conn: conn} do + insert_list(50, :withdrawal) + + withdrawal = insert(:withdrawal) + + conn = + get(conn, withdrawal_path(conn, :index), %{ + "type" => "JSON", + "index" => Integer.to_string(withdrawal.index) + }) + + items = Map.get(json_response(conn, 200), "items") + + assert length(items) == 50 + end + + test "next_page_path exist if not on last page", %{conn: conn} do + %Withdrawal{index: index} = + 60 + |> insert_list(:withdrawal) + |> Enum.fetch!(10) + + conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) + + expected_path = + withdrawal_path(conn, :index, %{ + index: index, + items_count: "50" + }) + + assert Map.get(json_response(conn, 200), "next_page_path") == expected_path + end + + test "next_page_path is empty if on last page", %{conn: conn} do + insert(:withdrawal) + + conn = get(conn, withdrawal_path(conn, :index), %{"type" => "JSON"}) + + refute conn |> json_response(200) |> Map.get("next_page_path") + end + end +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 3b5fbf4470..76e7417c90 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -261,6 +261,17 @@ defmodule EthereumJSONRPC do |> fetch_blocks_by_params(&Block.ByNumber.request/1, json_rpc_named_arguments) end + @doc """ + Fetches blocks by block number list. + """ + @spec fetch_blocks_by_numbers([block_number()], json_rpc_named_arguments) :: + {:ok, Blocks.t()} | {:error, reason :: term} + def fetch_blocks_by_numbers(block_numbers, json_rpc_named_arguments) do + block_numbers + |> Enum.map(fn number -> %{number: number} end) + |> fetch_blocks_by_params(&Block.ByNumber.request/1, json_rpc_named_arguments) + end + @doc """ Fetches uncle blocks by nephew hashes and indices. """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex index 7c4de364b9..5737aa9096 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex @@ -6,7 +6,7 @@ defmodule EthereumJSONRPC.Block do import EthereumJSONRPC, only: [quantity_to_integer: 1, timestamp_to_datetime: 1] - alias EthereumJSONRPC.{Transactions, Uncles} + alias EthereumJSONRPC.{Transactions, Uncles, Withdrawals} @type elixir :: %{String.t() => non_neg_integer | DateTime.t() | String.t() | nil} @type params :: %{ @@ -29,7 +29,8 @@ defmodule EthereumJSONRPC.Block do total_difficulty: non_neg_integer(), transactions_root: EthereumJSONRPC.hash(), uncles: [EthereumJSONRPC.hash()], - base_fee_per_gas: non_neg_integer() + base_fee_per_gas: non_neg_integer(), + withdrawals_root: EthereumJSONRPC.hash() } @typedoc """ @@ -67,6 +68,7 @@ defmodule EthereumJSONRPC.Block do [uncles](https://bitcoin.stackexchange.com/questions/39329/in-ethereum-what-is-an-uncle-block) `t:EthereumJSONRPC.hash/0`. * `"baseFeePerGas"` - `t:EthereumJSONRPC.quantity/0` of wei to denote amount of fee burned per unit gas used. Introduced in [EIP-1559](https://github.com/ethereum/EIPs/blob/master/EIPS/eip-1559.md) + * `"withdrawalsRoot"` - `t:EthereumJSONRPC.hash/0` of the root of the withdrawals. """ @type t :: %{String.t() => EthereumJSONRPC.data() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | nil} @@ -140,7 +142,8 @@ defmodule EthereumJSONRPC.Block do timestamp: Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"), total_difficulty: 340282366920938463463374607431465668165, transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: [] + uncles: [], + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" } [Geth] `elixir` can be converted to params @@ -188,7 +191,8 @@ defmodule EthereumJSONRPC.Block do timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), total_difficulty: 1039309006117, transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: [] + uncles: [], + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" } """ @@ -235,7 +239,9 @@ defmodule EthereumJSONRPC.Block do total_difficulty: total_difficulty, transactions_root: transactions_root, uncles: uncles, - base_fee_per_gas: base_fee_per_gas + base_fee_per_gas: base_fee_per_gas, + withdrawals_root: + Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") } end @@ -279,7 +285,9 @@ defmodule EthereumJSONRPC.Block do timestamp: timestamp, transactions_root: transactions_root, uncles: uncles, - base_fee_per_gas: base_fee_per_gas + base_fee_per_gas: base_fee_per_gas, + withdrawals_root: + Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") } end @@ -323,7 +331,9 @@ defmodule EthereumJSONRPC.Block do timestamp: timestamp, total_difficulty: total_difficulty, transactions_root: transactions_root, - uncles: uncles + uncles: uncles, + withdrawals_root: + Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") } end @@ -366,7 +376,9 @@ defmodule EthereumJSONRPC.Block do state_root: state_root, timestamp: timestamp, transactions_root: transactions_root, - uncles: uncles + uncles: uncles, + withdrawals_root: + Map.get(elixir, "withdrawalsRoot", "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421") } end @@ -506,6 +518,73 @@ defmodule EthereumJSONRPC.Block do |> Enum.map(fn {uncle_hash, index} -> %{"hash" => uncle_hash, "nephewHash" => nephew_hash, "index" => index} end) end + @doc """ + Get `t:EthereumJSONRPC.Withdrawals.elixir/0` from `t:elixir/0`. + + iex> EthereumJSONRPC.Block.elixir_to_withdrawals( + ...> %{ + ...> "baseFeePerGas" => 7, + ...> "difficulty" => 0, + ...> "extraData" => "0x", + ...> "gasLimit" => 7_009_844, + ...> "gasUsed" => 0, + ...> "hash" => "0xc0b72358464dc55cb51c990360d94809e40f291603a7664d55cf83f87edb799d", + ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ...> "miner" => "0xe7c180eada8f60d63e9671867b2e0ca2649207a8", + ...> "mixHash" => "0x9cc5c22d51f47caf700636f629e0765a5fe3388284682434a3717d099960681a", + ...> "nonce" => "0x0000000000000000", + ...> "number" => 541, + ...> "parentHash" => "0x9bc27f8db423bea352a32b819330df307dd351da71f3b3f8ac4ad56856c1e053", + ...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + ...> "size" => 1107, + ...> "stateRoot" => "0x9de54b38595b4b8baeece667ae1f7bec8cfc814a514248985e3d98c91d331c71", + ...> "timestamp" => Timex.parse!("2022-12-15T21:06:15Z", "{ISO:Extended:Z}"), + ...> "totalDifficulty" => 1, + ...> "transactions" => [], + ...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ...> "uncles" => [], + ...> "withdrawals" => [ + ...> %{ + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => 4_040_000_000_000, + ...> "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + ...> "index" => 3867, + ...> "validatorIndex" => 1721 + ...> }, + ...> %{ + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => 4_040_000_000_000, + ...> "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + ...> "index" => 3868, + ...> "validatorIndex" => 1771 + ...> } + ...> ], + ...> "withdrawalsRoot" => "0x23e926286a20cba56ee0fcf0eca7aae44f013bd9695aaab58478e8d69b0c3d68" + ...> } + ...> ) + [ + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4040000000000, + "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + "index" => 3867, + "validatorIndex" => 1721 + }, + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4040000000000, + "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + "index" => 3868, + "validatorIndex" => 1771 + } + ] + + """ + @spec elixir_to_withdrawals(elixir) :: Withdrawals.elixir() + def elixir_to_withdrawals(%{"withdrawals" => withdrawals}), do: withdrawals + def elixir_to_withdrawals(_), do: [] + @doc """ Decodes the stringly typed numerical fields to `t:non_neg_integer/0` and the timestamps to `t:DateTime.t/0` @@ -535,7 +614,22 @@ defmodule EthereumJSONRPC.Block do ...> "totalDifficulty" => "0x2ffffffffffffffffffffffffedf78e41", ...> "transactions" => [], ...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - ...> "uncles" => [] + ...> "uncles" => [], + ...> "withdrawals" => [ + ...> %{ + ...> "index" => "0xf1b", + ...> "validatorIndex" => "0x6b9", + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => "0x3aca2c3d000" + ...> }, + ...> %{ + ...> "index" => "0xf1c", + ...> "validatorIndex" => "0x6eb", + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => "0x3aca2c3d000" + ...> } + ...> ], + ...> "withdrawalsRoot" => "0x23e926286a20cba56ee0fcf0eca7aae44f013bd9695aaab58478e8d69b0c3d68" ...> } ...> ) %{ @@ -563,43 +657,71 @@ defmodule EthereumJSONRPC.Block do "totalDifficulty" => 1020847100762815390390123822295002091073, "transactions" => [], "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] + "uncles" => [], + "withdrawals" => [ + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4_040_000_000_000, + "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + "index" => 3867, + "blockNumber" => 3, + "validatorIndex" => 1721 + }, + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4_040_000_000_000, + "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + "index" => 3868, + "blockNumber" => 3, + "validatorIndex" => 1771 + } + ], + "withdrawalsRoot" => "0x23e926286a20cba56ee0fcf0eca7aae44f013bd9695aaab58478e8d69b0c3d68" } """ def to_elixir(block) when is_map(block) do - Enum.into(block, %{}, &entry_to_elixir/1) + Enum.into(block, %{}, &entry_to_elixir(&1, block)) end - defp entry_to_elixir({key, quantity}) + defp entry_to_elixir({key, quantity}, _block) when key in ~w(difficulty gasLimit gasUsed minimumGasPrice baseFeePerGas number size cumulativeDifficulty totalDifficulty paidFees) and not is_nil(quantity) do {key, quantity_to_integer(quantity)} end # Size and totalDifficulty may be `nil` for uncle blocks - defp entry_to_elixir({key, nil}) when key in ~w(size totalDifficulty) do + defp entry_to_elixir({key, nil}, _block) when key in ~w(size totalDifficulty) do {key, nil} end # double check that no new keys are being missed by requiring explicit match for passthrough # `t:EthereumJSONRPC.address/0` and `t:EthereumJSONRPC.hash/0` pass through as `Explorer.Chain` can verify correct # hash format - defp entry_to_elixir({key, _} = entry) + defp entry_to_elixir({key, _} = entry, _block) when key in ~w(author extraData hash logsBloom miner mixHash nonce parentHash receiptsRoot sealFields sha3Uncles - signature stateRoot step transactionsRoot uncles), + signature stateRoot step transactionsRoot uncles withdrawalsRoot), do: entry - defp entry_to_elixir({"timestamp" = key, timestamp}) do + defp entry_to_elixir({"timestamp" = key, timestamp}, _block) do {key, timestamp_to_datetime(timestamp)} end - defp entry_to_elixir({"transactions" = key, transactions}) do + defp entry_to_elixir({"transactions" = key, transactions}, _block) do {key, Transactions.to_elixir(transactions)} end + defp entry_to_elixir({"withdrawals" = key, nil}, _block) do + {key, []} + end + + defp entry_to_elixir({"withdrawals" = key, withdrawals}, %{"hash" => block_hash, "number" => block_number}) + when not is_nil(block_number) do + {key, Withdrawals.to_elixir(withdrawals, block_hash, quantity_to_integer(block_number))} + end + # Arbitrum fields - defp entry_to_elixir({"l1BlockNumber", _}) do + defp entry_to_elixir({"l1BlockNumber", _}, _block) do {:ignore, :ignore} end @@ -609,7 +731,7 @@ defmodule EthereumJSONRPC.Block do # blockExtraData extDataHash - Avalanche https://github.com/blockscout/blockscout/pull/5348 # vrf vrfProof - Harmony # ... - defp entry_to_elixir({_, _}) do + defp entry_to_elixir({_, _}, _block) do {:ignore, :ignore} end end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex index dc1740a4aa..8e76783b40 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex @@ -4,7 +4,7 @@ defmodule EthereumJSONRPC.Blocks do and [`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber) from batch requests. """ - alias EthereumJSONRPC.{Block, Transactions, Transport, Uncles} + alias EthereumJSONRPC.{Block, Transactions, Transport, Uncles, Withdrawals} @type elixir :: [Block.elixir()] @type params :: [Block.params()] @@ -12,12 +12,14 @@ defmodule EthereumJSONRPC.Blocks do blocks_params: [map()], block_second_degree_relations_params: [map()], transactions_params: [map()], + withdrawals_params: Withdrawals.params(), errors: [Transport.error()] } defstruct blocks_params: [], block_second_degree_relations_params: [], transactions_params: [], + withdrawals_params: [], errors: [] def requests(id_to_params, request) when is_map(id_to_params) and is_function(request, 1) do @@ -45,16 +47,19 @@ defmodule EthereumJSONRPC.Blocks do elixir_uncles = elixir_to_uncles(elixir_blocks) elixir_transactions = elixir_to_transactions(elixir_blocks) + elixir_withdrawals = elixir_to_withdrawals(elixir_blocks) block_second_degree_relations_params = Uncles.elixir_to_params(elixir_uncles) transactions_params = Transactions.elixir_to_params(elixir_transactions) + withdrawals_params = Withdrawals.elixir_to_params(elixir_withdrawals) blocks_params = elixir_to_params(elixir_blocks) %__MODULE__{ errors: errors, blocks_params: blocks_params, block_second_degree_relations_params: block_second_degree_relations_params, - transactions_params: transactions_params + transactions_params: transactions_params, + withdrawals_params: withdrawals_params } end @@ -110,7 +115,8 @@ defmodule EthereumJSONRPC.Blocks do timestamp: Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"), total_difficulty: 131072, transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"] + uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"], + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" } ] @@ -271,6 +277,74 @@ defmodule EthereumJSONRPC.Blocks do Enum.flat_map(elixir, &Block.elixir_to_uncles/1) end + @doc """ + Extracts the `t:EthereumJSONRPC.Withdrawals.elixir/0` from the `t:elixir/0`. + + iex> EthereumJSONRPC.Blocks.elixir_to_withdrawals([ + ...> %{ + ...> "baseFeePerGas" => 7, + ...> "difficulty" => 0, + ...> "extraData" => "0x", + ...> "gasLimit" => 7_009_844, + ...> "gasUsed" => 0, + ...> "hash" => "0xc0b72358464dc55cb51c990360d94809e40f291603a7664d55cf83f87edb799d", + ...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + ...> "miner" => "0xe7c180eada8f60d63e9671867b2e0ca2649207a8", + ...> "mixHash" => "0x9cc5c22d51f47caf700636f629e0765a5fe3388284682434a3717d099960681a", + ...> "nonce" => "0x0000000000000000", + ...> "number" => 541, + ...> "parentHash" => "0x9bc27f8db423bea352a32b819330df307dd351da71f3b3f8ac4ad56856c1e053", + ...> "receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", + ...> "size" => 1107, + ...> "stateRoot" => "0x9de54b38595b4b8baeece667ae1f7bec8cfc814a514248985e3d98c91d331c71", + ...> "timestamp" => Timex.parse!("2022-12-15T21:06:15Z", "{ISO:Extended:Z}"), + ...> "totalDifficulty" => 1, + ...> "transactions" => [], + ...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", + ...> "uncles" => [], + ...> "withdrawals" => [ + ...> %{ + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => 4_040_000_000_000, + ...> "blockHash" => "0xc0b72358464dc55cb51c990360d94809e40f291603a7664d55cf83f87edb799d", + ...> "index" => 3867, + ...> "validatorIndex" => 1721 + ...> }, + ...> %{ + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => 4_040_000_000_000, + ...> "blockHash" => "0xc0b72358464dc55cb51c990360d94809e40f291603a7664d55cf83f87edb799d", + ...> "index" => 3868, + ...> "validatorIndex" => 1771 + ...> } + ...> ], + ...> "withdrawalsRoot" => "0x23e926286a20cba56ee0fcf0eca7aae44f013bd9695aaab58478e8d69b0c3d68" + ...> } + ...> ]) + [ + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4040000000000, + "blockHash" => "0xc0b72358464dc55cb51c990360d94809e40f291603a7664d55cf83f87edb799d", + "index" => 3867, + "validatorIndex" => 1721 + }, + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4040000000000, + "blockHash" => "0xc0b72358464dc55cb51c990360d94809e40f291603a7664d55cf83f87edb799d", + "index" => 3868, + "validatorIndex" => 1771 + } + ] + + """ + @spec elixir_to_withdrawals(elixir) :: Withdrawals.elixir() + def elixir_to_withdrawals(elixir) do + Enum.flat_map(elixir, &Block.elixir_to_withdrawals/1) + end + @doc """ Decodes the stringly typed numerical fields to `t:non_neg_integer/0` and the timestamps to `t:DateTime.t/0` @@ -299,7 +373,22 @@ defmodule EthereumJSONRPC.Blocks do ...> "totalDifficulty" => "0x20000", ...> "transactions" => [], ...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - ...> "uncles" => [] + ...> "uncles" => [], + ...> "withdrawals" => [ + ...> %{ + ...> "index" => "0xf1b", + ...> "validatorIndex" => "0x6b9", + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => "0x3aca2c3d000" + ...> }, + ...> %{ + ...> "index" => "0xf1c", + ...> "validatorIndex" => "0x6eb", + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => "0x3aca2c3d000" + ...> } + ...> ], + ...> "withdrawalsRoot" => "0x23e926286a20cba56ee0fcf0eca7aae44f013bd9695aaab58478e8d69b0c3d68" ...> } ...> ] ...> ) @@ -327,7 +416,26 @@ defmodule EthereumJSONRPC.Blocks do "totalDifficulty" => 131072, "transactions" => [], "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - "uncles" => [] + "uncles" => [], + "withdrawals" => [ + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4_040_000_000_000, + "blockHash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", + "index" => 3867, + "validatorIndex" => 1721, + "blockNumber" => 0 + }, + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4_040_000_000_000, + "blockHash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f", + "index" => 3868, + "validatorIndex" => 1771, + "blockNumber" => 0 + } + ], + "withdrawalsRoot" => "0x23e926286a20cba56ee0fcf0eca7aae44f013bd9695aaab58478e8d69b0c3d68" } ] """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawal.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawal.ex new file mode 100644 index 0000000000..6c86c73d43 --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawal.ex @@ -0,0 +1,101 @@ +defmodule EthereumJSONRPC.Withdrawal do + @moduledoc """ + Withdrawal format included in the return of + `eth_getBlockByHash` and `eth_getBlockByNumber` + """ + + import EthereumJSONRPC, only: [quantity_to_integer: 1] + + @type elixir :: %{ + String.t() => EthereumJSONRPC.address() | EthereumJSONRPC.hash() | String.t() | non_neg_integer() | nil + } + + @typedoc """ + * `"index"` - the withdrawal number `t:EthereumJSONRPC.quantity/0`. + * `"validatorIndex"` - the validator number initiated the withdrawal `t:EthereumJSONRPC.quantity/0`. + * `"address"` - `t:EthereumJSONRPC.address/0` of the receiver. + * `"amount"` - `t:EthereumJSONRPC.quantity/0` of wei transferred. + """ + @type t :: %{ + String.t() => + EthereumJSONRPC.address() | EthereumJSONRPC.hash() | EthereumJSONRPC.quantity() | String.t() | nil + } + + @type params :: %{ + index: non_neg_integer(), + validator_index: non_neg_integer(), + address_hash: EthereumJSONRPC.address(), + block_hash: EthereumJSONRPC.hash(), + block_number: non_neg_integer(), + amount: non_neg_integer() + } + + @doc """ + Converts `t:elixir/0` to `t:params/0`. + + iex> EthereumJSONRPC.Withdrawal.elixir_to_params( + ...> %{ + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => 4040000000000, + ...> "index" => 3867, + ...> "validatorIndex" => 1721, + ...> "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + ...> "blockNumber" => 3 + ...> } + ...> ) + %{ + address_hash: "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + amount: 4040000000000, + block_hash: "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + block_number: 3, + index: 3867, + validator_index: 1721 + } + """ + @spec elixir_to_params(elixir) :: params + def elixir_to_params(%{ + "index" => index, + "validatorIndex" => validator_index, + "address" => address_hash, + "amount" => amount, + "blockHash" => block_hash, + "blockNumber" => block_number + }) do + %{ + index: index, + validator_index: validator_index, + address_hash: address_hash, + block_hash: block_hash, + block_number: block_number, + amount: amount + } + end + + @doc """ + Decodes the stringly typed numerical fields to `t:non_neg_integer/0`. + + iex> EthereumJSONRPC.Withdrawal.to_elixir( + ...> %{ + ...> "index" => "0xf1b", + ...> "validatorIndex" => "0x6b9", + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => "0x3aca2c3d000" + ...> }, "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", 1 + ...> ) + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4040000000000, + "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + "index" => 3867, + "validatorIndex" => 1721, + "blockNumber" => 1 + } + """ + @spec to_elixir(%{String.t() => String.t()}, String.t(), non_neg_integer()) :: elixir + def to_elixir(withdrawal, block_hash, block_number) when is_map(withdrawal) do + Enum.into(withdrawal, %{"blockHash" => block_hash, "blockNumber" => block_number}, &entry_to_elixir/1) + end + + defp entry_to_elixir({key, value}) when key in ~w(index validatorIndex amount), do: {key, quantity_to_integer(value)} + defp entry_to_elixir({key, value}) when key in ~w(address), do: {key, value} +end diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawals.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawals.ex new file mode 100644 index 0000000000..1c520b9f9a --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/withdrawals.ex @@ -0,0 +1,67 @@ +defmodule EthereumJSONRPC.Withdrawals do + @moduledoc """ + List of withdrawals format included in the return of + `eth_getBlockByHash` and `eth_getBlockByNumber` + """ + + alias EthereumJSONRPC.Withdrawal + + @type elixir :: [Withdrawal.elixir()] + @type params :: [Withdrawal.params()] + @type t :: [Withdrawal.t()] + + @doc """ + Converts `t:elixir/0` to `t:params/0`. + + iex> EthereumJSONRPC.Withdrawals.elixir_to_params([ + ...> %{ + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => 4040000000000, + ...> "index" => 3867, + ...> "validatorIndex" => 1721, + ...> "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + ...> "blockNumber" => 1 + ...> } + ...> ]) + [ + %{ + address_hash: "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + amount: 4040000000000, + block_hash: "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + index: 3867, + validator_index: 1721, + block_number: 1 + } + ] + """ + @spec elixir_to_params(elixir) :: params + def elixir_to_params(elixir) when is_list(elixir) do + Enum.map(elixir, &Withdrawal.elixir_to_params/1) + end + + @doc """ + Decodes stringly typed fields in entries of `withdrawals`. + + iex> EthereumJSONRPC.Withdrawals.to_elixir([ + ...> %{ + ...> "index" => "0xf1b", + ...> "validatorIndex" => "0x6b9", + ...> "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + ...> "amount" => "0x3aca2c3d000" + ...> }], "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", 3) + [ + %{ + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => 4040000000000, + "blockHash" => "0x7f035c5f3c0678250853a1fde6027def7cac1812667bd0d5ab7ccb94eb8b6f3a", + "index" => 3867, + "blockNumber" => 3, + "validatorIndex" => 1721 + } + ] + """ + @spec to_elixir([%{String.t() => String.t()}], String.t(), non_neg_integer()) :: elixir + def to_elixir(withdrawals, block_hash, block_number) when is_list(withdrawals) do + Enum.map(withdrawals, &Withdrawal.to_elixir(&1, block_hash, block_number)) + end +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs index 23e1e442c5..c409ecd99c 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/block_test.exs @@ -52,7 +52,8 @@ defmodule EthereumJSONRPC.BlockTest do timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"), total_difficulty: nil, transactions_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", - uncles: [] + uncles: [], + withdrawals_root: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421" } end end @@ -62,4 +63,14 @@ defmodule EthereumJSONRPC.BlockTest do assert Block.elixir_to_transactions(%{}) == [] end end + + describe "elixir_to_withdrawals/1" do + test "converts to empty list if there is no withdrawals key" do + assert Block.elixir_to_withdrawals(%{}) == [] + end + + test "converts to empty list if withdrawals is nil" do + assert Block.elixir_to_withdrawals(%{withdrawals: nil}) == [] + end + end end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawal_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawal_test.exs new file mode 100644 index 0000000000..8f8214c62d --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawal_test.exs @@ -0,0 +1,5 @@ +defmodule EthereumJSONRPC.WithdrawalTest do + use ExUnit.Case, async: true + + doctest EthereumJSONRPC.Withdrawal +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawals_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawals_test.exs new file mode 100644 index 0000000000..a4d4b48459 --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/withdrawals_test.exs @@ -0,0 +1,5 @@ +defmodule EthereumJSONRPC.WithdrawalsTest do + use ExUnit.Case, async: true + + doctest EthereumJSONRPC.Withdrawals +end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 802cbe6c2a..3b89dbf9d4 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -61,7 +61,8 @@ defmodule Explorer.Chain do Token.Instance, TokenTransfer, Transaction, - Wei + Wei, + Withdrawal } alias Explorer.Chain.Block.{EmissionReward, Reward} @@ -615,6 +616,21 @@ defmodule Explorer.Chain do |> select_repo(options).all() end + @spec address_hash_to_withdrawals( + Hash.Address.t(), + [paging_options | necessity_by_association_option] + ) :: [Withdrawal.t()] + def address_hash_to_withdrawals(address_hash, options \\ []) when is_list(options) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + address_hash + |> Withdrawal.address_hash_to_withdrawals_query() + |> join_associations(necessity_by_association) + |> handle_withdrawals_paging_options(paging_options) + |> select_repo(options).all() + end + @doc """ address_hash_to_token_transfers_including_contract/2 function returns token transfers on address (to/from/contract). It is used by CSV export of token transfers button. @@ -990,6 +1006,21 @@ defmodule Explorer.Chain do )).() end + @spec block_to_withdrawals( + Hash.Full.t(), + [paging_options | necessity_by_association_option] + ) :: [Withdrawal.t()] + def block_to_withdrawals(block_hash, options \\ []) when is_list(options) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + block_hash + |> Withdrawal.block_hash_to_withdrawals_query() + |> join_associations(necessity_by_association) + |> handle_withdrawals_paging_options(paging_options) + |> select_repo(options).all() + end + @doc """ Finds sum of gas_used for new (EIP-1559) txs belongs to block """ @@ -1066,6 +1097,13 @@ defmodule Explorer.Chain do Repo.aggregate(query, :count, :hash) end + @spec check_if_withdrawals_in_block(Hash.Full.t()) :: boolean() + def check_if_withdrawals_in_block(block_hash, options \\ []) do + block_hash + |> Withdrawal.block_hash_to_withdrawals_unordered_query() + |> select_repo(options).exists?() + end + @spec address_to_incoming_transaction_count(Hash.Address.t()) :: non_neg_integer() def address_to_incoming_transaction_count(address_hash) do to_address_query = @@ -2665,6 +2703,13 @@ defmodule Explorer.Chain do ) end + @spec check_if_withdrawals_at_address(Hash.Address.t()) :: boolean() + def check_if_withdrawals_at_address(address_hash, options \\ []) do + address_hash + |> Withdrawal.address_hash_to_withdrawals_unordered_query() + |> select_repo(options).exists?() + end + @doc """ Counts all of the block validations and groups by the `miner_hash`. """ @@ -4578,6 +4623,14 @@ defmodule Explorer.Chain do |> limit(^paging_options.page_size) end + defp handle_withdrawals_paging_options(query, nil), do: query + + defp handle_withdrawals_paging_options(query, paging_options) do + query + |> Withdrawal.page_withdrawals(paging_options) + |> limit(^paging_options.page_size) + end + defp handle_random_access_paging_options(query, empty_options) when empty_options in [nil, [], %{}], do: limit(query, ^(@default_page_size + 1)) @@ -6797,5 +6850,15 @@ defmodule Explorer.Chain do watchlist_names = Enum.reduce(watchlist_addresses, %{}, fn wa, acc -> Map.put(acc, wa.address_hash, wa.name) end) {watchlist_names, address_hashes_to_mined_transactions_without_rewards(address_hashes, options)} + end + + def list_withdrawals(options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + + Withdrawal.list_withdrawals() + |> join_associations(necessity_by_association) + |> handle_withdrawals_paging_options(paging_options) + |> select_repo(options).all() end end diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 2b60f9ffe9..44b317a42b 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -20,7 +20,8 @@ defmodule Explorer.Chain.Address do SmartContractAdditionalSource, Token, Transaction, - Wei + Wei, + Withdrawal } alias Explorer.Chain.Cache.NetVersion @@ -120,6 +121,7 @@ defmodule Explorer.Chain.Address do has_many(:names, Address.Name, foreign_key: :address_hash) has_many(:decompiled_smart_contracts, DecompiledSmartContract, foreign_key: :address_hash) has_many(:smart_contract_additional_sources, SmartContractAdditionalSource, foreign_key: :address_hash) + has_many(:withdrawals, Withdrawal, foreign_key: :address_hash) timestamps() end diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index dfd4512644..d427ea476c 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -7,7 +7,7 @@ defmodule Explorer.Chain.Block do use Explorer.Schema - alias Explorer.Chain.{Address, Gas, Hash, PendingBlockOperation, Transaction, Wei} + alias Explorer.Chain.{Address, Gas, Hash, PendingBlockOperation, Transaction, Wei, Withdrawal} alias Explorer.Chain.Block.{Reward, SecondDegreeRelation} @optional_attrs ~w(size refetch_needed total_difficulty difficulty base_fee_per_gas)a @@ -100,6 +100,8 @@ defmodule Explorer.Chain.Block do has_many(:rewards, Reward, foreign_key: :block_hash) + has_many(:withdrawals, Withdrawal, foreign_key: :block_hash) + has_one(:pending_operations, PendingBlockOperation, foreign_key: :block_hash) end diff --git a/apps/explorer/lib/explorer/chain/import/runner/withdrawals.ex b/apps/explorer/lib/explorer/chain/import/runner/withdrawals.ex new file mode 100644 index 0000000000..1c84df6141 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/import/runner/withdrawals.ex @@ -0,0 +1,106 @@ +defmodule Explorer.Chain.Import.Runner.Withdrawals do + @moduledoc """ + Bulk imports `t:Explorer.Chain.Withdrawal.t/0`. + """ + + require Ecto.Query + + alias Ecto.{Changeset, Multi, Repo} + alias Explorer.Chain.{Import, Withdrawal} + alias Explorer.Prometheus.Instrumenter + + import Ecto.Query, only: [from: 2] + + @behaviour Import.Runner + + # milliseconds + @timeout 60_000 + + @type imported :: [Withdrawal.t()] + + @impl Import.Runner + def ecto_schema_module, do: Withdrawal + + @impl Import.Runner + def option_key, do: :withdrawals + + @impl Import.Runner + def imported_table_row do + %{ + value_type: "[#{ecto_schema_module()}.t()]", + value_description: "List of `t:#{ecto_schema_module()}.t/0`s" + } + end + + @impl Import.Runner + def run(multi, changes_list, %{timestamps: timestamps} = options) do + insert_options = + options + |> Map.get(option_key(), %{}) + |> Map.take(~w(on_conflict timeout)a) + |> Map.put_new(:timeout, @timeout) + |> Map.put(:timestamps, timestamps) + + Multi.run(multi, :withdrawals, fn repo, _ -> + Instrumenter.block_import_stage_runner( + fn -> insert(repo, changes_list, insert_options) end, + :block_referencing, + :withdrawals, + :withdrawals + ) + end) + end + + @impl Import.Runner + def timeout, do: @timeout + + @spec insert(Repo.t(), [map()], %{ + optional(:on_conflict) => Import.Runner.on_conflict(), + required(:timeout) => timeout, + required(:timestamps) => Import.timestamps() + }) :: + {:ok, [Withdrawal.t()]} + | {:error, [Changeset.t()]} + defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do + on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) + + # Enforce Withdrawal ShareLocks order (see docs: sharelocks.md) + ordered_changes_list = Enum.sort_by(changes_list, & &1.index) + + {:ok, _} = + Import.insert_changes_list( + repo, + ordered_changes_list, + conflict_target: [:index], + on_conflict: on_conflict, + for: Withdrawal, + returning: true, + timeout: timeout, + timestamps: timestamps + ) + end + + defp default_on_conflict do + from( + withdrawal in Withdrawal, + update: [ + set: [ + validator_index: fragment("EXCLUDED.validator_index"), + amount: fragment("EXCLUDED.amount"), + address_hash: fragment("EXCLUDED.address_hash"), + block_hash: fragment("EXCLUDED.block_hash"), + inserted_at: fragment("LEAST(?, EXCLUDED.inserted_at)", withdrawal.inserted_at), + updated_at: fragment("GREATEST(?, EXCLUDED.updated_at)", withdrawal.updated_at) + ] + ], + where: + fragment( + "(EXCLUDED.validator_index, EXCLUDED.amount, EXCLUDED.address_hash, EXCLUDED.block_hash) IS DISTINCT FROM (?, ?, ?, ?)", + withdrawal.validator_index, + withdrawal.amount, + withdrawal.address_hash, + withdrawal.block_hash + ) + ) + end +end diff --git a/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex b/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex index 13362daf81..578e4b9894 100644 --- a/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex +++ b/apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex @@ -18,7 +18,8 @@ defmodule Explorer.Chain.Import.Stage.BlockReferencing do Runner.Tokens, Runner.TokenTransfers, Runner.Address.TokenBalances, - Runner.TransactionActions + Runner.TransactionActions, + Runner.Withdrawals ] @impl Stage diff --git a/apps/explorer/lib/explorer/chain/withdrawal.ex b/apps/explorer/lib/explorer/chain/withdrawal.ex new file mode 100644 index 0000000000..9e1df561ea --- /dev/null +++ b/apps/explorer/lib/explorer/chain/withdrawal.ex @@ -0,0 +1,113 @@ +defmodule Explorer.Chain.Withdrawal do + @moduledoc """ + A stored representation of withdrawal introduced in [EIP-4895](https://eips.ethereum.org/EIPS/eip-4895) + """ + + use Explorer.Schema + + alias Explorer.Chain.{Address, Block, Hash, Wei} + alias Explorer.PagingOptions + + @type t :: %__MODULE__{ + index: non_neg_integer(), + validator_index: non_neg_integer(), + amount: Wei.t(), + block: %Ecto.Association.NotLoaded{} | Block.t(), + block_hash: Hash.Full.t(), + address: %Ecto.Association.NotLoaded{} | Address.t(), + address_hash: Hash.Address.t() + } + + @required_attrs ~w(index validator_index amount address_hash block_hash)a + + @primary_key {:index, :integer, autogenerate: false} + schema "withdrawals" do + field(:validator_index, :integer) + field(:amount, Wei) + + belongs_to(:address, Address, + foreign_key: :address_hash, + references: :hash, + type: Hash.Address + ) + + belongs_to(:block, Block, + foreign_key: :block_hash, + references: :hash, + type: Hash.Full + ) + + timestamps() + end + + @spec changeset( + Explorer.Chain.Withdrawal.t(), + :invalid | %{optional(:__struct__) => none, optional(atom | binary) => any} + ) :: Ecto.Changeset.t() + def changeset(%__MODULE__{} = withdrawal, attrs \\ %{}) do + withdrawal + |> cast(attrs, @required_attrs) + |> validate_required(@required_attrs) + |> unique_constraint(:index, name: :withdrawals_pkey) + end + + @spec page_withdrawals(Ecto.Query.t(), PagingOptions.t()) :: Ecto.Query.t() + def page_withdrawals(query, %PagingOptions{key: nil}), do: query + + def page_withdrawals(query, %PagingOptions{key: {index}}) do + where(query, [withdrawal], withdrawal.index < ^index) + end + + @spec block_hash_to_withdrawals_query(Hash.Full.t()) :: Ecto.Query.t() + def block_hash_to_withdrawals_query(block_hash) do + block_hash + |> block_hash_to_withdrawals_unordered_query() + |> order_by(desc: :index) + end + + @spec block_hash_to_withdrawals_unordered_query(Hash.Full.t()) :: Ecto.Query.t() + def block_hash_to_withdrawals_unordered_query(block_hash) do + from(withdrawal in __MODULE__, + select: withdrawal, + where: withdrawal.block_hash == ^block_hash + ) + end + + @spec address_hash_to_withdrawals_query(Hash.Address.t()) :: Ecto.Query.t() + def address_hash_to_withdrawals_query(address_hash) do + address_hash + |> address_hash_to_withdrawals_unordered_query() + |> order_by(desc: :index) + end + + @spec address_hash_to_withdrawals_unordered_query(Hash.Address.t()) :: Ecto.Query.t() + def address_hash_to_withdrawals_unordered_query(address_hash) do + from(withdrawal in __MODULE__, + select: withdrawal, + left_join: block in assoc(withdrawal, :block), + where: withdrawal.address_hash == ^address_hash, + where: block.consensus, + preload: [block: block] + ) + end + + @spec blocks_without_withdrawals_query(non_neg_integer()) :: Ecto.Query.t() + def blocks_without_withdrawals_query(from_block) do + from(withdrawal in __MODULE__, + right_join: block in assoc(withdrawal, :block), + select: block.number, + distinct: block.number, + where: block.number >= ^from_block, + where: block.consensus == ^true, + where: is_nil(withdrawal.index) + ) + end + + @spec list_withdrawals :: Ecto.Query.t() + def list_withdrawals do + from(withdrawal in __MODULE__, + select: withdrawal, + order_by: [desc: :index] + ) + end +end diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index e259f39a98..efbcfe1561 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -2,6 +2,9 @@ defmodule Explorer.Helper do @moduledoc """ Common explorer helper """ + + def parse_integer(nil), do: nil + def parse_integer(string) do case Integer.parse(string) do {number, ""} -> number diff --git a/apps/explorer/priv/repo/migrations/20221223214711_create_withdrawals.exs b/apps/explorer/priv/repo/migrations/20221223214711_create_withdrawals.exs new file mode 100644 index 0000000000..aaf307f081 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20221223214711_create_withdrawals.exs @@ -0,0 +1,19 @@ +defmodule Explorer.Repo.Migrations.CreareWithdrawals do + use Ecto.Migration + + def change do + create table(:withdrawals, primary_key: false) do + add(:index, :integer, null: false, primary_key: true) + add(:validator_index, :integer, null: false) + add(:amount, :numeric, precision: 100, null: false) + + timestamps(null: false, type: :utc_datetime_usec) + + add(:address_hash, references(:addresses, column: :hash, on_delete: :delete_all, type: :bytea), null: false) + add(:block_hash, references(:blocks, column: :hash, on_delete: :delete_all, type: :bytea), null: false) + end + + create(index(:withdrawals, [:address_hash])) + create(index(:withdrawals, [:block_hash])) + end +end diff --git a/apps/explorer/test/explorer/chain/withdrawal_test.exs b/apps/explorer/test/explorer/chain/withdrawal_test.exs new file mode 100644 index 0000000000..ebf88f3499 --- /dev/null +++ b/apps/explorer/test/explorer/chain/withdrawal_test.exs @@ -0,0 +1,82 @@ +defmodule Explorer.Chain.WithdrawalTest do + use Explorer.DataCase + + alias Ecto.Changeset + alias Explorer.Chain.Withdrawal + alias Explorer.Chain + + describe "changeset/2" do + test "with valid attributes" do + assert %Changeset{valid?: true} = + :withdrawal + |> build() + |> Withdrawal.changeset(%{}) + end + + test "with invalid attributes" do + changeset = %Withdrawal{} |> Withdrawal.changeset(%{racecar: "yellow ham"}) + refute(changeset.valid?) + end + + test "with duplicate information" do + %Withdrawal{index: index} = insert(:withdrawal) + + assert {:error, %Changeset{valid?: false, errors: [index: {"has already been taken", _}]}} = + %Withdrawal{} + |> Withdrawal.changeset(params_for(:withdrawal, index: index)) + |> Repo.insert() + end + end + + describe "block_hash_to_withdrawals_query/1" do + test "finds only withdrawals of this block" do + withdrawal_a = insert(:withdrawal) + withdrawal_b = insert(:withdrawal) + + results = + Withdrawal.block_hash_to_withdrawals_query(withdrawal_a.block_hash) + |> Repo.all() + |> Enum.map(& &1.index) + + refute Enum.member?(results, withdrawal_b.index) + assert Enum.member?(results, withdrawal_a.index) + end + + test "order the results DESC by index" do + block = insert(:block, withdrawals: insert_list(50, :withdrawal)) + + results = + Withdrawal.block_hash_to_withdrawals_query(block.hash) + |> Repo.all() + |> Enum.map(& &1.index) + + assert results |> Enum.sort(:desc) == results + end + end + + describe "address_hash_to_withdrawals_query/1" do + test "finds only withdrawals of this address" do + withdrawal_a = insert(:withdrawal) + withdrawal_b = insert(:withdrawal) + + results = + Withdrawal.address_hash_to_withdrawals_query(withdrawal_a.address_hash) + |> Repo.all() + |> Enum.map(& &1.index) + + refute Enum.member?(results, withdrawal_b.index) + assert Enum.member?(results, withdrawal_a.index) + end + + test "order the results DESC by index" do + address = insert(:address, withdrawals: insert_list(50, :withdrawal)) + + results = + Withdrawal.address_hash_to_withdrawals_query(address.hash) + |> Repo.all() + |> Enum.map(& &1.index) + + assert results |> Enum.sort(:desc) == results + end + end +end diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 06c3d0fe84..89799b759e 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -39,7 +39,8 @@ defmodule Explorer.Factory do Token, TokenTransfer, Token.Instance, - Transaction + Transaction, + Withdrawal } alias Explorer.SmartContract.Helper @@ -947,5 +948,28 @@ defmodule Explorer.Factory do } end + def withdrawal_factory do + block = build(:block) + address = build(:address) + + %Withdrawal{ + index: withdrawal_index(), + validator_index: withdrawal_validator_index(), + amount: Enum.random(1..100_000), + block: block, + block_hash: block.hash, + address: address, + address_hash: address.hash + } + end + + def withdrawal_index do + sequence("withdrawal_index", & &1) + end + + def withdrawal_validator_index do + sequence("withdrawal_validator_index", & &1) + end + def random_bool, do: Enum.random([true, false]) end diff --git a/apps/indexer/README.md b/apps/indexer/README.md index f7c0217d16..60d286cdf5 100644 --- a/apps/indexer/README.md +++ b/apps/indexer/README.md @@ -31,6 +31,7 @@ Some data has to be extracted from already fetched data, and there're several tr - `block/realtime`: listens for new blocks from websocket and polls node for new blocks, imports new ones one by one - `block/catchup`: gets unfetched ranges of blocks, imports them in batches - `transaction_action`: optionally fetches/rewrites transaction actions for old blocks (in a given range of blocks for given protocols) +- `withdrawals`: optionally fetches withdrawals for old blocks (in the given from boundary of block numbers) Both block fetchers retrieve/extract the blocks themselves and the following additional data: @@ -40,6 +41,7 @@ Both block fetchers retrieve/extract the blocks themselves and the following add - `token_transfers` - `transaction_actions` - `addresses` +- `withdrawals` The following stubs for further async fetching are inserted as well: diff --git a/apps/indexer/config/dev.exs b/apps/indexer/config/dev.exs index f7f9b205d9..28f928baa1 100644 --- a/apps/indexer/config/dev.exs +++ b/apps/indexer/config/dev.exs @@ -35,3 +35,8 @@ config :logger, :block_import_timings, level: :debug, path: Path.absname("logs/dev/indexer/block_import_timings.log"), metadata_filter: [fetcher: :block_import_timings] + +config :logger, :withdrawal, + level: :debug, + path: Path.absname("logs/dev/indexer/withdrawal.log"), + metadata_filter: [fetcher: :withdrawal] diff --git a/apps/indexer/config/prod.exs b/apps/indexer/config/prod.exs index 7c92ca5853..8e81a078f4 100644 --- a/apps/indexer/config/prod.exs +++ b/apps/indexer/config/prod.exs @@ -42,3 +42,9 @@ config :logger, :block_import_timings, path: Path.absname("logs/prod/indexer/block_import_timings.log"), metadata_filter: [fetcher: :block_import_timings], rotate: %{max_bytes: 52_428_800, keep: 19} + +config :logger, :withdrawal, + level: :info, + path: Path.absname("logs/prod/indexer/withdrawal.log"), + metadata_filter: [fetcher: :withdrawal], + rotate: %{max_bytes: 52_428_800, keep: 19} diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index 77b4056b76..217cf6e13f 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -130,6 +130,7 @@ defmodule Indexer.Block.Fetcher do %Blocks{ blocks_params: blocks_params, transactions_params: transactions_params_without_receipts, + withdrawals_params: withdrawals_params, block_second_degree_relations_params: block_second_degree_relations_params, errors: blocks_errors }}} <- {:blocks, fetched_blocks}, @@ -150,14 +151,16 @@ defmodule Indexer.Block.Fetcher do mint_transfers: mint_transfers, token_transfers: token_transfers, transactions: transactions_with_receipts, - transaction_actions: transaction_actions + transaction_actions: transaction_actions, + withdrawals: withdrawals_params }), coin_balances_params_set = %{ beneficiary_params: MapSet.to_list(beneficiary_params_set), blocks_params: blocks, logs_params: logs, - transactions_params: transactions_with_receipts + transactions_params: transactions_with_receipts, + withdrawals: withdrawals_params } |> AddressCoinBalances.params_set(), coin_balances_params_daily_set = @@ -186,7 +189,8 @@ defmodule Indexer.Block.Fetcher do token_transfers: %{params: token_transfers}, tokens: %{on_conflict: :nothing, params: tokens}, transactions: %{params: transactions_with_receipts}, - transaction_actions: %{params: transaction_actions} + transaction_actions: %{params: transaction_actions}, + withdrawals: %{params: withdrawals_params} } ) do Prometheus.Instrumenter.block_batch_fetch(fetch_time, callback_module) diff --git a/apps/indexer/lib/indexer/fetcher/withdrawal.ex b/apps/indexer/lib/indexer/fetcher/withdrawal.ex new file mode 100644 index 0000000000..eb700cd2ba --- /dev/null +++ b/apps/indexer/lib/indexer/fetcher/withdrawal.ex @@ -0,0 +1,159 @@ +defmodule Indexer.Fetcher.Withdrawal do + @moduledoc """ + Reindexes withdrawals from blocks that were indexed before app update. + """ + + use GenServer + use Indexer.Fetcher + + require Logger + + alias EthereumJSONRPC.Blocks + alias Explorer.{Chain, Repo} + alias Explorer.Chain.Withdrawal + alias Explorer.Helper + alias Indexer.Transform.Addresses + + @interval :timer.seconds(10) + @batch_size 10 + @concurrency 5 + + defstruct blocks_to_fetch: [], + interval: @interval, + json_rpc_named_arguments: [], + max_batch_size: @batch_size, + max_concurrency: @concurrency + + def child_spec([init_arguments]) do + child_spec([init_arguments, []]) + end + + def child_spec([_init_arguments, _gen_server_options] = start_link_arguments) do + default = %{ + id: __MODULE__, + start: {__MODULE__, :start_link, start_link_arguments} + } + + Supervisor.child_spec(default, restart: :transient) + end + + def start_link(arguments, gen_server_options \\ []) do + GenServer.start_link(__MODULE__, arguments, gen_server_options) + end + + @impl GenServer + def init(opts) when is_list(opts) do + Logger.metadata(fetcher: :withdrawal) + first_block = Application.get_env(:indexer, __MODULE__)[:first_block] + + if first_block |> Helper.parse_integer() |> is_integer() do + # withdrawals from all other blocks will be imported by realtime and catchup indexers + json_rpc_named_arguments = opts[:json_rpc_named_arguments] + + unless json_rpc_named_arguments do + raise ArgumentError, + ":json_rpc_named_arguments must be provided to `#{__MODULE__}.init to allow for json_rpc calls when running." + end + + state = %__MODULE__{ + blocks_to_fetch: first_block |> Helper.parse_integer() |> missing_block_numbers(), + interval: opts[:interval] || @interval, + json_rpc_named_arguments: json_rpc_named_arguments, + max_batch_size: opts[:max_batch_size] || @batch_size, + max_concurrency: opts[:max_concurrency] || @concurrency + } + + Process.send_after(self(), :fetch_withdrawals, state.interval) + + {:ok, state} + else + Logger.warn("Please, specify the first block of the block range for #{__MODULE__}.") + :ignore + end + end + + @impl GenServer + def handle_info( + :fetch_withdrawals, + %__MODULE__{ + blocks_to_fetch: blocks_to_fetch, + interval: interval, + json_rpc_named_arguments: json_rpc_named_arguments, + max_batch_size: batch_size, + max_concurrency: concurrency + } = state + ) do + Logger.metadata(fetcher: :withdrawal) + + if Enum.empty?(blocks_to_fetch) do + Logger.info("Withdrawals from old blocks are fetched.") + {:stop, :normal, state} + else + new_blocks_to_fetch = + blocks_to_fetch + |> Stream.chunk_every(batch_size) + |> Task.async_stream( + &{EthereumJSONRPC.fetch_blocks_by_numbers(&1, json_rpc_named_arguments), &1}, + max_concurrency: concurrency, + timeout: :infinity, + zip_input_on_exit: true + ) + |> Enum.reduce([], &fetch_reducer/2) + + Process.send_after(self(), :fetch_withdrawals, interval) + + {:noreply, %__MODULE__{state | blocks_to_fetch: new_blocks_to_fetch}} + end + end + + def handle_info({ref, _result}, state) do + Process.demonitor(ref, [:flush]) + {:noreply, state} + end + + def handle_info( + {:DOWN, _ref, :process, _pid, reason}, + state + ) do + if reason === :normal do + {:noreply, state} + else + Logger.metadata(fetcher: :withdrawal) + Logger.error(fn -> "Withdrawals fetcher task exited due to #{inspect(reason)}." end) + {:noreply, state} + end + end + + defp fetch_reducer({:ok, {{:ok, %Blocks{withdrawals_params: withdrawals_params}}, block_numbers}}, acc) do + addresses = Addresses.extract_addresses(%{withdrawals: withdrawals_params}) + + case Chain.import(%{addresses: %{params: addresses}, withdrawals: %{params: withdrawals_params}}) do + {:ok, _} -> + acc + + {:error, reason} -> + Logger.error(inspect(reason) <> ". Retrying.") + [block_numbers | acc] |> List.flatten() + + {:error, step, failed_value, _changes_so_far} -> + Logger.error("failed to insert: " <> inspect(failed_value) <> ". Retrying.", step: step) + [block_numbers | acc] |> List.flatten() + end + end + + defp fetch_reducer({:ok, {{:error, reason}, block_numbers}}, acc) do + Logger.error("failed to fetch: " <> inspect(reason) <> ". Retrying.") + [block_numbers | acc] |> List.flatten() + end + + defp fetch_reducer({:exit, {block_numbers, reason}}, acc) do + Logger.error("failed to fetch: " <> inspect(reason) <> ". Retrying.") + [block_numbers | acc] |> List.flatten() + end + + defp missing_block_numbers(from) do + blocks = from |> Withdrawal.blocks_without_withdrawals_query() |> Repo.all() + Logger.debug("missing_block_numbers #{length(blocks)}") + blocks + end +end diff --git a/apps/indexer/lib/indexer/supervisor.ex b/apps/indexer/lib/indexer/supervisor.ex index b8d6fbcaa7..38b2e39f6d 100644 --- a/apps/indexer/lib/indexer/supervisor.ex +++ b/apps/indexer/lib/indexer/supervisor.ex @@ -30,7 +30,8 @@ defmodule Indexer.Supervisor do TokenBalance, TokenUpdater, TransactionAction, - UncleBlock + UncleBlock, + Withdrawal } alias Indexer.Temporary.{ @@ -143,7 +144,8 @@ defmodule Indexer.Supervisor do [ %{block_fetcher: block_fetcher, block_interval: block_interval, memory_monitor: memory_monitor}, [name: BlockCatchup.Supervisor] - ]} + ]}, + {Withdrawal.Supervisor, [[json_rpc_named_arguments: json_rpc_named_arguments]]} ] |> List.flatten() diff --git a/apps/indexer/lib/indexer/transform/address_coin_balances.ex b/apps/indexer/lib/indexer/transform/address_coin_balances.ex index cd71508bc2..03319b0dec 100644 --- a/apps/indexer/lib/indexer/transform/address_coin_balances.ex +++ b/apps/indexer/lib/indexer/transform/address_coin_balances.ex @@ -50,6 +50,13 @@ defmodule Indexer.Transform.AddressCoinBalances do when is_list(block_second_degree_relations_params), do: initial + defp reducer({:withdrawals, withdrawals}, acc) when is_list(withdrawals) do + Enum.into(withdrawals, acc, fn %{address_hash: address_hash, block_number: block_number} + when is_binary(address_hash) and is_integer(block_number) -> + %{address_hash: address_hash, block_number: block_number} + end) + end + defp internal_transactions_params_reducer(%{block_number: block_number} = internal_transaction_params, acc) when is_integer(block_number) do case internal_transaction_params do diff --git a/apps/indexer/lib/indexer/transform/addresses.ex b/apps/indexer/lib/indexer/transform/addresses.ex index acedc9c126..5787cc62bb 100644 --- a/apps/indexer/lib/indexer/transform/addresses.ex +++ b/apps/indexer/lib/indexer/transform/addresses.ex @@ -133,6 +133,12 @@ defmodule Indexer.Transform.Addresses do %{from: :block_number, to: :fetched_coin_balance_block_number}, %{from: :address_hash, to: :hash} ] + ], + withdrawals: [ + [ + %{from: :block_number, to: :fetched_coin_balance_block_number}, + %{from: :address_hash, to: :hash} + ] ] } @@ -427,6 +433,12 @@ defmodule Indexer.Transform.Addresses do required(:address_hash) => String.t(), required(:block_number) => non_neg_integer() } + ], + optional(:withdrawals) => [ + %{ + required(:address_hash) => String.t(), + required(:block_number) => non_neg_integer() + } ] }) :: [params] def extract_addresses(fetched_data, options \\ []) when is_map(fetched_data) and is_list(options) do diff --git a/apps/indexer/test/indexer/fetcher/withdrawal_test.exs b/apps/indexer/test/indexer/fetcher/withdrawal_test.exs new file mode 100644 index 0000000000..cfe60e9e4a --- /dev/null +++ b/apps/indexer/test/indexer/fetcher/withdrawal_test.exs @@ -0,0 +1,152 @@ +defmodule Indexer.Fetcher.WithdrawalTest do + use EthereumJSONRPC.Case + use Explorer.DataCase + + import Mox + import EthereumJSONRPC, only: [integer_to_quantity: 1] + + alias Explorer.Chain + alias Indexer.Fetcher.Withdrawal + + setup :verify_on_exit! + setup :set_mox_global + + setup do + initial_env = Application.get_all_env(:indexer) + on_exit(fn -> Application.put_all_env([{:indexer, initial_env}]) end) + end + + test "do not crash app when WITHDRAWALS_FIRST_BLOCK is undefined", %{ + json_rpc_named_arguments: json_rpc_named_arguments + } do + Application.put_env(:indexer, Withdrawal.Supervisor, disabled?: "false") + Withdrawal.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + assert [{Indexer.Fetcher.Withdrawal, :undefined, :worker, [Indexer.Fetcher.Withdrawal]} | _] = + Withdrawal.Supervisor |> Supervisor.which_children() + end + + test "do not start when all old blocks are fetched", %{json_rpc_named_arguments: json_rpc_named_arguments} do + Application.put_env(:indexer, Withdrawal.Supervisor, disabled?: "false") + Withdrawal.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + Application.put_env(:indexer, Withdrawal, first_block: "0") + + assert [{Indexer.Fetcher.Withdrawal, :undefined, :worker, [Indexer.Fetcher.Withdrawal]} | _] = + Withdrawal.Supervisor |> Supervisor.which_children() + end + + test "stops when all old blocks are fetched", %{json_rpc_named_arguments: json_rpc_named_arguments} do + Application.put_env(:indexer, Withdrawal.Supervisor, disabled?: "false") + Application.put_env(:indexer, Withdrawal, first_block: "0") + + block_a = insert(:block) + block_b = insert(:block) + + block_a_number_string = integer_to_quantity(block_a.number) + block_b_number_string = integer_to_quantity(block_b.number) + + EthereumJSONRPC.Mox + |> expect(:json_rpc, 2, fn requests, _options -> + {:ok, + Enum.map(requests, fn + %{id: id, method: "eth_getBlockByNumber", params: [^block_a_number_string, true]} -> + %{ + id: id, + result: %{ + "author" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "difficulty" => "0x6bc767dd80781", + "extraData" => "0x5050594520737061726b706f6f6c2d6574682d7477", + "gasLimit" => "0x7a121d", + "gasUsed" => "0x79cbe9", + "hash" => to_string(block_a.hash), + "logsBloom" => + "0x044d42d008801488400e1809190200a80d06105bc0c4100b047895c0d518327048496108388040140010b8208006288102e206160e21052322440924002090c1c808a0817405ab238086d028211014058e949401012403210314896702d06880c815c3060a0f0809987c81044488292cc11d57882c912a808ca10471c84460460040000c0001012804022000a42106591881d34407420ba401e1c08a8d00a000a34c11821a80222818a4102152c8a0c044032080c6462644223104d618e0e544072008120104408205c60510542264808488220403000106281a0290404220112c10b080145028c8000300b18a2c8280701c882e702210b00410834840108084", + "miner" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "mixHash" => "0xda53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "nonce" => "0x0946e5f01fce12bc", + "number" => "0x708677", + "parentHash" => "0x62543e836e0ef7edfa9e38f26526092c4be97efdf5ba9e0f53a4b0b7d5bc930a", + "receiptsRoot" => "0xa7d2b82bd8526de11736c18bd5cc8cfe2692106c4364526f3310ad56d78669c4", + "sealFields" => [ + "0xa0da53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "0x880946e5f01fce12bc" + ], + "sha3Uncles" => "0x483a8a21a5825ad270f358b3ea56e060bbb8b3082d9a92ec8fa17a5c7e6fc1b6", + "size" => "0x544c", + "stateRoot" => "0x85daa9cd528004c1609d4cb3520fd958e85983bb4183124a4a9f7137fd39c691", + "timestamp" => "0x5c8bc76e", + "totalDifficulty" => "0x201a42c35142ae94458", + "transactions" => [], + "transactionsRoot" => "0xcd6c12fa43cd4e92ad5c0bf232b30488bbcbfe273c5b4af0366fced0767d54db", + "uncles" => [], + "withdrawals" => [ + %{ + "index" => "0x1", + "validatorIndex" => "0x80b", + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => "0x2c17a12dc00" + } + ] + } + } + + %{id: id, method: "eth_getBlockByNumber", params: [^block_b_number_string, true]} -> + %{ + id: id, + result: %{ + "author" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "difficulty" => "0x6bc767dd80781", + "extraData" => "0x5050594520737061726b706f6f6c2d6574682d7477", + "gasLimit" => "0x7a121d", + "gasUsed" => "0x79cbe9", + "hash" => to_string(block_b.hash), + "logsBloom" => + "0x044d42d008801488400e1809190200a80d06105bc0c4100b047895c0d518327048496108388040140010b8208006288102e206160e21052322440924002090c1c808a0817405ab238086d028211014058e949401012403210314896702d06880c815c3060a0f0809987c81044488292cc11d57882c912a808ca10471c84460460040000c0001012804022000a42106591881d34407420ba401e1c08a8d00a000a34c11821a80222818a4102152c8a0c044032080c6462644223104d618e0e544072008120104408205c60510542264808488220403000106281a0290404220112c10b080145028c8000300b18a2c8280701c882e702210b00410834840108084", + "miner" => "0x5a0b54d5dc17e0aadc383d2db43b0a0d3e029c4c", + "mixHash" => "0xda53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "nonce" => "0x0946e5f01fce12bc", + "number" => "0x708677", + "parentHash" => "0x62543e836e0ef7edfa9e38f26526092c4be97efdf5ba9e0f53a4b0b7d5bc930a", + "receiptsRoot" => "0xa7d2b82bd8526de11736c18bd5cc8cfe2692106c4364526f3310ad56d78669c4", + "sealFields" => [ + "0xa0da53ae7c2b3c529783d6cdacdb90587fd70eb651c0f04253e8ff17de97844010", + "0x880946e5f01fce12bc" + ], + "sha3Uncles" => "0x483a8a21a5825ad270f358b3ea56e060bbb8b3082d9a92ec8fa17a5c7e6fc1b6", + "size" => "0x544c", + "stateRoot" => "0x85daa9cd528004c1609d4cb3520fd958e85983bb4183124a4a9f7137fd39c691", + "timestamp" => "0x5c8bc76e", + "totalDifficulty" => "0x201a42c35142ae94458", + "transactions" => [], + "transactionsRoot" => "0xcd6c12fa43cd4e92ad5c0bf232b30488bbcbfe273c5b4af0366fced0767d54db", + "uncles" => [], + "withdrawals" => [ + %{ + "index" => "0x2", + "validatorIndex" => "0x80b", + "address" => "0x388ea662ef2c223ec0b047d41bf3c0f362142ad5", + "amount" => "0x2c17a12dc00" + } + ] + } + } + end)} + end) + + pid = Withdrawal.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments) + + assert [{Indexer.Fetcher.Withdrawal, worker_pid, :worker, [Indexer.Fetcher.Withdrawal]} | _] = + Withdrawal.Supervisor |> Supervisor.which_children() + + assert is_pid(worker_pid) + + :timer.sleep(300) + + assert [{Indexer.Fetcher.Withdrawal, :undefined, :worker, [Indexer.Fetcher.Withdrawal]} | _] = + Withdrawal.Supervisor |> Supervisor.which_children() + + # Terminates the process so it finishes all Ecto processes. + GenServer.stop(pid) + end +end diff --git a/apps/indexer/test/support/indexer/fetcher/withdrawal_supervisor_case.ex b/apps/indexer/test/support/indexer/fetcher/withdrawal_supervisor_case.ex new file mode 100644 index 0000000000..2f419efcc8 --- /dev/null +++ b/apps/indexer/test/support/indexer/fetcher/withdrawal_supervisor_case.ex @@ -0,0 +1,17 @@ +defmodule Indexer.Fetcher.Withdrawal.Supervisor.Case do + alias Indexer.Fetcher.Withdrawal + + def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do + merged_fetcher_arguments = + Keyword.merge( + fetcher_arguments, + interval: 1, + max_batch_size: 1, + max_concurrency: 1 + ) + + [merged_fetcher_arguments] + |> Withdrawal.Supervisor.child_spec() + |> ExUnit.Callbacks.start_supervised!() + end +end diff --git a/config/config.exs b/config/config.exs index 66c40f086e..554d3ae557 100644 --- a/config/config.exs +++ b/config/config.exs @@ -34,6 +34,7 @@ config :logger, {LoggerFileBackend, :reading_token_functions}, {LoggerFileBackend, :pending_transactions_to_refetch}, {LoggerFileBackend, :empty_blocks_to_refetch}, + {LoggerFileBackend, :withdrawal}, {LoggerFileBackend, :api}, {LoggerFileBackend, :block_import_timings}, {LoggerFileBackend, :account}, diff --git a/config/runtime.exs b/config/runtime.exs index b312d011b6..7e6c891cd3 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -495,6 +495,11 @@ config :indexer, Indexer.Fetcher.CoinBalance, batch_size: ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_BATCH_SIZE", 500), concurrency: ConfigHelper.parse_integer_env_var("INDEXER_COIN_BALANCES_CONCURRENCY", 4) +config :indexer, Indexer.Fetcher.Withdrawal.Supervisor, + disabled?: System.get_env("INDEXER_DISABLE_WITHDRAWALS_FETCHER", "true") == "true" + +config :indexer, Indexer.Fetcher.Withdrawal, first_block: System.get_env("WITHDRAWALS_FIRST_BLOCK") + Code.require_file("#{config_env()}.exs", "config/runtime") for config <- "../apps/*/config/runtime/#{config_env()}.exs" |> Path.expand(__DIR__) |> Path.wildcard() do diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index f7ab96a631..0f69031e74 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -117,6 +117,8 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false # INDEXER_TX_ACTIONS_REINDEX_PROTOCOLS= # INDEXER_TX_ACTIONS_AAVE_V3_POOL_CONTRACT= # INDEXER_REALTIME_FETCHER_MAX_GAP= +# INDEXER_DISABLE_WITHDRAWALS_FETCHER= +# WITHDRAWALS_FIRST_BLOCK= # TOKEN_ID_MIGRATION_FIRST_BLOCK= # TOKEN_ID_MIGRATION_CONCURRENCY= # TOKEN_ID_MIGRATION_BATCH_SIZE= diff --git a/docker/Makefile b/docker/Makefile index 843cc65658..495e4dcef9 100644 --- a/docker/Makefile +++ b/docker/Makefile @@ -554,6 +554,11 @@ ifdef INDEXER_REALTIME_FETCHER_MAX_GAP endif ifdef INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE=$(INDEXER_INTERNAL_TRANSACTIONS_TRACER_TYPE)' +ifdef INDEXER_DISABLE_WITHDRAWALS_FETCHER + BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_DISABLE_WITHDRAWALS_FETCHER=$(INDEXER_DISABLE_WITHDRAWALS_FETCHER)' +endif +ifdef WITHDRAWALS_FIRST_BLOCK + BLOCKSCOUT_CONTAINER_PARAMS += -e 'WITHDRAWALS_FIRST_BLOCK=$(WITHDRAWALS_FIRST_BLOCK)' endif ifdef TOKEN_ID_MIGRATION_FIRST_BLOCK BLOCKSCOUT_CONTAINER_PARAMS += -e 'TOKEN_ID_MIGRATION_FIRST_BLOCK=$(TOKEN_ID_MIGRATION_FIRST_BLOCK)'