Merge pull request #9044 from blockscout/mf-gas-tracker-v1

Expand gas price oracle functionality
pull/8812/head
Victor Baranov 11 months ago committed by GitHub
commit 3cbaa1c423
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      CHANGELOG.md
  2. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/v1/gas_price_oracle_controller.ex
  3. 34
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/stats_controller.ex
  4. 8
      apps/block_scout_web/lib/block_scout_web/templates/chain/gas_price_oracle_legend_item.html.eex
  5. 5
      apps/block_scout_web/lib/block_scout_web/views/chain_view.ex
  6. 206
      apps/explorer/lib/explorer/chain/cache/gas_price_oracle.ex
  7. 2
      apps/explorer/lib/explorer/market/market.ex
  8. 8
      apps/explorer/lib/explorer/market/market_history.ex
  9. 9
      apps/explorer/priv/repo/migrations/20240103094720_constrain_null_date_market_history.exs
  10. 237
      apps/explorer/test/explorer/chain/cache/gas_price_oracle_test.exs
  11. 1
      config/runtime.exs
  12. 17
      cspell.json
  13. 6
      docker-compose/envs/common-blockscout.env

@ -4,6 +4,8 @@
### Features ### Features
- [#9044](https://github.com/blockscout/blockscout/pull/9044) - Expand gas price oracle functionality
### Fixes ### Fixes
- [#9062](https://github.com/blockscout/blockscout/pull/9062) - Fix blockscout-ens integration - [#9062](https://github.com/blockscout/blockscout/pull/9062) - Fix blockscout-ens integration

@ -30,8 +30,8 @@ defmodule BlockScoutWeb.API.V1.GasPriceOracleController do
|> send_resp(status, result) |> send_resp(status, result)
end end
def result(gas_prices) do defp result(gas_prices) do
gas_prices %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]}
|> Jason.encode!() |> Jason.encode!()
end end

@ -12,6 +12,7 @@ defmodule BlockScoutWeb.API.V2.StatsController do
alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Supply.RSK
alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Chain.Transaction.History.TransactionStats
alias Explorer.Counters.AverageBlockTime alias Explorer.Counters.AverageBlockTime
alias Plug.Conn
alias Timex.Duration alias Timex.Duration
@api_true [api?: true] @api_true [api?: true]
@ -39,6 +40,21 @@ defmodule BlockScoutWeb.API.V2.StatsController do
nil nil
end 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) gas_price = Application.get_env(:block_scout_web, :gas_price)
json( json(
@ -49,16 +65,20 @@ defmodule BlockScoutWeb.API.V2.StatsController do
"total_transactions" => TransactionCache.estimated_count() |> to_string(), "total_transactions" => TransactionCache.estimated_count() |> to_string(),
"average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(), "average_block_time" => AverageBlockTime.average_block_time() |> Duration.to_milliseconds(),
"coin_price" => exchange_rate_from_db.usd_value, "coin_price" => exchange_rate_from_db.usd_value,
"coin_price_change_percentage" => coin_price_change,
"total_gas_used" => GasUsage.total() |> to_string(), "total_gas_used" => GasUsage.total() |> to_string(),
"transactions_today" => Enum.at(transaction_stats, 0).number_of_transactions |> 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_used_today" => Enum.at(transaction_stats, 0).gas_used,
"gas_prices" => gas_prices, "gas_prices" => gas_prices,
"gas_prices_update_in" => GasPriceOracle.global_ttl(),
"gas_price_updated_at" => GasPriceOracle.get_updated_at(),
"static_gas_price" => gas_price, "static_gas_price" => gas_price,
"market_cap" => Helper.market_cap(market_cap_type, exchange_rate_from_db), "market_cap" => Helper.market_cap(market_cap_type, exchange_rate_from_db),
"tvl" => exchange_rate_from_db.tvl_usd, "tvl" => exchange_rate_from_db.tvl_usd,
"network_utilization_percentage" => network_utilization_percentage() "network_utilization_percentage" => network_utilization_percentage()
} }
|> add_rootstock_locked_btc() |> add_rootstock_locked_btc()
|> backward_compatibility(conn)
) )
end end
@ -135,4 +155,18 @@ defmodule BlockScoutWeb.API.V2.StatsController do
_ -> stats _ -> stats
end end
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 end

@ -8,7 +8,7 @@
<span class="dashboard-banner-chart-legend-value inline"> <span class="dashboard-banner-chart-legend-value inline">
<div> <div>
<div class="d-flex flex-row" style="height: 20px; line-height: 20px;"> <div class="d-flex flex-row" style="height: 20px; line-height: 20px;">
<div class="flex-column"><%= "#{gas_prices_from_oracle["average"]}" <> " " %><%= gettext "Gwei" %></div> <div class="flex-column"><%= "#{gas_prices_from_oracle[:average]}" <> " " %><%= gettext "Gwei" %></div>
<span <span
data-toggle="tooltip" data-toggle="tooltip"
data-placement="top" data-placement="top"
@ -17,9 +17,9 @@
title=" title="
<div class='custom-tooltip header'><%= gettext "Gas tracker" %></div> <div class='custom-tooltip header'><%= gettext "Gas tracker" %></div>
<div> <div>
<div class='custom-tooltip description left d-flex'><span><%= gettext "Slow" %></span><span class='custom-tooltip description right'><%= gas_prices_from_oracle["slow"] %> <%= gettext "Gwei" %></span></div> <div class='custom-tooltip description left d-flex'><span><%= gettext "Slow" %></span><span class='custom-tooltip description right'><%= gas_prices_from_oracle[:slow] %> <%= gettext "Gwei" %></span></div>
<div class='custom-tooltip description left d-flex'><span><%= gettext "Average" %></span><span class='custom-tooltip description right'><%= gas_prices_from_oracle["average"] %> <%= gettext "Gwei" %></span></div> <div class='custom-tooltip description left d-flex'><span><%= gettext "Average" %></span><span class='custom-tooltip description right'><%= gas_prices_from_oracle[:average] %> <%= gettext "Gwei" %></span></div>
<div class='custom-tooltip description left d-flex'><span><%= gettext "Fast" %></span><span class='custom-tooltip description right'><%= gas_prices_from_oracle["fast"] %> <%= gettext "Gwei" %></span></div> <div class='custom-tooltip description left d-flex'><span><%= gettext "Fast" %></span><span class='custom-tooltip description right'><%= gas_prices_from_oracle[:fast] %> <%= gettext "Gwei" %></span></div>
</div> </div>
" "
> >

@ -60,10 +60,7 @@ defmodule BlockScoutWeb.ChainView do
defp gas_prices do defp gas_prices do
case GasPriceOracle.get_gas_prices() do case GasPriceOracle.get_gas_prices() do
{:ok, gas_prices} -> {:ok, gas_prices} ->
gas_prices %{slow: gas_prices[:slow][:price], average: gas_prices[:average][:price], fast: gas_prices[:fast][:price]}
nil ->
nil
_ -> _ ->
nil nil

@ -17,27 +17,61 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
Wei Wei
} }
alias Explorer.Repo alias Explorer.Counters.AverageBlockTime
alias Explorer.{Market, Repo}
alias Timex.Duration
use Explorer.Chain.MapCache, use Explorer.Chain.MapCache,
name: :gas_price, name: :gas_price,
key: :gas_prices, key: :gas_prices,
key: :gas_prices_acc,
key: :updated_at,
key: :old_gas_prices, key: :old_gas_prices,
key: :async_task, key: :async_task,
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl], global_ttl: global_ttl(),
ttl_check_interval: :timer.seconds(1), ttl_check_interval: :timer.seconds(1),
callback: &async_task_on_deletion(&1) callback: &async_task_on_deletion(&1)
@doc """ @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()) :: @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 def get_average_gas_price(num_of_blocks, safelow_percentile, average_percentile, fast_percentile) do
safelow_percentile_fraction = safelow_percentile / 100 safelow_percentile_fraction = safelow_percentile / 100
average_percentile_fraction = average_percentile / 100 average_percentile_fraction = average_percentile / 100
fast_percentile_fraction = fast_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 = fee_query =
from( from(
block in Block, block in Block,
@ -45,120 +79,186 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
where: block.consensus == true, where: block.consensus == true,
where: transaction.status == ^1, where: transaction.status == ^1,
where: transaction.gas_price > ^0, where: transaction.gas_price > ^0,
group_by: block.number, where: transaction.block_number > ^from_block,
order_by: [desc: block.number], group_by: transaction.block_number,
order_by: [desc: transaction.block_number],
select: %{ select: %{
block_number: transaction.block_number,
slow_gas_price: slow_gas_price:
fragment( fragment(
"percentile_disc(?) within group ( order by ? )", "percentile_disc(? :: real) within group ( order by ? )",
^safelow_percentile_fraction, ^safelow_percentile_fraction,
transaction.gas_price transaction.gas_price
), ),
average_gas_price: average_gas_price:
fragment( fragment(
"percentile_disc(?) within group ( order by ? )", "percentile_disc(? :: real) within group ( order by ? )",
^average_percentile_fraction, ^average_percentile_fraction,
transaction.gas_price transaction.gas_price
), ),
fast_gas_price: fast_gas_price:
fragment( fragment(
"percentile_disc(?) within group ( order by ? )", "percentile_disc(? :: real) within group ( order by ? )",
^fast_percentile_fraction, ^fast_percentile_fraction,
transaction.gas_price transaction.gas_price
), ),
slow: slow_priority_fee_per_gas:
fragment( fragment(
"percentile_disc(?) within group ( order by ? )", "percentile_disc(? :: real) within group ( order by ? )",
^safelow_percentile_fraction, ^safelow_percentile_fraction,
transaction.max_priority_fee_per_gas transaction.max_priority_fee_per_gas
), ),
average: average_priority_fee_per_gas:
fragment( fragment(
"percentile_disc(?) within group ( order by ? )", "percentile_disc(? :: real) within group ( order by ? )",
^average_percentile_fraction, ^average_percentile_fraction,
transaction.max_priority_fee_per_gas transaction.max_priority_fee_per_gas
), ),
fast: fast_priority_fee_per_gas:
fragment( fragment(
"percentile_disc(?) within group ( order by ? )", "percentile_disc(? :: real) within group ( order by ? )",
^fast_percentile_fraction, ^fast_percentile_fraction,
transaction.max_priority_fee_per_gas 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 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 catch
error -> error ->
{:error, error} Logger.error("Failed to get gas prices: #{inspect(error)}")
{{:error, error}, get_gas_prices_acc()}
end end
defp merge_gas_prices(new, acc, acc_size), do: Enum.take(new ++ acc, acc_size)
defp process_fee_data_from_db([]) do defp process_fee_data_from_db([]) do
%{ %{
"slow" => nil, slow: nil,
"average" => nil, average: nil,
"fast" => nil fast: nil
} }
end end
defp process_fee_data_from_db(fees) do defp process_fee_data_from_db(fees) do
fees_length = Enum.count(fees)
%{ %{
slow_gas_price: slow_gas_price, slow_gas_price: slow_gas_price,
average_gas_price: average_gas_price, average_gas_price: average_gas_price,
fast_gas_price: fast_gas_price, fast_gas_price: fast_gas_price,
slow: slow, slow_priority_fee_per_gas: slow_priority_fee_per_gas,
average: average, average_priority_fee_per_gas: average_priority_fee_per_gas,
fast: fast fast_priority_fee_per_gas: fast_priority_fee_per_gas,
} = slow_time: slow_time,
fees average_time: average_time,
|> Enum.reduce( fast_time: fast_time
&Map.merge(&1, &2, fn } = merge_fees(fees)
_, 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)
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments) json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
{slow_fee, average_fee, fast_fee} = {slow_fee, average_fee, fast_fee} =
case {nil not in [slow, average, fast], EthereumJSONRPC.fetch_block_by_tag("pending", json_rpc_named_arguments)} do case nil not in [slow_priority_fee_per_gas, average_priority_fee_per_gas, fast_priority_fee_per_gas] &&
{true, {:ok, %Blocks{blocks_params: [%{base_fee_per_gas: base_fee}]}}} when not is_nil(base_fee) -> 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) base_fee_wei = base_fee |> Decimal.new() |> Wei.from(:wei)
{ {
priority_with_base_fee(slow, base_fee_wei), priority_with_base_fee(slow_priority_fee_per_gas, base_fee_wei),
priority_with_base_fee(average, base_fee_wei), priority_with_base_fee(average_priority_fee_per_gas, base_fee_wei),
priority_with_base_fee(fast, 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)} {gas_price(slow_gas_price), gas_price(average_gas_price), gas_price(fast_gas_price)}
end end
exchange_rate_from_db = Market.get_coin_exchange_rate()
%{ %{
"slow" => slow_fee, slow: compose_gas_price(slow_fee, slow_time, exchange_rate_from_db),
"average" => average_fee, average: compose_gas_price(average_fee, average_time, exchange_rate_from_db),
"fast" => fast_fee fast: compose_gas_price(fast_fee, fast_time, exchange_rate_from_db)
} }
end 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 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 end
defp gas_price(value) do defp gas_price(value) do
value |> Wei.from(:wei) |> Wei.to(:gwei) |> Decimal.to_float() |> Float.ceil(2) value |> Wei.from(:wei)
end 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 num_of_blocks, do: Application.get_env(:explorer, __MODULE__)[:num_of_blocks]
defp safelow, do: Application.get_env(:explorer, __MODULE__)[:safelow_percentile] defp safelow, do: Application.get_env(:explorer, __MODULE__)[:safelow_percentile]
@ -181,12 +281,14 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
{:ok, task} = {:ok, task} =
Task.start(fn -> Task.start(fn ->
try do 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_gas_prices(result)
set_updated_at(DateTime.utc_now())
rescue rescue
e -> e ->
Logger.debug([ Logger.error([
"Couldn't update gas used gas_prices", "Couldn't update gas used gas_prices",
Exception.format(:error, e, __STACKTRACE__) Exception.format(:error, e, __STACKTRACE__)
]) ])
@ -198,6 +300,10 @@ defmodule Explorer.Chain.Cache.GasPriceOracle do
{:update, task} {:update, task}
end end
defp handle_fallback(:gas_prices_acc) do
{:return, []}
end
defp handle_fallback(_), do: {:return, nil} defp handle_fallback(_), do: {:return, nil}
# By setting this as a `callback` an async task will be started each time the # By setting this as a `callback` an async task will be started each time the

@ -52,7 +52,7 @@ defmodule Explorer.Market do
@doc """ @doc """
Get most recent exchange rate for the native coin from ETS or from DB. 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 def get_coin_exchange_rate do
get_native_coin_exchange_rate_from_cache() || get_native_coin_exchange_rate_from_db() || Token.null() get_native_coin_exchange_rate_from_cache() || get_native_coin_exchange_rate_from_db() || Token.null()
end end

@ -23,10 +23,10 @@ defmodule Explorer.Market.MarketHistory do
* `:market_cap` - TVL in USD. * `:market_cap` - TVL in USD.
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
closing_price: Decimal.t(), closing_price: Decimal.t() | nil,
date: Date.t(), date: Date.t(),
opening_price: Decimal.t(), opening_price: Decimal.t() | nil,
market_cap: Decimal.t(), market_cap: Decimal.t() | nil,
tvl: Decimal.t() tvl: Decimal.t() | nil
} }
end end

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

@ -4,6 +4,7 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
import Mox import Mox
alias Explorer.Chain.Cache.GasPriceOracle alias Explorer.Chain.Cache.GasPriceOracle
alias Explorer.Counters.AverageBlockTime
@block %{ @block %{
"difficulty" => "0x0", "difficulty" => "0x0",
@ -48,12 +49,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
test "returns nil percentile values if no blocks in the DB" 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) expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id}], _options -> {:ok, [%{id: id, result: @block}]} end)
assert {:ok, assert {{:ok,
%{ %{
"slow" => nil, slow: nil,
"average" => nil, average: nil,
"fast" => nil fast: nil
}} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90)
end end
test "returns nil percentile values if blocks are empty in the DB" do 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)
insert(:block) insert(:block)
assert {:ok, assert {{:ok,
%{ %{
"slow" => nil, slow: nil,
"average" => nil, average: nil,
"fast" => nil fast: nil
}} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90)
end end
test "returns nil percentile values for blocks with failed txs in the DB" do 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" hash: "0xac2a7dab94d965893199e7ee01649e2d66f0787a4c558b3118c09e80d4df8269"
) )
assert {:ok, assert {{:ok,
%{ %{
"slow" => nil, slow: nil,
"average" => nil, average: nil,
"fast" => nil fast: nil
}} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) }}, []} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90)
end end
test "returns nil percentile values for transactions with 0 gas price aka 'whitelisted transactions' in the DB" do 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" hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03"
) )
assert {:ok, assert {{:ok,
%{ %{
"slow" => nil, slow: nil,
"average" => nil, average: nil,
"fast" => nil fast: nil
}} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) }}, []} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90)
end end
test "returns the same percentile values if gas price is the same over transactions" do 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" hash: "0x5d5c2776f96704e7845f7d3c1fbba6685ab6efd6f82b6cd11d549f3b3a46bd03"
) )
assert {:ok, assert {{:ok,
%{ %{
"slow" => 1.0, slow: %{price: 1.0},
"average" => 1.0, average: %{price: 1.0},
"fast" => 1.0 fast: %{price: 1.0}
}} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90) }}, _} = GasPriceOracle.get_average_gas_price(2, 35, 60, 90)
end end
test "returns correct min gas price from the block" do test "returns correct min gas price from the block" do
@ -215,12 +216,12 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016"
) )
assert {:ok, assert {{:ok,
%{ %{
"slow" => 1.0, slow: %{price: 1.0},
"average" => 2.0, average: %{price: 2.0},
"fast" => 2.0 fast: %{price: 2.0}
}} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90)
end end
test "returns correct average percentile" do test "returns correct average percentile" do
@ -266,10 +267,10 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
hash: "0x7d4bc5569053fc29f471901e967c9e60205ac7a122b0e9a789683652c34cc11a" hash: "0x7d4bc5569053fc29f471901e967c9e60205ac7a122b0e9a789683652c34cc11a"
) )
assert {:ok, assert {{:ok,
%{ %{
"average" => 3.34 average: %{price: 3.34}
}} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90) }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90)
end end
test "returns correct gas price for EIP-1559 transactions" do test "returns correct gas price for EIP-1559 transactions" do
@ -320,13 +321,173 @@ defmodule Explorer.Chain.Cache.GasPriceOracleTest do
hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016" hash: "0x906b80861b4a0921acfbb91a7b527227b0d32adabc88bc73e8c52ff714e55016"
) )
assert {:ok, assert {{:ok,
%{ %{
# including base fee # including base fee
"slow" => 1.5, slow: %{price: 1.5},
"average" => 2.5, average: %{price: 2.5}
"fast" => 2.5 }}, _} = GasPriceOracle.get_average_gas_price(3, 35, 60, 90)
}} = 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 end
end end

@ -243,6 +243,7 @@ config :explorer, Explorer.Chain.Cache.PendingBlockOperation,
config :explorer, Explorer.Chain.Cache.GasPriceOracle, config :explorer, Explorer.Chain.Cache.GasPriceOracle,
global_ttl: ConfigHelper.parse_time_env_var("GAS_PRICE_ORACLE_CACHE_PERIOD", "30s"), 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), 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), 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), average_percentile: ConfigHelper.parse_integer_env_var("GAS_PRICE_ORACLE_AVERAGE_PERCENTILE", 60),

@ -166,6 +166,7 @@
"Faileddi", "Faileddi",
"falala", "falala",
"Filesize", "Filesize",
"fkey",
"Floki", "Floki",
"fontawesome", "fontawesome",
"fortawesome", "fortawesome",
@ -264,6 +265,7 @@
"mergeable", "mergeable",
"Merkle", "Merkle",
"metatags", "metatags",
"microsecs",
"millis", "millis",
"mintings", "mintings",
"mistmatches", "mistmatches",
@ -408,6 +410,7 @@
"snapshotted", "snapshotted",
"snapshotting", "snapshotting",
"Sokol", "Sokol",
"SOLIDITYSCAN",
"soljson", "soljson",
"someout", "someout",
"sourcecode", "sourcecode",
@ -534,19 +537,7 @@
"zindex", "zindex",
"zipcode", "zipcode",
"zkbob", "zkbob",
"zkevm", "zkevm"
"erts",
"Asfpp",
"Nerg",
"secp",
"qwertyuioiuytrewertyuioiuytrertyuio",
"urlset",
"lastmod",
"qitmeer",
"meer",
"DefiLlama",
"SOLIDITYSCAN",
"fkey"
], ],
"enableFiletypes": [ "enableFiletypes": [
"dotenv", "dotenv",

@ -182,6 +182,12 @@ COIN_BALANCE_HISTORY_DAYS=90
APPS_MENU=true APPS_MENU=true
EXTERNAL_APPS=[] EXTERNAL_APPS=[]
# GAS_PRICE= # 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=
# RESTRICTED_LIST_KEY= # RESTRICTED_LIST_KEY=
SHOW_MAINTENANCE_ALERT=false SHOW_MAINTENANCE_ALERT=false

Loading…
Cancel
Save