diff --git a/CHANGELOG.md b/CHANGELOG.md index 6a638b58..727a0fd0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,14 @@ ### Enhancements - Add [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) set-code transaction support (type 4) +- Add `Ethers.fee_history/4` (`eth_feeHistory`) with decoded integer quantities, also usable + in `Ethers.batch/2` via `{:fee_history, [block_count, newest_block, reward_percentiles]}` +- Add `Ethers.estimate_fees/1`: estimate EIP-1559 `max_fee_per_gas` and + `max_priority_fee_per_gas` from recent blocks' priority fees (`:slow`/`:standard`/`:fast` + speeds or a raw percentile) with `max_fee_per_gas = 2 * next_base_fee + priority_fee` +- Transaction auto-fill now prices EIP-1559 transactions with `Ethers.estimate_fees/1` + instead of overpaying with a margin over `eth_gasPrice`, falling back to the legacy + behaviour on RPC clients without `eth_feeHistory` support ## 0.7.0 (2026-07-20) diff --git a/lib/ethers.ex b/lib/ethers.ex index 181a03ca..a4b04140 100644 --- a/lib/ethers.ex +++ b/lib/ethers.ex @@ -92,6 +92,7 @@ defmodule Ethers do current_block_number: :eth_block_number, current_gas_price: :eth_gas_price, estimate_gas: :eth_estimate_gas, + fee_history: :eth_fee_history, gas_price: :eth_gas_price, get_logs: :eth_get_logs, get_transaction_count: :eth_get_transaction_count, @@ -104,6 +105,12 @@ defmodule Ethers do } @send_transaction_actions [:send_transaction, :send] + # Percentiles the priority fees of recent blocks are sampled at per fee estimation speed + @fee_estimation_percentiles %{slow: 25, standard: 50, fast: 75} + @fee_estimation_block_count 10 + + @block_tags ["latest", "earliest", "pending", "safe", "finalized"] + @type t_batch_request :: atom() | {atom, term()} | {atom, term(), Keyword.t()} defguardp valid_result(bin) when bin != "0x" @@ -396,8 +403,12 @@ defmodule Ethers do - `:chain_id`: Chain id for the transaction (defaults to chain id from RPC server). - `:gas_price`: (legacy only) max price willing to pay for each gas. - `:gas`: Gas limit for execution of this transaction. - - `:max_fee_per_gas`: (EIP-1559 only) max fee per gas (defaults to 120% current gas price estimate). - - `:max_priority_fee_per_gas`: (EIP-1559 only) max priority fee per gas or validator tip. (defaults to zero) + - `:max_fee_per_gas`: (EIP-1559 only) max fee per gas (estimated with `Ethers.estimate_fees/1` + from recent blocks, falling back to 120% of the current gas price on RPC clients without + `eth_feeHistory` support). + - `:max_priority_fee_per_gas`: (EIP-1559 only) max priority fee per gas or validator tip. + (estimated with `Ethers.estimate_fees/1` from recent blocks, falling back to + `eth_maxPriorityFeePerGas`) - `:nonce`: Nonce of the transaction. (defaults to number of transactions of from address) - `:rpc_client`: The RPC Client to use. It should implement ethereum jsonRPC API. default: Ethereumex.HttpClient - `:rpc_opts`: Extra options to pass to rpc_client. (Like timeout, Server URL, etc.) @@ -845,6 +856,118 @@ defmodule Ethers do end end + @doc """ + Returns the fee history from the RPC API (`eth_feeHistory`). + + Also works with `Ethers.batch/2` as `{:fee_history, [block_count, newest_block, reward_percentiles]}`. + + ## Parameters + - `block_count`: Number of blocks to look back. (positive integer) + - `newest_block`: Highest block of the requested range — a block number (integer) or one + of the block tags `"latest"`, `"earliest"`, `"pending"`, `"safe"` or `"finalized"`. + Hex encoded block numbers are not accepted. (default `"latest"`) + - `reward_percentiles`: List of percentiles (0-100) to sample the priority fees of the + transactions in each block at. When empty, no `:reward` data is returned. (default `[]`) + - `opts`: RPC related options. + + ## Returns + + `{:ok, fee_history}` where fee_history is a map with these keys (quantities decoded to + integers): + + - `:oldest_block`: Number of the first block in the range. + - `:base_fee_per_gas`: Base fee per gas for each block in the range, **plus one extra + entry: the expected base fee of the next block**. + - `:gas_used_ratio`: Gas used ratio (0..1) of each block in the range. + - `:reward`: For each block, the transaction priority fees at each requested percentile. + `nil` (or sometimes `[]`, depending on the node) when `reward_percentiles` is empty. + - `:base_fee_per_blob_gas` / `:blob_gas_used_ratio`: Same as their gas counterparts, for + blob gas (EIP-4844). `nil` when the node does not report them. + + Returns `{:error, :not_supported}` if the configured RPC client does not implement the + optional `eth_fee_history/4` callback. + """ + @spec fee_history(pos_integer(), binary() | non_neg_integer(), [number()], Keyword.t()) :: + {:ok, map()} | {:error, term()} + def fee_history(block_count, newest_block \\ "latest", reward_percentiles \\ [], opts \\ []) do + {rpc_client, rpc_opts} = get_rpc_client(opts) + + with {:ok, [block_count, newest_block, reward_percentiles]} <- + pre_process([block_count, newest_block, reward_percentiles], [], :fee_history, opts) do + if rpc_callback_supported?(rpc_client, :eth_fee_history, 4) do + rpc_client.eth_fee_history(block_count, newest_block, reward_percentiles, rpc_opts) + else + {:error, :not_supported} + end + |> post_process(nil, :fee_history) + end + end + + @doc """ + Same as `Ethers.fee_history/4` but raises on error. + """ + @spec fee_history!(pos_integer(), binary() | non_neg_integer(), [number()], Keyword.t()) :: + map() | no_return() + def fee_history!(block_count, newest_block \\ "latest", reward_percentiles \\ [], opts \\ []) do + case fee_history(block_count, newest_block, reward_percentiles, opts) do + {:ok, fee_history} -> fee_history + {:error, reason} -> raise ExecutionError, reason + end + end + + @doc """ + Estimates EIP-1559 fees (`max_fee_per_gas` and `max_priority_fee_per_gas`) based on + recent blocks using `Ethers.fee_history/4`. + + The priority fee is the median of the recent blocks' priority fees sampled at the + percentile selected by `:speed`, and the max fee adds headroom for base fee growth: + + max_priority_fee_per_gas = median(reward at percentile over recent blocks) + max_fee_per_gas = 2 * next_block_base_fee + max_priority_fee_per_gas + + This is also what transaction auto-fill uses to price EIP-1559 transactions when the fees + are not explicitly provided (with a fallback to the legacy gas price based estimation on + RPC clients without `eth_feeHistory` support). + + ## Options + - `:speed`: `:slow`, `:standard` or `:fast` — samples priority fees at the 25th, 50th or + 75th percentile respectively. Also accepts a raw percentile number (0-100). + (default `:standard`) + - `:block_count`: Number of recent blocks to sample. (default `10`) + - `:rpc_client` / `:rpc_opts`: RPC related options. + + ## Returns + - `{:ok, %{max_fee_per_gas: integer, max_priority_fee_per_gas: integer}}` on success. + - `{:error, :invalid_speed}` if `:speed` is not recognized. + - `{:error, :no_fee_history_data}` if the node returned no usable fee history. + - `{:error, reason}` on RPC failures. + """ + @spec estimate_fees(Keyword.t()) :: + {:ok, + %{max_fee_per_gas: non_neg_integer(), max_priority_fee_per_gas: non_neg_integer()}} + | {:error, term()} + def estimate_fees(opts \\ []) do + block_count = Keyword.get(opts, :block_count, @fee_estimation_block_count) + + with {:ok, percentile} <- fee_percentile(Keyword.get(opts, :speed, :standard)), + {:ok, fee_history} <- fee_history(block_count, "latest", [percentile], opts) do + calculate_fee_estimation(fee_history) + end + end + + @doc """ + Same as `Ethers.estimate_fees/1` but raises on error. + """ + @spec estimate_fees!(Keyword.t()) :: + %{max_fee_per_gas: non_neg_integer(), max_priority_fee_per_gas: non_neg_integer()} + | no_return() + def estimate_fees!(opts \\ []) do + case estimate_fees(opts) do + {:ok, fees} -> fees + {:error, reason} -> raise ExecutionError, reason + end + end + @doc """ Fetches the event logs with the given filter. @@ -1067,6 +1190,25 @@ defmodule Ethers do {:ok, log_params} end + defp pre_process([block_count, newest_block, reward_percentiles], [], :fee_history, _opts) + when is_integer(block_count) and block_count > 0 and is_list(reward_percentiles) do + # Only one representation per input: quantities are integers (hex encoded here for + # the RPC), the newest block is an integer or one of the named block tags + case newest_block do + number when is_integer(number) and number >= 0 -> + {:ok, + [Utils.integer_to_hex(block_count), Utils.integer_to_hex(number), reward_percentiles]} + + tag when tag in @block_tags -> + {:ok, [Utils.integer_to_hex(block_count), tag, reward_percentiles]} + + _other -> + {:error, :invalid_fee_history_params} + end + end + + defp pre_process(_data, [], :fee_history, _opts), do: {:error, :invalid_fee_history_params} + defp pre_process([], [], _action, _opts), do: :ok defp pre_process(data, [], _action, _opts), do: {:ok, data} @@ -1142,6 +1284,20 @@ defmodule Ethers do defp post_process({:ok, nil}, _tx_hash, :get_transaction_receipt), do: {:error, :transaction_receipt_not_found} + defp post_process({:ok, resp}, _data, :fee_history) when is_map(resp) do + {:ok, + %{ + oldest_block: resp |> Map.get("oldestBlock") |> maybe_hex_to_integer(), + base_fee_per_gas: + resp |> Map.get("baseFeePerGas", []) |> Enum.map(&Utils.hex_to_integer!/1), + gas_used_ratio: Map.get(resp, "gasUsedRatio", []), + reward: resp |> Map.get("reward") |> decode_fee_history_rewards(), + base_fee_per_blob_gas: + resp |> Map.get("baseFeePerBlobGas") |> maybe_map(&Utils.hex_to_integer!/1), + blob_gas_used_ratio: Map.get(resp, "blobGasUsedRatio") + }} + end + defp post_process({:ok, result}, _tx_data, _action), do: {:ok, result} @@ -1168,6 +1324,66 @@ defmodule Ethers do defp post_process({:error, cause}, _tx_data, _action), do: {:error, cause} + defp fee_percentile(speed) when is_map_key(@fee_estimation_percentiles, speed), + do: {:ok, Map.fetch!(@fee_estimation_percentiles, speed)} + + defp fee_percentile(percentile) + when is_number(percentile) and percentile >= 0 and percentile <= 100, + do: {:ok, percentile} + + defp fee_percentile(_speed), do: {:error, :invalid_speed} + + defp calculate_fee_estimation(%{base_fee_per_gas: base_fees, reward: rewards}) + when base_fees != [] and is_list(rewards) do + # base_fee_per_gas has one extra entry: the expected base fee of the next block + next_base_fee = List.last(base_fees) + + case rewards |> Enum.flat_map(&List.wrap/1) |> median() do + nil -> + {:error, :no_fee_history_data} + + max_priority_fee_per_gas -> + {:ok, + %{ + # Adds 100% headroom for base fee growth (one doubling) plus the tip + max_fee_per_gas: 2 * next_base_fee + max_priority_fee_per_gas, + max_priority_fee_per_gas: max_priority_fee_per_gas + }} + end + end + + defp calculate_fee_estimation(_fee_history), do: {:error, :no_fee_history_data} + + defp median([]), do: nil + + defp median(values) do + sorted = Enum.sort(values) + count = length(sorted) + middle = div(count, 2) + + if rem(count, 2) == 1 do + Enum.at(sorted, middle) + else + div(Enum.at(sorted, middle - 1) + Enum.at(sorted, middle), 2) + end + end + + defp decode_fee_history_rewards(nil), do: nil + + defp decode_fee_history_rewards(rewards) when is_list(rewards), + do: + Enum.map(rewards, fn block_rewards -> maybe_map(block_rewards, &Utils.hex_to_integer!/1) end) + + defp maybe_map(nil, _fun), do: nil + defp maybe_map(list, fun) when is_list(list), do: Enum.map(list, fun) + + defp maybe_hex_to_integer(nil), do: nil + defp maybe_hex_to_integer(hex), do: Utils.hex_to_integer!(hex) + + defp rpc_callback_supported?(rpc_client, callback, arity) do + Code.ensure_loaded?(rpc_client) and function_exported?(rpc_client, callback, arity) + end + defp ensure_hex_value(params, key) do case Map.get(params, key) do v when is_integer(v) -> %{params | key => Utils.integer_to_hex(v)} diff --git a/lib/ethers/rpc_client/adapter.ex b/lib/ethers/rpc_client/adapter.ex index fc0e3d42..ce6a5230 100644 --- a/lib/ethers/rpc_client/adapter.ex +++ b/lib/ethers/rpc_client/adapter.ex @@ -14,6 +14,13 @@ defmodule Ethers.RpcClient.Adapter do @callback eth_estimate_gas(map(), keyword()) :: {:ok, binary()} | error() + @callback eth_fee_history( + block_count :: binary(), + newest_block :: binary(), + reward_percentiles :: [number()], + keyword() + ) :: {:ok, map()} | error() + @callback eth_gas_price(keyword()) :: {:ok, binary()} | error() @callback eth_get_balance(binary(), binary(), keyword()) :: {:ok, binary()} | error() @@ -36,4 +43,9 @@ defmodule Ethers.RpcClient.Adapter do @callback eth_send_transaction(map(), keyword()) :: {:ok, binary()} | error() @callback eth_send_raw_transaction(binary(), keyword()) :: {:ok, binary()} | error() + + # Optional to keep custom RPC client adapters backwards compatible. Ethers checks + # availability with `function_exported?/3` and falls back to the legacy gas price + # based fee estimation when the adapter does not implement it. + @optional_callbacks eth_fee_history: 4 end diff --git a/lib/ethers/transaction.ex b/lib/ethers/transaction.ex index 66b19dff..46a29733 100644 --- a/lib/ethers/transaction.ex +++ b/lib/ethers/transaction.ex @@ -42,6 +42,9 @@ defmodule Ethers.Transaction do # Margin precision is 0.01% (12345 = 123.45%) @margin_precision 10_000 + # Fields auto-filled through Ethers.estimate_fees/1 when the RPC client supports it + @fee_estimation_fields [:max_fee_per_gas, :max_priority_fee_per_gas] + @typedoc """ EVM Transaction type """ @@ -116,6 +119,11 @@ defmodule Ethers.Transaction do @doc """ Fills missing transaction fields with default values from the network based on transaction type. + Missing `max_fee_per_gas` and `max_priority_fee_per_gas` values are estimated from recent + blocks with `Ethers.estimate_fees/1` (`eth_feeHistory`). When the RPC client does not + support `eth_feeHistory` (or the node returns no usable data), they fall back to the + legacy estimation based on `eth_gasPrice` and `eth_maxPriorityFeePerGas`. + ## Parameters - `params` - Updated Transaction params - `opts` - Options to pass to the RPC client @@ -128,9 +136,12 @@ defmodule Ethers.Transaction do def add_auto_fetchable_fields(params, opts) do params = Map.put_new(params, :type, @default_transaction_type) + missing_keys = Enum.reject(params.type.auto_fetchable_fields(), &Map.get(params, &1)) + + {params, missing_keys} = maybe_estimate_fees(params, missing_keys, opts) + {keys, actions} = - params.type.auto_fetchable_fields() - |> Enum.reject(&Map.get(params, &1)) + missing_keys |> Enum.map(&{&1, fill_action(&1, params)}) |> Enum.unzip() @@ -146,6 +157,26 @@ defmodule Ethers.Transaction do end end + defp maybe_estimate_fees(params, missing_keys, opts) do + missing_fee_keys = Enum.filter(missing_keys, &(&1 in @fee_estimation_fields)) + + with [_ | _] <- missing_fee_keys, + true <- fee_history_supported?(opts), + {:ok, fees} <- Ethers.estimate_fees(opts) do + {Map.merge(params, Map.take(fees, missing_fee_keys)), missing_keys -- missing_fee_keys} + else + # Nothing to estimate or no eth_feeHistory support - fall back to the legacy + # gas price based fill actions for any missing fee field + _no_estimation -> {params, missing_keys} + end + end + + defp fee_history_supported?(opts) do + {rpc_client, _rpc_opts} = Ethers.get_rpc_client(opts) + + Code.ensure_loaded?(rpc_client) and function_exported?(rpc_client, :eth_fee_history, 4) + end + @doc """ Encodes a transaction for network transmission following EIP-155/EIP-1559. diff --git a/test/ethers/fee_estimation_test.exs b/test/ethers/fee_estimation_test.exs new file mode 100644 index 00000000..e931d1d0 --- /dev/null +++ b/test/ethers/fee_estimation_test.exs @@ -0,0 +1,208 @@ +defmodule Ethers.FeeEstimationTest.FeeHistoryRPC do + @moduledoc false + # Returns a fixed eth_feeHistory response so the estimation math can be verified. + # Next block base fee (last entry) is 0xa0 = 160, rewards have a median of 0x3 = 3. + + def eth_fee_history(block_count, newest_block, [percentile], opts) do + if pid = opts[:send_params_to_pid] do + send(pid, {:fee_history_params, block_count, newest_block, percentile}) + end + + reward = + if opts[:empty_rewards] do + [[], [], [], [], []] + else + [["0x1"], ["0x5"], ["0x3"], ["0x2"], ["0x4"]] + end + + {:ok, + %{ + "oldestBlock" => "0x1", + "baseFeePerGas" => ["0x64", "0x6e", "0x78", "0x82", "0x8c", "0xa0"], + "gasUsedRatio" => [0.1, 0.2, 0.3, 0.4, 0.5], + "reward" => reward + }} + end +end + +defmodule Ethers.FeeEstimationTest.LegacyOnlyRPC do + @moduledoc false + # An RPC client without eth_feeHistory support - auto-fill must fall back to the + # legacy gas price based estimation. + + def batch_request(requests, _opts) do + {:ok, + Enum.map(requests, fn + {:eth_gas_price, []} -> {:ok, "0x100"} + {:eth_max_priority_fee_per_gas, []} -> {:ok, "0x10"} + end)} + end +end + +defmodule Ethers.FeeEstimationTest do + use ExUnit.Case + + alias Ethers.FeeEstimationTest.FeeHistoryRPC + alias Ethers.FeeEstimationTest.LegacyOnlyRPC + alias Ethers.Transaction + alias Ethers.Transaction.Eip1559 + + @from "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + @to "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc" + + describe "fee_history/4" do + test "returns decoded fee history" do + assert {:ok, fee_history} = Ethers.fee_history(3, "latest", [50]) + + assert is_integer(fee_history.oldest_block) + assert Enum.all?(fee_history.base_fee_per_gas, &is_integer/1) + # One extra entry: the expected base fee of the next block + assert length(fee_history.base_fee_per_gas) == length(fee_history.gas_used_ratio) + 1 + assert Enum.all?(fee_history.reward, fn [reward] -> is_integer(reward) end) + end + + test "returns no reward data when no percentiles are requested" do + assert {:ok, %{reward: reward}} = Ethers.fee_history(2) + + # Geth omits the reward field entirely, anvil returns an empty list + assert reward in [nil, []] + end + + test "accepts an integer newest block" do + {:ok, block_number} = Ethers.current_block_number() + + assert {:ok, %{oldest_block: oldest_block}} = Ethers.fee_history(2, block_number, [50]) + assert oldest_block == block_number - 1 + end + + test "works in batch requests" do + assert {:ok, [{:ok, %{base_fee_per_gas: [_ | _]}}, {:ok, gas_price}]} = + Ethers.batch([{:fee_history, [2, "latest", [50]]}, :current_gas_price]) + + assert is_integer(gas_price) + end + + test "returns error for invalid params" do + assert {:error, :invalid_fee_history_params} = Ethers.fee_history(2, "latest", 50) + assert {:error, :invalid_fee_history_params} = Ethers.fee_history(0, "latest", [50]) + end + + test "returns error when the RPC client does not support eth_feeHistory" do + assert {:error, :not_supported} = + Ethers.fee_history(2, "latest", [50], rpc_client: Ethers.TestRPCModule) + + # estimate_fees propagates the error instead of raising + assert {:error, :not_supported} = Ethers.estimate_fees(rpc_client: Ethers.TestRPCModule) + end + + test "accepts only one representation per input - no hex encoded quantities" do + # Quantities are integers and the newest block is an integer or a named block tag. + # Hex encoded strings are rejected instead of being passed through. + assert {:error, :invalid_fee_history_params} = Ethers.fee_history("0x2", "latest", [50]) + assert {:error, :invalid_fee_history_params} = Ethers.fee_history(2, "0x10", [50]) + assert {:error, :invalid_fee_history_params} = Ethers.fee_history(2, "newest", [50]) + end + + test "bang version returns unwrapped value and raises on error" do + assert %{base_fee_per_gas: [_ | _]} = Ethers.fee_history!(2, "latest", [50]) + + assert_raise Ethers.ExecutionError, fn -> + Ethers.fee_history!(2, "latest", 50) + end + end + end + + describe "estimate_fees/1" do + test "estimates max fee and max priority fee from fee history" do + # median(1, 5, 3, 2, 4) = 3 and next block base fee = 160: + # max_fee_per_gas = 2 * 160 + 3 + assert {:ok, %{max_fee_per_gas: 323, max_priority_fee_per_gas: 3}} == + Ethers.estimate_fees(rpc_client: FeeHistoryRPC) + end + + test "samples the percentile matching the requested speed" do + opts = [rpc_client: FeeHistoryRPC, rpc_opts: [send_params_to_pid: self()]] + + {:ok, _fees} = Ethers.estimate_fees(opts) + assert_received {:fee_history_params, "0xA", "latest", 50} + + {:ok, _fees} = Ethers.estimate_fees([speed: :slow] ++ opts) + assert_received {:fee_history_params, "0xA", "latest", 25} + + {:ok, _fees} = Ethers.estimate_fees([speed: :fast, block_count: 4] ++ opts) + assert_received {:fee_history_params, "0x4", "latest", 75} + + {:ok, _fees} = Ethers.estimate_fees([speed: 90] ++ opts) + assert_received {:fee_history_params, "0xA", "latest", 90} + end + + test "returns error for invalid speed" do + assert {:error, :invalid_speed} = Ethers.estimate_fees(speed: :warp) + assert {:error, :invalid_speed} = Ethers.estimate_fees(speed: 101) + end + + test "returns error when the node reports no rewards" do + assert {:error, :no_fee_history_data} = + Ethers.estimate_fees(rpc_client: FeeHistoryRPC, rpc_opts: [empty_rewards: true]) + end + + test "works against a real node" do + assert {:ok, %{max_fee_per_gas: max_fee, max_priority_fee_per_gas: max_priority_fee}} = + Ethers.estimate_fees() + + assert is_integer(max_fee) and is_integer(max_priority_fee) + assert max_fee > max_priority_fee + end + + test "bang version returns unwrapped value and raises on error" do + assert %{max_fee_per_gas: 323} = Ethers.estimate_fees!(rpc_client: FeeHistoryRPC) + + assert_raise Ethers.ExecutionError, fn -> + Ethers.estimate_fees!(speed: :warp) + end + end + end + + describe "transaction auto-fill" do + test "fills missing fees from fee history" do + params = %{type: Eip1559, chain_id: 1, nonce: 1, gas: 21_000} + + assert {:ok, filled} = + Transaction.add_auto_fetchable_fields(params, rpc_client: FeeHistoryRPC) + + assert %{max_fee_per_gas: 323, max_priority_fee_per_gas: 3} = filled + end + + test "only fills the missing fee fields" do + params = %{type: Eip1559, chain_id: 1, nonce: 1, gas: 21_000, max_priority_fee_per_gas: 7} + + assert {:ok, filled} = + Transaction.add_auto_fetchable_fields(params, rpc_client: FeeHistoryRPC) + + assert %{max_fee_per_gas: 323, max_priority_fee_per_gas: 7} = filled + end + + test "falls back to gas price estimation without eth_feeHistory support" do + params = %{type: Eip1559, chain_id: 1, nonce: 1, gas: 21_000} + + assert {:ok, filled} = + Transaction.add_auto_fetchable_fields(params, rpc_client: LegacyOnlyRPC) + + # 120% margin over the 0x100 gas price, and the plain 0x10 priority fee + assert %{max_fee_per_gas: 307, max_priority_fee_per_gas: 16} = filled + end + + test "fills all auto-fetchable fields against a real node" do + params = %{type: Eip1559, from: @from, to: @to, value: 1} + + assert {:ok, filled} = Transaction.add_auto_fetchable_fields(params, []) + + assert is_integer(filled.chain_id) + assert is_integer(filled.nonce) + assert is_integer(filled.gas) + assert is_integer(filled.max_fee_per_gas) + assert is_integer(filled.max_priority_fee_per_gas) + assert filled.max_fee_per_gas > filled.max_priority_fee_per_gas + end + end +end