From f2a79e45da745d8fbfb9f82f040aeeea52ba727d Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Fri, 22 Dec 2023 18:50:28 +0300 Subject: [PATCH] Expand gas price oracle functionality --- CHANGELOG.md | 2 + .../api/v1/gas_price_oracle_controller.ex | 4 +- .../controllers/api/v2/stats_controller.ex | 34 +++ .../gas_price_oracle_legend_item.html.eex | 10 +- .../lib/block_scout_web/views/chain_view.ex | 5 +- .../explorer/chain/cache/gas_price_oracle.ex | 206 ++++++++++---- apps/explorer/lib/explorer/market/market.ex | 2 +- .../lib/explorer/market/market_history.ex | 8 +- ...720_constrain_null_date_market_history.exs | 9 + .../chain/cache/gas_price_oracle_test.exs | 255 ++++++++++++++---- config/runtime.exs | 1 + cspell.json | 17 +- docker-compose/envs/common-blockscout.env | 6 + 13 files changed, 433 insertions(+), 126 deletions(-) create mode 100644 apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs diff --git a/CHANGELOG.md b/CHANGELOG.md index f4f63d9919..71a6a08a49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- [#9044](https://github.com/blockscout/blockscout/pull/9044) - Expand gas price oracle functionality + ### Fixes - [#9062](https://github.com/blockscout/blockscout/pull/9062) - Fix blockscout-ens integration diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex index 7b70b4b427..1c99dcb171 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex @@ -30,8 +30,8 @@ defmodule BlockScoutWeb.API.V1.GasPriceOracleController do |> send_resp(status, result) end - def result(gas_prices) do - gas_prices + defp result(gas_prices) do + %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]} |> Jason.encode!() end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex index 825d9f1b25..34ea7b3eae 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex @@ -12,6 +12,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Counters.AverageBlockTime + alias Plug.Conn alias Timex.Duration @api_true [api?: true] @@ -39,6 +40,21 @@ defmodule BlockScoutWeb.API.V2.StatsController do nil end + coin_price_change = + case Market.fetch_recent_history() do + [today, yesterday | _] -> + today.closing_price && yesterday.closing_price && + today.closing_price + |> Decimal.div(yesterday.closing_price) + |> Decimal.sub(1) + |> Decimal.mult(100) + |> Decimal.to_float() + |> Float.ceil(2) + + _ -> + nil + end + gas_price = Application.get_env(:block_scout_web, :gas_price) json( @@ -49,16 +65,20 @@ defmodule BlockScoutWeb.API.V2.StatsController do "total_transactions" => TransactionCache.estimated_count() |> to_string(), "average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(), "coin_price" => exchange_rate_from_db.usd_value, + "coin_price_change_percentage" => coin_price_change, "total_gas_used" => GasUsage.total() |> to_string(), "transactions_today" => Enum.at(transaction_stats, 0).number_of_transactions |> to_string(), "gas_used_today" => Enum.at(transaction_stats, 0).gas_used, "gas_prices" => gas_prices, + "gas_prices_update_in" => GasPriceOracle.global_ttl(), + "gas_price_updated_at" => GasPriceOracle.get_updated_at(), "static_gas_price" => gas_price, "market_cap" => Helper.market_cap(market_cap_type, exchange_rate_from_db), "tvl" => exchange_rate_from_db.tvl_usd, "network_utilization_percentage" => network_utilization_percentage() } |> add_rootstock_locked_btc() + |> backward_compatibility(conn) ) end @@ -135,4 +155,18 @@ defmodule BlockScoutWeb.API.V2.StatsController do _ -> stats end end + + defp backward_compatibility(response, conn) do + case Conn.get_req_header(conn, "updated-gas-oracle") do + ["true"] -> + response + + _ -> + response + |> Map.update("gas_prices", nil, fn + gas_prices -> + %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]} + end) + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex index 4b84dce358..f07899ea9b 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex @@ -8,7 +8,7 @@
-
<%= "#{gas_prices_from_oracle["average"]}" <> " " %><%= gettext "Gwei" %>
+
<%= "#{gas_prices_from_oracle[:average]}" <> " " %><%= gettext "Gwei" %>
-
<%= gettext "Slow" %><%= gas_prices_from_oracle["slow"] %> <%= gettext "Gwei" %>
-
<%= gettext "Average" %><%= gas_prices_from_oracle["average"] %> <%= gettext "Gwei" %>
-
<%= gettext "Fast" %><%= gas_prices_from_oracle["fast"] %> <%= gettext "Gwei" %>
+
<%= gettext "Slow" %><%= gas_prices_from_oracle[:slow] %> <%= gettext "Gwei" %>
+
<%= gettext "Average" %><%= gas_prices_from_oracle[:average] %> <%= gettext "Gwei" %>
+
<%= gettext "Fast" %><%= gas_prices_from_oracle[:fast] %> <%= gettext "Gwei" %>
" > @@ -40,4 +40,4 @@ <% end %> <% end %>
- \ No newline at end of file + diff --git a/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex b/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex index 43d66a2926..bbbcdebe86 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/chain_view.ex @@ -60,10 +60,7 @@ defmodule BlockScoutWeb.ChainView do defp gas_prices do case GasPriceOracle.get_gas_prices() do {:ok, gas_prices} -> - gas_prices - - nil -> - nil + %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]} _ -> nil diff --git a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex index e147c9c0c7..b1d0c10572 100644 --- a/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex +++ b/apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex @@ -17,27 +17,61 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do Wei } - alias Explorer.Repo + alias Explorer.Counters.AverageBlockTime + alias Explorer.{Market, Repo} + alias Timex.Duration use Explorer.Chain.MapCache, name: :gas_price, key: :gas_prices, + key: :gas_prices_acc, + key: :updated_at, key: :old_gas_prices, key: :async_task, - global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl], + global_ttl: global_ttl(), ttl_check_interval: :timer.seconds(1), callback: &async_task_on_deletion(&1) @doc """ - Get `safelow`, `average` and `fast` percentile of transactions gas prices among the last `num_of_blocks` blocks + Calculates the `slow`, `average`, and `fast` gas price and time percentiles from the last `num_of_blocks` blocks and estimates the fiat price for each percentile. + These percentiles correspond to the likelihood of a transaction being picked up by miners depending on the fee offered. """ @spec get_average_gas_price(pos_integer(), pos_integer(), pos_integer(), pos_integer()) :: - {:error, any} | {:ok, %{String.t() => nil | float, String.t() => nil | float, String.t() => nil | float}} + {{:error, any} | {:ok, %{slow: gas_price, average: gas_price, fast: gas_price}}, + [ + %{ + block_number: non_neg_integer(), + slow_gas_price: nil | Decimal.t(), + fast_gas_price: nil | Decimal.t(), + average_gas_price: nil | Decimal.t(), + slow_priority_fee_per_gas: nil | Decimal.t(), + average_priority_fee_per_gas: nil | Decimal.t(), + fast_priority_fee_per_gas: nil | Decimal.t(), + slow_time: nil | Decimal.t(), + average_time: nil | Decimal.t(), + fast_time: nil | Decimal.t() + } + ]} + when gas_price: nil | %{price: float(), time: float(), fiat_price: Decimal.t()} def get_average_gas_price(num_of_blocks, safelow_percentile, average_percentile, fast_percentile) do safelow_percentile_fraction = safelow_percentile / 100 average_percentile_fraction = average_percentile / 100 fast_percentile_fraction = fast_percentile / 100 + acc = get_gas_prices_acc() + + from_block = + case acc do + [%{block_number: from_block} | _] -> from_block + _ -> -1 + end + + average_block_time = + case AverageBlockTime.average_block_time() do + {:error, _} -> nil + average_block_time -> average_block_time |> Duration.to_milliseconds() + end + fee_query = from( block in Block, @@ -45,120 +79,186 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do where: block.consensus == true, where: transaction.status == ^1, where: transaction.gas_price > ^0, - group_by: block.number, - order_by: [desc: block.number], + where: transaction.block_number > ^from_block, + group_by: transaction.block_number, + order_by: [desc: transaction.block_number], select: %{ + block_number: transaction.block_number, slow_gas_price: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^safelow_percentile_fraction, transaction.gas_price ), average_gas_price: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^average_percentile_fraction, transaction.gas_price ), fast_gas_price: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^fast_percentile_fraction, transaction.gas_price ), - slow: + slow_priority_fee_per_gas: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^safelow_percentile_fraction, transaction.max_priority_fee_per_gas ), - average: + average_priority_fee_per_gas: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^average_percentile_fraction, transaction.max_priority_fee_per_gas ), - fast: + fast_priority_fee_per_gas: fragment( - "percentile_disc(?) within group ( order by ? )", + "percentile_disc(? :: real) within group ( order by ? )", ^fast_percentile_fraction, transaction.max_priority_fee_per_gas + ), + slow_time: + fragment( + "percentile_disc(? :: real) within group ( order by coalesce(extract(milliseconds from (?)::interval), ?) desc )", + ^safelow_percentile_fraction, + block.timestamp - transaction.earliest_processing_start, + ^average_block_time + ), + average_time: + fragment( + "percentile_disc(? :: real) within group ( order by coalesce(extract(milliseconds from (?)::interval), ?) desc )", + ^average_percentile_fraction, + block.timestamp - transaction.earliest_processing_start, + ^average_block_time + ), + fast_time: + fragment( + "percentile_disc(? :: real) within group ( order by coalesce(extract(milliseconds from (?)::interval), ?) desc )", + ^fast_percentile_fraction, + block.timestamp - transaction.earliest_processing_start, + ^average_block_time ) }, limit: ^num_of_blocks ) - gas_prices = fee_query |> Repo.all(timeout: :infinity) |> process_fee_data_from_db() + new_acc = fee_query |> Repo.all(timeout: :infinity) |> merge_gas_prices(acc, num_of_blocks) + + gas_prices = new_acc |> process_fee_data_from_db() - {:ok, gas_prices} + {{:ok, gas_prices}, new_acc} catch error -> - {:error, error} + Logger.error("Failed to get gas prices: #{inspect(error)}") + {{:error, error}, get_gas_prices_acc()} end + defp merge_gas_prices(new, acc, acc_size), do: Enum.take(new ++ acc, acc_size) + defp process_fee_data_from_db([]) do %{ - "slow" => nil, - "average" => nil, - "fast" => nil + slow: nil, + average: nil, + fast: nil } end defp process_fee_data_from_db(fees) do - fees_length = Enum.count(fees) - %{ slow_gas_price: slow_gas_price, average_gas_price: average_gas_price, fast_gas_price: fast_gas_price, - slow: slow, - average: average, - fast: fast - } = - fees - |> Enum.reduce( - &Map.merge(&1, &2, fn - _, v1, v2 when nil not in [v1, v2] -> Decimal.add(v1, v2) - _, v1, v2 -> v1 || v2 - end) - ) - |> Map.new(fn - {key, nil} -> {key, nil} - {key, value} -> {key, Decimal.div(value, fees_length)} - end) + slow_priority_fee_per_gas: slow_priority_fee_per_gas, + average_priority_fee_per_gas: average_priority_fee_per_gas, + fast_priority_fee_per_gas: fast_priority_fee_per_gas, + slow_time: slow_time, + average_time: average_time, + fast_time: fast_time + } = merge_fees(fees) json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) {slow_fee, average_fee, fast_fee} = - case {nil not in [slow, average, fast], EthereumJSONRPC.fetch_block_by_tag("pending", json_rpc_named_arguments)} do - {true, {:ok, %Blocks{blocks_params: [%{base_fee_per_gas: base_fee}]}}} when not is_nil(base_fee) -> + case nil not in [slow_priority_fee_per_gas, average_priority_fee_per_gas, fast_priority_fee_per_gas] && + EthereumJSONRPC.fetch_block_by_tag("pending", json_rpc_named_arguments) do + {:ok, %Blocks{blocks_params: [%{base_fee_per_gas: base_fee}]}} when not is_nil(base_fee) -> base_fee_wei = base_fee |> Decimal.new() |> Wei.from(:wei) { - priority_with_base_fee(slow, base_fee_wei), - priority_with_base_fee(average, base_fee_wei), - priority_with_base_fee(fast, base_fee_wei) + priority_with_base_fee(slow_priority_fee_per_gas, base_fee_wei), + priority_with_base_fee(average_priority_fee_per_gas, base_fee_wei), + priority_with_base_fee(fast_priority_fee_per_gas, base_fee_wei) } _ -> {gas_price(slow_gas_price), gas_price(average_gas_price), gas_price(fast_gas_price)} end + exchange_rate_from_db = Market.get_coin_exchange_rate() + %{ - "slow" => slow_fee, - "average" => average_fee, - "fast" => fast_fee + slow: compose_gas_price(slow_fee, slow_time, exchange_rate_from_db), + average: compose_gas_price(average_fee, average_time, exchange_rate_from_db), + fast: compose_gas_price(fast_fee, fast_time, exchange_rate_from_db) } end + defp merge_fees(fees_from_db) do + fees_from_db + |> Stream.map(&Map.delete(&1, :block_number)) + |> Enum.reduce( + &Map.merge(&1, &2, fn + _, nil, nil -> nil + _, val, acc when nil not in [val, acc] and is_list(acc) -> [val | acc] + _, val, acc when nil not in [val, acc] -> [val, acc] + _, val, acc -> [val || acc] + end) + ) + |> Map.new(fn + {key, nil} -> + {key, nil} + + {key, value} -> + value = if is_list(value), do: value, else: [value] + count = Enum.count(value) + {key, value |> Enum.reduce(Decimal.new(0), &Decimal.add/2) |> Decimal.div(count)} + end) + end + + defp compose_gas_price(fee, time, exchange_rate_from_db) do + %{ + price: fee |> format_wei(), + time: time && time |> Decimal.to_float(), + fiat_price: fiat_fee(fee, exchange_rate_from_db) + } + end + + defp fiat_fee(fee, exchange_rate) do + exchange_rate.usd_value && + fee + |> Wei.to(:ether) + |> Decimal.mult(exchange_rate.usd_value) + |> Decimal.mult(simple_transaction_gas()) + |> Decimal.round(2) + end + defp priority_with_base_fee(priority, base_fee) do - priority |> Wei.from(:wei) |> Wei.sum(base_fee) |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2) + priority |> Wei.from(:wei) |> Wei.sum(base_fee) end defp gas_price(value) do - value |> Wei.from(:wei) |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2) + value |> Wei.from(:wei) end + defp format_wei(wei), do: wei |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2) + + def global_ttl, do: Application.get_env(:explorer, __MODULE__)[:global_ttl] + + defp simple_transaction_gas, do: Application.get_env(:explorer, __MODULE__)[:simple_transaction_gas] + defp num_of_blocks, do: Application.get_env(:explorer, __MODULE__)[:num_of_blocks] defp safelow, do: Application.get_env(:explorer, __MODULE__)[:safelow_percentile] @@ -181,12 +281,14 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do {:ok, task} = Task.start(fn -> try do - result = get_average_gas_price(num_of_blocks(), safelow(), average(), fast()) + {result, acc} = get_average_gas_price(num_of_blocks(), safelow(), average(), fast()) + set_gas_prices_acc(acc) set_gas_prices(result) + set_updated_at(DateTime.utc_now()) rescue e -> - Logger.debug([ + Logger.error([ "Couldn't update gas used gas_prices", Exception.format(:error, e, __STACKTRACE__) ]) @@ -198,6 +300,10 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do {:update, task} end + defp handle_fallback(:gas_prices_acc) do + {:return, []} + end + defp handle_fallback(_), do: {:return, nil} # By setting this as a `callback` an async task will be started each time the diff --git a/apps/explorer/lib/explorer/market/market.ex b/apps/explorer/lib/explorer/market/market.ex index 3691ac4eaa..6de43a1b75 100644 --- a/apps/explorer/lib/explorer/market/market.ex +++ b/apps/explorer/lib/explorer/market/market.ex @@ -52,7 +52,7 @@ defmodule Explorer.Market do @doc """ Get most recent exchange rate for the native coin from ETS or from DB. """ - @spec get_coin_exchange_rate() :: Token.t() | nil + @spec get_coin_exchange_rate() :: Token.t() def get_coin_exchange_rate do get_native_coin_exchange_rate_from_cache() || get_native_coin_exchange_rate_from_db() || Token.null() end diff --git a/apps/explorer/lib/explorer/market/market_history.ex b/apps/explorer/lib/explorer/market/market_history.ex index 0fabaf0e30..8c1411879e 100644 --- a/apps/explorer/lib/explorer/market/market_history.ex +++ b/apps/explorer/lib/explorer/market/market_history.ex @@ -23,10 +23,10 @@ defmodule Explorer.Market.MarketHistory do * `:market_cap` - TVL in USD. """ @type t :: %__MODULE__{ - closing_price: Decimal.t(), + closing_price: Decimal.t() | nil, date: Date.t(), - opening_price: Decimal.t(), - market_cap: Decimal.t(), - tvl: Decimal.t() + opening_price: Decimal.t() | nil, + market_cap: Decimal.t() | nil, + tvl: Decimal.t() | nil } end diff --git a/apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs b/apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs new file mode 100644 index 0000000000..ab43538e4e --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs @@ -0,0 +1,9 @@ +defmodule Explorer.Repo.Migrations.ConstrainNullDateMarketHistory do + use Ecto.Migration + + def change do + alter table(:market_history) do + modify(:date, :date, null: false, from: {:date, null: true}) + end + end +end diff --git a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs index d7004d11d0..20472f79ee 100644 --- a/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs +++ b/apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs @@ -4,6 +4,7 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do import Mox alias Explorer.Chain.Cache.GasPriceOracle + alias Explorer.Counters.AverageBlockTime @block %{ "difficulty" => "0x0", @@ -48,12 +49,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do test "returns nil percentile values if no blocks in the DB" do expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns nil percentile values if blocks are empty in the DB" do @@ -63,12 +64,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do insert(:block) insert(:block) - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns nil percentile values for blocks with failed txs in the DB" do @@ -89,12 +90,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" ) - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns nil percentile values for transactions with 0 gas price aka 'whitelisted transactions' in the DB" do @@ -127,12 +128,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" ) - assert {:ok, - %{ - "slow" => nil, - "average" => nil, - "fast" => nil - }} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) + assert {{:ok, + %{ + slow: nil, + average: nil, + fast: nil + }}, []} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) end test "returns the same percentile values if gas price is the same over transactions" do @@ -165,12 +166,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" ) - assert {:ok, - %{ - "slow" => 1.0, - "average" => 1.0, - "fast" => 1.0 - }} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) + assert {{:ok, + %{ + slow: %{price: 1.0}, + average: %{price: 1.0}, + fast: %{price: 1.0} + }}, _} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) end test "returns correct min gas price from the block" do @@ -215,12 +216,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" ) - assert {:ok, - %{ - "slow" => 1.0, - "average" => 2.0, - "fast" => 2.0 - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + slow: %{price: 1.0}, + average: %{price: 2.0}, + fast: %{price: 2.0} + }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns correct average percentile" do @@ -266,10 +267,10 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x7d4bc5569053fc29f471901e967c9e60205ac7a122b0e9a789683652c34cc11a" ) - assert {:ok, - %{ - "average" => 3.34 - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + average: %{price: 3.34} + }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) end test "returns correct gas price for EIP-1559 transactions" do @@ -320,13 +321,173 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" ) - assert {:ok, - %{ - # including base fee - "slow" => 1.5, - "average" => 2.5, - "fast" => 2.5 - }} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + assert {{:ok, + %{ + # including base fee + slow: %{price: 1.5}, + average: %{price: 2.5} + }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + end + + test "return gas prices with time if available" do + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) + + block1 = + insert(:block, + number: 100, + hash: "0x3e51328bccedee581e8ba35190216a61a5d67fd91ca528f3553142c0c7d18391", + timestamp: ~U[2023-12-12 12:12:30.000000Z] + ) + + block2 = + insert(:block, + number: 101, + hash: "0x76c3da57334fffdc66c0d954dce1a910fcff13ec889a13b2d8b0b6e9440ce729", + timestamp: ~U[2023-12-12 12:13:00.000000Z] + ) + + :transaction + |> insert( + status: :ok, + block_hash: block1.hash, + block_number: block1.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269", + earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z] + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03", + earliest_processing_start: ~U[2023-12-12 12:12:00.000000Z] + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 1, + gas_price: 3_000_000_000, + max_priority_fee_per_gas: 3_000_000_000, + max_fee_per_gas: 3_000_000_000, + hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016", + earliest_processing_start: ~U[2023-12-12 12:12:55.000000Z] + ) + + assert {{ + :ok, + %{ + average: %{price: 2.5, time: 15000.0}, + fast: %{price: 2.5, time: 15000.0}, + slow: %{price: 1.5, time: 17500.0} + } + }, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + end + + test "return gas prices with average block time if earliest_processing_start is not available" do + expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end) + old_env = Application.get_env(:explorer, AverageBlockTime) + Application.put_env(:explorer, AverageBlockTime, enabled: true, cache_period: 1_800_000) + start_supervised!(AverageBlockTime) + + block_number = 99_999_999 + first_timestamp = ~U[2023-12-12 12:12:30.000000Z] + + Enum.each(1..100, fn i -> + insert(:block, + number: block_number + 1 + i, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -(101 - i) - 12) + ) + end) + + block1 = + insert(:block, + number: block_number + 102, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -10) + ) + + block2 = + insert(:block, + number: block_number + 103, + consensus: true, + timestamp: Timex.shift(first_timestamp, seconds: -7) + ) + + AverageBlockTime.refresh() + + :transaction + |> insert( + status: :ok, + block_hash: block1.hash, + block_number: block1.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269" + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 0, + gas_price: 1_000_000_000, + max_priority_fee_per_gas: 1_000_000_000, + max_fee_per_gas: 1_000_000_000, + hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03" + ) + + :transaction + |> insert( + status: :ok, + block_hash: block2.hash, + block_number: block2.number, + cumulative_gas_used: 884_322, + gas_used: 106_025, + index: 1, + gas_price: 3_000_000_000, + max_priority_fee_per_gas: 3_000_000_000, + max_fee_per_gas: 3_000_000_000, + hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" + ) + + AverageBlockTime.refresh() + + assert {{ + :ok, + %{ + average: %{price: 2.5, time: 1000.0}, + fast: %{price: 2.5, time: 1000.0}, + slow: %{price: 1.5, time: 1000.0} + } + }, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) + + Application.put_env(:explorer, AverageBlockTime, old_env) end end end diff --git a/config/runtime.exs b/config/runtime.exs index f876325abb..afcd147129 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -243,6 +243,7 @@ config :explorer, Explorer.Chain.Cache.PendingBlockOperation, config :explorer, Explorer.Chain.Cache.GasPriceOracle, global_ttl: ConfigHelper.parse_time_env_var("GAS_PRICE_ORACLE_CACHE_PERIOD", "30s"), + simple_transaction_gas: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_SIMPLE_TRANSACTION_GAS", 21000), num_of_blocks: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_NUM_OF_BLOCKS", 200), safelow_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_SAFELOW_PERCENTILE", 35), average_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_AVERAGE_PERCENTILE", 60), diff --git a/cspell.json b/cspell.json index 3cdf520ecd..78b98a92d7 100644 --- a/cspell.json +++ b/cspell.json @@ -166,6 +166,7 @@ "Faileddi", "falala", "Filesize", + "fkey", "Floki", "fontawesome", "fortawesome", @@ -264,6 +265,7 @@ "mergeable", "Merkle", "metatags", + "microsecs", "millis", "mintings", "mistmatches", @@ -408,6 +410,7 @@ "snapshotted", "snapshotting", "Sokol", + "SOLIDITYSCAN", "soljson", "someout", "sourcecode", @@ -534,19 +537,7 @@ "zindex", "zipcode", "zkbob", - "zkevm", - "erts", - "Asfpp", - "Nerg", - "secp", - "qwertyuioiuytrewertyuioiuytrertyuio", - "urlset", - "lastmod", - "qitmeer", - "meer", - "DefiLlama", - "SOLIDITYSCAN", - "fkey" + "zkevm" ], "enableFiletypes": [ "dotenv", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 73af4cb85a..f3e67286b6 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -182,6 +182,12 @@ COIN_BALANCE_HISTORY_DAYS=90 APPS_MENU=true EXTERNAL_APPS=[] # GAS_PRICE= +# GAS_PRICE_ORACLE_CACHE_PERIOD= +# GAS_PRICE_ORACLE_SIMPLE_TRANSACTION_GAS= +# GAS_PRICE_ORACLE_NUM_OF_BLOCKS= +# GAS_PRICE_ORACLE_SAFELOW_PERCENTILE= +# GAS_PRICE_ORACLE_AVERAGE_PERCENTILE= +# GAS_PRICE_ORACLE_FAST_PERCENTILE= # RESTRICTED_LIST= # RESTRICTED_LIST_KEY= SHOW_MAINTENANCE_ALERT=false