diff --git a/.dialyzer-ignore b/.dialyzer-ignore index bf2a526f58..4e3f681c19 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -30,3 +30,4 @@ lib/explorer/smart_contract/verifier.ex:89 lib/block_scout_web/templates/address_contract/index.html.eex:117 lib/explorer/staking/stake_snapshotting.ex:14: Function do_snapshotting/6 has no local return lib/explorer/staking/stake_snapshotting.ex:179 +lib/explorer/counters/address_gas_usage_counter.ex:69 diff --git a/CHANGELOG.md b/CHANGELOG.md index eadf45322d..e6b5948b67 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,7 @@ ## Current ### Features +- [#3384](https://github.com/poanetwork/blockscout/pull/3384) - Address total gas usage - [#3377](https://github.com/poanetwork/blockscout/pull/3377) - Add links to contract libraries - [#2292](https://github.com/poanetwork/blockscout/pull/2292), [#3356](https://github.com/poanetwork/blockscout/pull/3356), [#3359](https://github.com/poanetwork/blockscout/pull/3359), [#3360](https://github.com/poanetwork/blockscout/pull/3360), [#3365](https://github.com/poanetwork/blockscout/pull/3365) - Add Web UI for POSDAO Staking DApp - [#3354](https://github.com/poanetwork/blockscout/pull/3354) - Tx hash in EOA coin balance history diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index 2ed9ab2090..b8b0291232 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -21,6 +21,7 @@ export const initialState = { balanceCard: null, fetchedCoinBalanceBlockNumber: null, transactionCount: null, + gasUsageCount: null, validationCount: null, countersFetched: false } @@ -41,6 +42,7 @@ export function reducer (state = initialState, action) { case 'COUNTERS_FETCHED': { return Object.assign({}, state, { transactionCount: action.transactionCount, + gasUsageCount: action.gasUsageCount, validationCount: action.validationCount, countersFetched: true }) @@ -111,6 +113,22 @@ const elements = { } } }, + '[data-selector="gas-usage-count"]': { + load ($el) { + return { gasUsageCount: numeral($el.text()).value() } + }, + render ($el, state, oldState) { + if (state.countersFetched && state.gasUsageCount) { + if (oldState.gasUsageCount === state.gasUsageCount) return + $el.empty().append(numeral(state.gasUsageCount).format() + ' Gas used') + $el.show() + $el.parent('.address-detail-item').removeAttr('style') + } else { + $el.hide() + $el.parent('.address-detail-item').css('display', 'none') + } + } + }, '[data-selector="fetched-coin-balance-block-number"]': { load ($el) { return { fetchedCoinBalanceBlockNumber: numeral($el.text()).value() } diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index b7ce5c8506..02afa62adb 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.AddressController do import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] alias BlockScoutWeb.{AccessHelpers, AddressView} - alias Explorer.Counters.AddressTransactionsCounter + alias Explorer.Counters.{AddressTransactionsCounter, AddressTransactionsGasUsageCounter} alias Explorer.{Chain, Market} alias Explorer.ExchangeRates.Token alias Phoenix.View @@ -84,9 +84,13 @@ defmodule BlockScoutWeb.AddressController do def address_counters(conn, %{"id" => address_hash_string}) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.hash_to_address(address_hash) do - {transaction_count, validation_count} = transaction_and_validation_count(address) + {transaction_count, gas_usage_count, validation_count} = transaction_and_validation_count(address) - json(conn, %{transaction_count: transaction_count, validation_count: validation_count}) + json(conn, %{ + transaction_count: transaction_count, + gas_usage_count: gas_usage_count, + validation_count: validation_count + }) else _ -> not_found(conn) end @@ -98,12 +102,17 @@ defmodule BlockScoutWeb.AddressController do transaction_count(address) end) + gas_usage_count_task = + Task.async(fn -> + gas_usage_count(address) + end) + validation_count_task = Task.async(fn -> validation_count(address) end) - [transaction_count_task, validation_count_task] + [transaction_count_task, gas_usage_count_task, validation_count_task] |> Task.yield_many(:timer.seconds(60)) |> Enum.map(fn {_task, res} -> case res do @@ -124,6 +133,10 @@ defmodule BlockScoutWeb.AddressController do AddressTransactionsCounter.fetch(address) end + def gas_usage_count(address) do + AddressTransactionsGasUsageCounter.fetch(address) + end + defp validation_count(address) do Chain.address_to_validation_count(address.hash) end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index bf49e576f2..c82671ec59 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -102,6 +102,10 @@ + <%= if @address.fetched_coin_balance_block_number do %> <%= gettext("Last Balance Update: Block #") %><%= @address.fetched_coin_balance_block_number %> diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index f7b1ea11aa..58697dbcfa 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -463,7 +463,7 @@ msgid "Create2" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:122 +#: lib/block_scout_web/templates/address/overview.html.eex:126 msgid "Created by" msgstr "" @@ -625,7 +625,7 @@ msgid "Error: (Awaiting internal transactions for reason)" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:135 +#: lib/block_scout_web/templates/address/overview.html.eex:139 msgid "Error: Could not determine contract creator." msgstr "" @@ -960,7 +960,7 @@ msgid "It could still be in the TX Pool of a different node, waiting to be broad msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:107 +#: lib/block_scout_web/templates/address/overview.html.eex:111 msgid "Last Balance Update: Block #" msgstr "" @@ -1173,7 +1173,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/overview.html.eex:59 -#: lib/block_scout_web/templates/address/overview.html.eex:159 +#: lib/block_scout_web/templates/address/overview.html.eex:163 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:51 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:101 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:36 @@ -1658,7 +1658,7 @@ msgid "Yes" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:127 +#: lib/block_scout_web/templates/address/overview.html.eex:131 msgid "at" msgstr "" @@ -1716,8 +1716,8 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 -#: lib/block_scout_web/templates/address/overview.html.eex:160 -#: lib/block_scout_web/templates/address/overview.html.eex:168 +#: lib/block_scout_web/templates/address/overview.html.eex:164 +#: lib/block_scout_web/templates/address/overview.html.eex:172 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:102 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:110 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:122 diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index f7b1ea11aa..58697dbcfa 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -463,7 +463,7 @@ msgid "Create2" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:122 +#: lib/block_scout_web/templates/address/overview.html.eex:126 msgid "Created by" msgstr "" @@ -625,7 +625,7 @@ msgid "Error: (Awaiting internal transactions for reason)" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:135 +#: lib/block_scout_web/templates/address/overview.html.eex:139 msgid "Error: Could not determine contract creator." msgstr "" @@ -960,7 +960,7 @@ msgid "It could still be in the TX Pool of a different node, waiting to be broad msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:107 +#: lib/block_scout_web/templates/address/overview.html.eex:111 msgid "Last Balance Update: Block #" msgstr "" @@ -1173,7 +1173,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/overview.html.eex:59 -#: lib/block_scout_web/templates/address/overview.html.eex:159 +#: lib/block_scout_web/templates/address/overview.html.eex:163 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:51 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:101 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:36 @@ -1658,7 +1658,7 @@ msgid "Yes" msgstr "" #, elixir-format -#: lib/block_scout_web/templates/address/overview.html.eex:127 +#: lib/block_scout_web/templates/address/overview.html.eex:131 msgid "at" msgstr "" @@ -1716,8 +1716,8 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37 -#: lib/block_scout_web/templates/address/overview.html.eex:160 -#: lib/block_scout_web/templates/address/overview.html.eex:168 +#: lib/block_scout_web/templates/address/overview.html.eex:164 +#: lib/block_scout_web/templates/address/overview.html.eex:172 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:102 #: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:110 #: lib/block_scout_web/templates/tokens/overview/_details.html.eex:122 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs index cf0126937d..b3c63dc9e8 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.AddressControllerTest do import Mox alias Explorer.Chain.Address - alias Explorer.Counters.{AddressesCounter, AddressTransactionsCounter} + alias Explorer.Counters.{AddressesCounter} describe "GET index/2" do setup :set_mox_global @@ -85,7 +85,7 @@ defmodule BlockScoutWeb.AddressControllerTest do assert conn.status == 200 {:ok, response} = Jason.decode(conn.resp_body) - assert %{"transaction_count" => 0, "validation_count" => 0} == response + assert %{"transaction_count" => 0, "validation_count" => 0, "gas_usage_count" => 0} == response end end end diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs index 96757c8999..2a845fe249 100644 --- a/apps/explorer/config/config.exs +++ b/apps/explorer/config/config.exs @@ -94,6 +94,17 @@ address_transactions_counter_cache_period = _ -> :timer.hours(1) end +config :explorer, Explorer.Counters.AddressTransactionsGasUsageCounter, + enabled: true, + enable_consolidation: true, + update_interval_in_seconds: balances_update_interval || 30 * 60 + +address_transactions_gas_usage_counter_cache_period = + case Integer.parse(System.get_env("ADDRESS_TRANSACTIONS_GAS_USAGE_COUNTER_CACHE_PERIOD", "")) do + {secs, ""} -> :timer.seconds(secs) + _ -> :timer.hours(1) + end + config :explorer, Explorer.Counters.AddressTransactionsCounter, enabled: true, enable_consolidation: true, diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index f6d58af84b..b4b77d2f04 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -79,6 +79,7 @@ defmodule Explorer.Application do configure(Explorer.Counters.AddressesWithBalanceCounter), configure(Explorer.Counters.AddressesCounter), configure(Explorer.Counters.AddressTransactionsCounter), + configure(Explorer.Counters.AddressTransactionsGasUsageCounter), configure(Explorer.Counters.AverageBlockTime), configure(Explorer.Counters.Bridge), configure(Explorer.Validator.MetadataProcessor), diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 9e0f5aa441..acff41c861 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -724,6 +724,28 @@ defmodule Explorer.Chain do Repo.aggregate(to_address_query, :count, :hash, timeout: :infinity) end + @spec address_to_incoming_transaction_gas_usage(Hash.Address.t()) :: non_neg_integer() + def address_to_incoming_transaction_gas_usage(address_hash) do + to_address_query = + from( + transaction in Transaction, + where: transaction.to_address_hash == ^address_hash + ) + + Repo.aggregate(to_address_query, :sum, :gas_used, timeout: :infinity) + end + + @spec address_to_outcoming_transaction_gas_usage(Hash.Address.t()) :: non_neg_integer() + def address_to_outcoming_transaction_gas_usage(address_hash) do + to_address_query = + from( + transaction in Transaction, + where: transaction.from_address_hash == ^address_hash + ) + + Repo.aggregate(to_address_query, :sum, :gas_used, timeout: :infinity) + end + @spec max_incoming_transactions_count() :: non_neg_integer() def max_incoming_transactions_count, do: @max_incoming_transactions_count @@ -1963,6 +1985,21 @@ defmodule Explorer.Chain do end end + @spec address_to_gas_usage_count(Address.t()) :: non_neg_integer() + def address_to_gas_usage_count(address) do + if contract?(address) do + incoming_transaction_gas_usage = address_to_incoming_transaction_gas_usage(address.hash) + + if incoming_transaction_gas_usage == 0 do + address_to_outcoming_transaction_gas_usage(address.hash) + else + incoming_transaction_gas_usage + end + else + address_to_outcoming_transaction_gas_usage(address.hash) + end + end + defp contract?(%{contract_code: nil}), do: false defp contract?(%{contract_code: _}), do: true diff --git a/apps/explorer/lib/explorer/counters/address_gas_usage_counter.ex b/apps/explorer/lib/explorer/counters/address_gas_usage_counter.ex new file mode 100644 index 0000000000..c5384fb9ad --- /dev/null +++ b/apps/explorer/lib/explorer/counters/address_gas_usage_counter.ex @@ -0,0 +1,112 @@ +defmodule Explorer.Counters.AddressTransactionsGasUsageCounter do + @moduledoc """ + Caches Address transactions gas usage counter. + """ + use GenServer + + alias Explorer.Chain + + @cache_name :address_transactions_gas_usage_counter + @last_update_key "last_update" + @cache_period Application.get_env(:explorer, __MODULE__)[:period] + + @ets_opts [ + :set, + :named_table, + :public, + read_concurrency: true + ] + + config = Application.get_env(:explorer, Explorer.Counters.AddressTransactionsGasUsageCounter) + @enable_consolidation Keyword.get(config, :enable_consolidation) + + @spec start_link(term()) :: GenServer.on_start() + def start_link(_) do + GenServer.start_link(__MODULE__, :ok, name: __MODULE__) + end + + @impl true + def init(_args) do + create_cache_table() + + {:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}} + end + + @impl true + def handle_continue(:ok, %{consolidate?: true} = state) do + {:noreply, state} + end + + @impl true + def handle_continue(:ok, state) do + {:noreply, state} + end + + @impl true + def handle_info(:consolidate, state) do + {:noreply, state} + end + + def fetch(address) do + if cache_expired?(address) do + Task.start_link(fn -> + update_cache(address) + end) + end + + address_hash_string = get_address_hash_string(address) + fetch_from_cache("hash_#{address_hash_string}") + end + + def cache_name, do: @cache_name + + defp cache_expired?(address) do + address_hash_string = get_address_hash_string(address) + updated_at = fetch_from_cache("hash_#{address_hash_string}_#{@last_update_key}") + + cond do + is_nil(updated_at) -> true + current_time() - updated_at > @cache_period -> true + true -> false + end + end + + defp update_cache(address) do + address_hash_string = get_address_hash_string(address) + put_into_cache("hash_#{address_hash_string}_#{@last_update_key}", current_time()) + new_data = Chain.address_to_gas_usage_count(address) + put_into_cache("hash_#{address_hash_string}", new_data) + end + + defp fetch_from_cache(key) do + case :ets.lookup(@cache_name, key) do + [{_, value}] -> + value + + [] -> + 0 + end + end + + defp put_into_cache(key, value) do + :ets.insert(@cache_name, {key, value}) + end + + defp get_address_hash_string(address) do + Base.encode16(address.hash.bytes, case: :lower) + end + + defp current_time do + utc_now = DateTime.utc_now() + + DateTime.to_unix(utc_now, :millisecond) + end + + def create_cache_table do + if :ets.whereis(@cache_name) == :undefined do + :ets.new(@cache_name, @ets_opts) + end + end + + def enable_consolidation?, do: @enable_consolidation +end diff --git a/apps/explorer/lib/explorer/counters/address_transactions_counter.ex b/apps/explorer/lib/explorer/counters/address_transactions_counter.ex index 6a2313da45..6fc1c1de90 100644 --- a/apps/explorer/lib/explorer/counters/address_transactions_counter.ex +++ b/apps/explorer/lib/explorer/counters/address_transactions_counter.ex @@ -17,7 +17,7 @@ defmodule Explorer.Counters.AddressTransactionsCounter do read_concurrency: true ] - config = Application.get_env(:explorer, Explorer.Counters.AddressesCounter) + config = Application.get_env(:explorer, Explorer.Counters.AddressTransactionsCounter) @enable_consolidation Keyword.get(config, :enable_consolidation) @spec start_link(term()) :: GenServer.on_start()