diff --git a/CHANGELOG.md b/CHANGELOG.md index c4cf82d576..d2c189a001 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ ### Features +- [#9189](https://github.com/blockscout/blockscout/pull/9189) - User operations in the search - [#9169](https://github.com/blockscout/blockscout/pull/9169) - Add bridged tokens functionality to master branch - [#9158](https://github.com/blockscout/blockscout/pull/9158) - Increase shared memory for PostgreSQL containers - [#9155](https://github.com/blockscout/blockscout/pull/9155) - Allow bypassing avg block time in proxy implementation re-fetch ttl calculation diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index a0798ff247..5df8512461 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -15,6 +15,8 @@ defmodule BlockScoutWeb.Chain do token_contract_address_from_token_name: 1 ] + alias Explorer.Chain.UserOperation + import Explorer.Helper, only: [parse_integer: 1] alias Ecto.Association.NotLoaded @@ -54,7 +56,7 @@ defmodule BlockScoutWeb.Chain do @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} @address_hash_len 40 - @tx_block_hash_len 64 + @tx_block_op_hash_len 64 def default_paging_options do @default_paging_options @@ -80,20 +82,21 @@ defmodule BlockScoutWeb.Chain do end end - @spec from_param(String.t()) :: {:ok, Address.t() | Block.t() | Transaction.t()} | {:error, :not_found} + @spec from_param(String.t()) :: + {:ok, Address.t() | Block.t() | Transaction.t() | UserOperation.t()} | {:error, :not_found} def from_param(param) def from_param("0x" <> number_string = param) when byte_size(number_string) == @address_hash_len, do: address_from_param(param) - def from_param("0x" <> number_string = param) when byte_size(number_string) == @tx_block_hash_len, - do: block_or_transaction_from_param(param) + def from_param("0x" <> number_string = param) when byte_size(number_string) == @tx_block_op_hash_len, + do: block_or_transaction_or_operation_from_param(param) def from_param(param) when byte_size(param) == @address_hash_len, do: address_from_param("0x" <> param) - def from_param(param) when byte_size(param) == @tx_block_hash_len, - do: block_or_transaction_from_param("0x" <> param) + def from_param(param) when byte_size(param) == @tx_block_op_hash_len, + do: block_or_transaction_or_operation_from_param("0x" <> param) def from_param(string) when is_binary(string) do case param_to_block_number(string) do @@ -670,9 +673,9 @@ defmodule BlockScoutWeb.Chain do %{"fiat_value" => ctb.fiat_value, "value" => value, "id" => id} end - defp block_or_transaction_from_param(param) do + defp block_or_transaction_or_operation_from_param(param) do with {:error, :not_found} <- transaction_from_param(param) do - hash_string_to_block(param) + hash_string_to_block_or_operation(param) end end @@ -686,13 +689,15 @@ defmodule BlockScoutWeb.Chain do end end - defp hash_string_to_block(hash_string) do - case string_to_block_hash(hash_string) do - {:ok, hash} -> - hash_to_block(hash) - - :error -> - {:error, :not_found} + defp hash_string_to_block_or_operation(hash_string) do + with {:ok, hash} <- string_to_block_hash(hash_string), + {:error, :not_found} <- hash_to_block(hash), + {:user_operations_enabled, true} <- {:user_operations_enabled, UserOperation.user_operations_enabled?()} do + UserOperation.hash_to_user_operation(hash) + else + {:user_operations_enabled, false} -> {:error, :not_found} + :error -> {:error, :not_found} + res -> res end end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex index 0b48cedab8..aa4d3cbf89 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex @@ -80,6 +80,8 @@ <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: "0x" <> Base.encode16(@result.tx_hash, case: :lower) %> + <% "user_operation" -> %> + <%= "0x" <> Base.encode16(@result.user_operation_hash, case: :lower) %> <% "block" -> %> <%= link( "0x" <> Base.encode16(@result.block_hash, case: :lower), diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex index b56c673520..05a8b95eb0 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.API.V2.SearchView do alias BlockScoutWeb.{BlockView, Endpoint} alias Explorer.Chain - alias Explorer.Chain.{Address, Block, Hash, Transaction} + alias Explorer.Chain.{Address, Block, Hash, Transaction, UserOperation} def render("search_results.json", %{search_results: search_results, next_page_params: next_page_params}) do %{"items" => Enum.map(search_results, &prepare_search_result/1), "next_page_params" => next_page_params} @@ -84,6 +84,16 @@ defmodule BlockScoutWeb.API.V2.SearchView do } end + def prepare_search_result(%{type: "user_operation"} = search_result) do + user_operation_hash = hash_to_string(search_result.user_operation_hash) + + %{ + "type" => search_result.type, + "user_operation_hash" => user_operation_hash, + "timestamp" => search_result.timestamp + } + end + defp hash_to_string(%Hash{bytes: bytes}), do: hash_to_string(bytes) defp hash_to_string(hash), do: "0x" <> Base.encode16(hash, case: :lower) @@ -106,4 +116,8 @@ defmodule BlockScoutWeb.API.V2.SearchView do defp redirect_search_results(%Transaction{} = item) do %{"type" => "transaction", "parameter" => to_string(item.hash)} end + + defp redirect_search_results(%UserOperation{} = item) do + %{"type" => "user_operation", "parameter" => to_string(item.hash)} + end end diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index 20082bccbf..ebfc4baf47 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -24,7 +24,8 @@ defmodule Explorer.Chain.Search do DenormalizationHelper, SmartContract, Token, - Transaction + Transaction, + UserOperation } @doc """ @@ -39,38 +40,7 @@ defmodule Explorer.Chain.Search do result = case prepare_search_term(string) do {:some, term} -> - tokens_query = search_token_query(string, term) - contracts_query = search_contract_query(term) - labels_query = search_label_query(term) - tx_query = search_tx_query(string) - address_query = search_address_query(string) - block_query = search_block_query(string) - - basic_query = - from( - tokens in subquery(tokens_query), - union: ^contracts_query, - union: ^labels_query - ) - - query = - cond do - address_query -> - basic_query - |> union(^address_query) - - tx_query -> - basic_query - |> union(^tx_query) - |> union(^block_query) - - block_query -> - basic_query - |> union(^block_query) - - true -> - basic_query - end + query = base_joint_query(string, term) ordered_query = from(items in subquery(query), @@ -109,6 +79,50 @@ defmodule Explorer.Chain.Search do result ++ ens_result end + def base_joint_query(string, term) do + tokens_query = search_token_query(string, term) + contracts_query = search_contract_query(term) + labels_query = search_label_query(term) + address_query = search_address_query(string) + block_query = search_block_query(string) + + basic_query = + from( + tokens in subquery(tokens_query), + union: ^contracts_query, + union: ^labels_query + ) + + cond do + address_query -> + basic_query + |> union(^address_query) + + valid_full_hash?(string) -> + tx_query = search_tx_query(string) + + if UserOperation.user_operations_enabled?() do + user_operation_query = search_user_operation_query(string) + + basic_query + |> union(^tx_query) + |> union(^user_operation_query) + |> union(^block_query) + else + basic_query + |> union(^tx_query) + |> union(^block_query) + end + + block_query -> + basic_query + |> union(^block_query) + + true -> + basic_query + end + end + defp maybe_run_ens_task(%PagingOptions{key: nil}, query_string, options) do Task.async(fn -> search_ens_name(query_string, options) end) end @@ -158,8 +172,18 @@ defmodule Explorer.Chain.Search do |> select_repo(options).all() tx_result = - if query = search_tx_query(search_query) do - query + if valid_full_hash?(search_query) do + search_query + |> search_tx_query() + |> select_repo(options).all() + else + [] + end + + op_result = + if valid_full_hash?(search_query) && UserOperation.user_operations_enabled?() do + search_query + |> search_user_operation_query() |> select_repo(options).all() else [] @@ -185,7 +209,16 @@ defmodule Explorer.Chain.Search do ens_result = await_ens_task(ens_task) non_empty_lists = - [tokens_result, contracts_result, labels_result, tx_result, address_result, blocks_result, ens_result] + [ + tokens_result, + contracts_result, + labels_result, + tx_result, + op_result, + address_result, + blocks_result, + ens_result + ] |> Enum.filter(fn list -> Enum.count(list) > 0 end) |> Enum.sort_by(fn list -> Enum.count(list) end, :asc) @@ -232,26 +265,14 @@ defmodule Explorer.Chain.Search do end defp search_label_query(term) do - label_search_fields = %{ - address_hash: dynamic([att, _, _], att.address_hash), - tx_hash: dynamic([_, _, _], type(^nil, :binary)), - block_hash: dynamic([_, _, _], type(^nil, :binary)), - type: "label", - name: dynamic([_, at, _], at.display_name), - symbol: nil, - holder_count: nil, - inserted_at: dynamic([att, _, _], att.inserted_at), - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: dynamic([_, _, _], type(^nil, :utc_datetime_usec)), - verified: dynamic([_, _, smart_contract], not is_nil(smart_contract)), - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 1, - is_verified_via_admin_panel: nil - } + label_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([att, _, _], att.address_hash)) + |> Map.put(:type, "label") + |> Map.put(:name, dynamic([_, at, _], at.display_name)) + |> Map.put(:inserted_at, dynamic([att, _, _], att.inserted_at)) + |> Map.put(:verified, dynamic([_, _, smart_contract], not is_nil(smart_contract))) + |> Map.put(:priority, 1) inner_query = from(tag in AddressTag, @@ -269,26 +290,21 @@ defmodule Explorer.Chain.Search do end defp search_token_query(string, term) do - token_search_fields = %{ - address_hash: dynamic([token, _], token.contract_address_hash), - tx_hash: dynamic([_, _], type(^nil, :binary)), - block_hash: dynamic([_, _], type(^nil, :binary)), - type: "token", - name: dynamic([token, _], token.name), - symbol: dynamic([token, _], token.symbol), - holder_count: dynamic([token, _], token.holder_count), - inserted_at: dynamic([token, _], token.inserted_at), - block_number: 0, - icon_url: dynamic([token, _], token.icon_url), - token_type: dynamic([token, _], token.type), - timestamp: dynamic([_, _], type(^nil, :utc_datetime_usec)), - verified: dynamic([_, smart_contract], not is_nil(smart_contract)), - exchange_rate: dynamic([token, _], token.fiat_value), - total_supply: dynamic([token, _], token.total_supply), - circulating_market_cap: dynamic([token, _], token.circulating_market_cap), - priority: 0, - is_verified_via_admin_panel: dynamic([token, _], token.is_verified_via_admin_panel) - } + token_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([token, _], token.contract_address_hash)) + |> Map.put(:type, "token") + |> Map.put(:name, dynamic([token, _], token.name)) + |> Map.put(:symbol, dynamic([token, _], token.symbol)) + |> Map.put(:holder_count, dynamic([token, _], token.holder_count)) + |> Map.put(:inserted_at, dynamic([token, _], token.inserted_at)) + |> Map.put(:icon_url, dynamic([token, _], token.icon_url)) + |> Map.put(:token_type, dynamic([token, _], token.type)) + |> Map.put(:verified, dynamic([_, smart_contract], not is_nil(smart_contract))) + |> Map.put(:exchange_rate, dynamic([token, _], token.fiat_value)) + |> Map.put(:total_supply, dynamic([token, _], token.total_supply)) + |> Map.put(:circulating_market_cap, dynamic([token, _], token.circulating_market_cap)) + |> Map.put(:is_verified_via_admin_panel, dynamic([token, _], token.is_verified_via_admin_panel)) case Chain.string_to_address_hash(string) do {:ok, address_hash} -> @@ -310,26 +326,13 @@ defmodule Explorer.Chain.Search do end defp search_contract_query(term) do - contract_search_fields = %{ - address_hash: dynamic([smart_contract, _], smart_contract.address_hash), - tx_hash: dynamic([_, _], type(^nil, :binary)), - block_hash: dynamic([_, _], type(^nil, :binary)), - type: "contract", - name: dynamic([smart_contract, _], smart_contract.name), - symbol: nil, - holder_count: nil, - inserted_at: dynamic([_, address], address.inserted_at), - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: dynamic([_, _], type(^nil, :utc_datetime_usec)), - verified: true, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } + contract_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([smart_contract, _], smart_contract.address_hash)) + |> Map.put(:type, "contract") + |> Map.put(:name, dynamic([smart_contract, _], smart_contract.name)) + |> Map.put(:inserted_at, dynamic([_, address], address.inserted_at)) + |> Map.put(:verified, true) from(smart_contract in SmartContract, left_join: address in Address, @@ -342,26 +345,13 @@ defmodule Explorer.Chain.Search do defp search_address_query(term) do case Chain.string_to_address_hash(term) do {:ok, address_hash} -> - address_search_fields = %{ - address_hash: dynamic([address, _], address.hash), - block_hash: dynamic([_, _], type(^nil, :binary)), - tx_hash: dynamic([_, _], type(^nil, :binary)), - type: "address", - name: dynamic([_, address_name], address_name.name), - symbol: nil, - holder_count: nil, - inserted_at: dynamic([address, _], address.inserted_at), - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: dynamic([_, _], type(^nil, :utc_datetime_usec)), - verified: dynamic([address, _], address.verified), - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } + address_search_fields = + search_fields() + |> Map.put(:address_hash, dynamic([address, _], address.hash)) + |> Map.put(:type, "address") + |> Map.put(:name, dynamic([_, address_name], address_name.name)) + |> Map.put(:inserted_at, dynamic([_, address_name], address_name.inserted_at)) + |> Map.put(:verified, dynamic([address, _], address.verified)) from(address in Address, left_join: @@ -382,97 +372,73 @@ defmodule Explorer.Chain.Search do end end + defp valid_full_hash?(string_input) do + case Chain.string_to_transaction_hash(string_input) do + {:ok, _tx_hash} -> true + _ -> false + end + end + defp search_tx_query(term) do if DenormalizationHelper.denormalization_finished?() do - case Chain.string_to_transaction_hash(term) do - {:ok, tx_hash} -> - transaction_search_fields = %{ - address_hash: dynamic([_], type(^nil, :binary)), - tx_hash: dynamic([transaction], transaction.hash), - block_hash: dynamic([_], type(^nil, :binary)), - type: "transaction", - name: nil, - symbol: nil, - holder_count: nil, - inserted_at: dynamic([transaction], transaction.inserted_at), - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: dynamic([transaction], transaction.block_timestamp), - verified: nil, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } - - from(transaction in Transaction, - where: transaction.hash == ^tx_hash, - select: ^transaction_search_fields - ) - - _ -> - nil - end + transaction_search_fields = + search_fields() + |> Map.put(:tx_hash, dynamic([transaction], transaction.hash)) + |> Map.put(:block_hash, dynamic([transaction], transaction.block_hash)) + |> Map.put(:type, "transaction") + |> Map.put(:block_number, dynamic([transaction], transaction.block_number)) + |> Map.put(:inserted_at, dynamic([transaction], transaction.inserted_at)) + |> Map.put(:timestamp, dynamic([transaction], transaction.block_timestamp)) + + from(transaction in Transaction, + where: transaction.hash == ^term, + select: ^transaction_search_fields + ) else - case Chain.string_to_transaction_hash(term) do - {:ok, tx_hash} -> - transaction_search_fields = %{ - address_hash: dynamic([_, _], type(^nil, :binary)), - tx_hash: dynamic([transaction, _], transaction.hash), - block_hash: dynamic([_, _], type(^nil, :binary)), - type: "transaction", - name: nil, - symbol: nil, - holder_count: nil, - inserted_at: dynamic([transaction, _], transaction.inserted_at), - block_number: 0, - icon_url: nil, - token_type: nil, - timestamp: dynamic([_, block], block.timestamp), - verified: nil, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } - - from(transaction in Transaction, - left_join: block in Block, - on: transaction.block_hash == block.hash, - where: transaction.hash == ^tx_hash, - select: ^transaction_search_fields - ) - - _ -> - nil - end + transaction_search_fields = + search_fields() + |> Map.put(:tx_hash, dynamic([transaction, _], transaction.hash)) + |> Map.put(:block_hash, dynamic([transaction, _], transaction.block_hash)) + |> Map.put(:type, "transaction") + |> Map.put(:block_number, dynamic([transaction, _], transaction.block_number)) + |> Map.put(:inserted_at, dynamic([transaction, _], transaction.inserted_at)) + |> Map.put(:timestamp, dynamic([_, block], block.timestamp)) + + from(transaction in Transaction, + left_join: block in Block, + on: transaction.block_hash == block.hash, + where: transaction.hash == ^term, + select: ^transaction_search_fields + ) end end + defp search_user_operation_query(term) do + user_operation_search_fields = + search_fields() + |> Map.put(:user_operation_hash, dynamic([user_operation, _], user_operation.hash)) + |> Map.put(:block_hash, dynamic([user_operation, _], user_operation.block_hash)) + |> Map.put(:type, "user_operation") + |> Map.put(:inserted_at, dynamic([user_operation, _], user_operation.inserted_at)) + |> Map.put(:block_number, dynamic([user_operation, _], user_operation.block_number)) + |> Map.put(:timestamp, dynamic([_, block], block.timestamp)) + + from(user_operation in UserOperation, + left_join: block in Block, + on: user_operation.block_hash == block.hash, + where: user_operation.hash == ^term, + select: ^user_operation_search_fields + ) + end + defp search_block_query(term) do - block_search_fields = %{ - address_hash: dynamic([_], type(^nil, :binary)), - tx_hash: dynamic([_], type(^nil, :binary)), - block_hash: dynamic([block], block.hash), - type: "block", - name: nil, - symbol: nil, - holder_count: nil, - inserted_at: dynamic([block], block.inserted_at), - block_number: dynamic([block], block.number), - icon_url: nil, - token_type: nil, - timestamp: dynamic([block], block.timestamp), - verified: nil, - exchange_rate: nil, - total_supply: nil, - circulating_market_cap: nil, - priority: 0, - is_verified_via_admin_panel: nil - } + block_search_fields = + search_fields() + |> Map.put(:block_hash, dynamic([block], block.hash)) + |> Map.put(:type, "block") + |> Map.put(:block_number, dynamic([block], block.number)) + |> Map.put(:inserted_at, dynamic([block], block.inserted_at)) + |> Map.put(:timestamp, dynamic([block], block.timestamp)) case Chain.string_to_block_hash(term) do {:ok, block_hash} -> @@ -605,11 +571,23 @@ defmodule Explorer.Chain.Search do end defp merge_address_search_result_with_ens_info([], ens_info) do + search_fields() + |> Map.put(:address_hash, ens_info[:address_hash]) + |> Map.put(:type, "address") + |> Map.put(:ens_info, ens_info) + end + + defp merge_address_search_result_with_ens_info([address], ens_info) do + Map.put(address |> compose_result_checksummed_address_hash(), :ens_info, ens_info) + end + + defp search_fields do %{ - address_hash: ens_info[:address_hash], - block_hash: nil, - tx_hash: nil, - type: "address", + address_hash: dynamic([_], type(^nil, :binary)), + tx_hash: dynamic([_], type(^nil, :binary)), + user_operation_hash: dynamic([_], type(^nil, :binary)), + block_hash: dynamic([_], type(^nil, :binary)), + type: nil, name: nil, symbol: nil, holder_count: nil, @@ -617,18 +595,13 @@ defmodule Explorer.Chain.Search do block_number: 0, icon_url: nil, token_type: nil, - timestamp: nil, - verified: false, + timestamp: dynamic([_, _], type(^nil, :utc_datetime_usec)), + verified: nil, exchange_rate: nil, total_supply: nil, circulating_market_cap: nil, priority: 0, - is_verified_via_admin_panel: nil, - ens_info: ens_info + is_verified_via_admin_panel: nil } end - - defp merge_address_search_result_with_ens_info([address], ens_info) do - Map.put(address |> compose_result_checksummed_address_hash(), :ens_info, ens_info) - end end diff --git a/apps/explorer/lib/explorer/chain/smart_contract_additional_sources.ex b/apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex similarity index 100% rename from apps/explorer/lib/explorer/chain/smart_contract_additional_sources.ex rename to apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex diff --git a/apps/explorer/lib/explorer/chain/user_operation.ex b/apps/explorer/lib/explorer/chain/user_operation.ex new file mode 100644 index 0000000000..75300f6ea5 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/user_operation.ex @@ -0,0 +1,73 @@ +defmodule Explorer.Chain.UserOperation do + @moduledoc """ + The representation of a user operation for account abstraction (EIP-4337). + """ + + require Logger + + import Ecto.Query, + only: [ + where: 2 + ] + + use Explorer.Schema + alias Explorer.Chain + alias Explorer.Chain.Hash + alias Explorer.Utility.Microservice + + @type api? :: {:api?, true | false} + + @typedoc """ + * `hash` - the hash of User operation. + * `block_number` - the block number, where user operation happened. + * `block_hash` - the block hash, where user operation happened. + """ + + @type t :: %Explorer.Chain.UserOperation{ + hash: Hash.Full.t(), + block_number: Explorer.Chain.Block.block_number() | nil, + block_hash: Hash.Full.t() + } + + @primary_key false + schema "user_operations" do + field(:hash, Hash.Full, primary_key: true) + field(:block_number, :integer) + field(:block_hash, Hash.Full) + + timestamps() + end + + def changeset(%__MODULE__{} = user_operation, attrs) do + user_operation + |> cast(attrs, [ + :hash, + :block_number, + :block_hash + ]) + |> validate_required([:hash, :block_number, :block_hash]) + end + + @doc """ + Converts `t:Explorer.Chain.UserOperation.t/0` `hash` to the `t:Explorer.Chain.UserOperation.t/0` with that `hash`. + """ + @spec hash_to_user_operation(Hash.Full.t(), [api?]) :: + {:ok, __MODULE__.t()} | {:error, :not_found} + def hash_to_user_operation(%Hash{byte_count: unquote(Hash.Full.byte_count())} = hash, options \\ []) + when is_list(options) do + __MODULE__ + |> where(hash: ^hash) + |> Chain.select_repo(options).one() + |> case do + nil -> + {:error, :not_found} + + user_operation -> + {:ok, user_operation} + end + end + + def user_operations_enabled? do + Microservice.check_enabled(Explorer.MicroserviceInterfaces.AccountAbstraction) == :ok + end +end