Merge pull request #9189 from blockscout/vb-user-operations-in-the-search

User operations in the search
pull/9275/head
Victor Baranov 10 months ago committed by GitHub
commit 5a7f6e7eba
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 35
      apps/block_scout_web/lib/block_scout_web/chain.ex
  3. 2
      apps/block_scout_web/lib/block_scout_web/templates/search/_tile.html.eex
  4. 16
      apps/block_scout_web/lib/block_scout_web/views/api/v2/search_view.ex
  5. 375
      apps/explorer/lib/explorer/chain/search.ex
  6. 0
      apps/explorer/lib/explorer/chain/smart_contract_additional_source.ex
  7. 73
      apps/explorer/lib/explorer/chain/user_operation.ex

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

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

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

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

@ -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
}
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 == ^tx_hash,
where: transaction.hash == ^term,
select: ^transaction_search_fields
)
_ ->
nil
end
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
}
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 == ^tx_hash,
where: transaction.hash == ^term,
select: ^transaction_search_fields
)
_ ->
nil
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

@ -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
Loading…
Cancel
Save