diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index f9ccc80da2..116662c12b 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -2125,6 +2125,26 @@ defmodule Explorer.Chain do |> Repo.all() end + @doc """ + Finds replaced/dropped transactions and sets their status to `:error` with `dropped/replaced` error. + """ + @spec update_replaced_transactions(:infinity | non_neg_integer()) :: {integer(), nil | [term()]} + def update_replaced_transactions(timeout \\ :infinity) do + query = + from(transaction in Transaction, + where: is_nil(transaction.block_number), + join: mined_transaction in Transaction, + where: + transaction.from_address_hash == mined_transaction.from_address_hash and + transaction.nonce == mined_transaction.nonce and not is_nil(mined_transaction.block_number), + update: [ + set: [status: ^:error, error: "dropped/replaced"] + ] + ) + + Repo.update_all(query, [], timeout: timeout) + end + @doc """ Update a new `t:Token.t/0` record. diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 73fbc8bd57..738a1e4443 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -937,6 +937,60 @@ defmodule Explorer.ChainTest do end end + describe "update_replaced_transactions" do + test "finds and updates replaced transaction" do + common_from_address_hash = %Explorer.Chain.Hash{ + byte_count: 20, + bytes: <<0x4615CC10092B514258577DAFCA98C142577F1578::big-integer-size(20)-unit(8)>> + } + + address = insert(:address, hash: common_from_address_hash) + + common_nonce = 10 + + replaced_transaction_hash = %Explorer.Chain.Hash{ + byte_count: 32, + bytes: <<0x9FC76417374AA880D4449A1F7F31EC597F00B1F6F3DD2D66F4C9C6C445836D8B::big-integer-size(32)-unit(8)>> + } + + insert(:transaction, + hash: replaced_transaction_hash, + nonce: common_nonce, + from_address: address + ) + + mined_transaction_hash = %Explorer.Chain.Hash{ + byte_count: 32, + bytes: <<0x8FC76417374AA880D4449A1F7F31EC597F00B1F6F3DD2D66F4C9C6C445836D8B::big-integer-size(32)-unit(8)>> + } + + block = insert(:block) + + insert(:transaction, + hash: mined_transaction_hash, + nonce: common_nonce, + from_address: address, + block_number: block.number, + block_hash: block.hash, + cumulative_gas_used: 10, + gas_used: 1, + index: 0, + status: :ok + ) + + {1, nil} = Explorer.Chain.update_replaced_transactions() + + found_replaced_transaction = + Repo.one!( + from(transaction in Transaction, + where: transaction.status == ^:error and transaction.error == "dropped/replaced" + ) + ) + + assert found_replaced_transaction.hash == replaced_transaction_hash + end + end + # Full tests in `test/explorer/import_test.exs` describe "import/1" do @import_data %{ diff --git a/apps/indexer/lib/indexer/replaced_transaction/fetcher.ex b/apps/indexer/lib/indexer/replaced_transaction/fetcher.ex index 4a0b8f433c..679d4d8c43 100644 --- a/apps/indexer/lib/indexer/replaced_transaction/fetcher.ex +++ b/apps/indexer/lib/indexer/replaced_transaction/fetcher.ex @@ -6,14 +6,12 @@ defmodule Indexer.ReplacedTransaction.Fetcher do This fetcher finds these transaction and sets them `failed` status with `dropped/replaced` error. """ + use GenServer require Logger - import Ecto.Query, only: [from: 2] - - alias Explorer.Chain.Transaction - alias Explorer.Repo + alias Explorer.Chain alias Indexer.ReplacedTransaction # 1 second @@ -23,6 +21,7 @@ defmodule Indexer.ReplacedTransaction.Fetcher do @query_timeout 60_000 defstruct interval: @default_interval, + query_timeout: @query_timeout, task: nil def child_spec([init_arguments]) do @@ -53,7 +52,8 @@ defmodule Indexer.ReplacedTransaction.Fetcher do state = %__MODULE__{ - interval: opts[:replaced_transaction_interval] || @default_interval + interval: opts[:replaced_transaction_interval] || @default_interval, + query_timeout: opts[:replaced_transaction_query_timeout] || @query_timeout, } |> schedule_find() @@ -62,7 +62,7 @@ defmodule Indexer.ReplacedTransaction.Fetcher do @impl GenServer def handle_info(:find, %__MODULE__{} = state) do - task = Task.Supervisor.async_nolink(ReplacedTransaction.TaskSupervisor, fn -> task() end) + task = Task.Supervisor.async_nolink(ReplacedTransaction.TaskSupervisor, fn -> task(state) end) {:noreply, %__MODULE__{state | task: task}} end @@ -86,23 +86,11 @@ defmodule Indexer.ReplacedTransaction.Fetcher do %__MODULE__{state | task: nil} end - defp task do + defp task(%__MODULE__{query_timeout: query_timeout}) do Logger.metadata(fetcher: :replaced_transaction) - query = - from(transaction in Transaction, - where: is_nil(transaction.block_number), - join: mined_transaction in Transaction, - where: - transaction.from_address_hash == mined_transaction.from_address_hash and - transaction.nonce == mined_transaction.nonce and not is_nil(mined_transaction.block_number), - update: [ - set: [status: ^:error, error: "dropped/replaced"] - ] - ) - try do - Repo.update_all(query, [], timeout: @query_timeout) + Chain.update_replaced_transactions(query_timeout) rescue error -> Logger.error(fn -> ["Failed to make pending transactions dropped: ", inspect(error)] end)