Add user_op interpretation (#9473)

* Add /api/v2/proxy/account-abstraction/operations/{operation_hash_param}/summary endpoint

* Add changelog, fix test

* Process review comments
pull/9588/merge
nikitosing 8 months ago committed by GitHub
parent 646c8492b8
commit ac5625df90
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 1
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  3. 16
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex
  4. 45
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/proxy/account_abstraction_controller.ex
  5. 143
      apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex
  6. 2
      apps/explorer/lib/explorer/chain.ex
  7. 17
      apps/explorer/lib/explorer/chain/log.ex
  8. 53
      apps/explorer/lib/explorer/chain/token_transfer.ex
  9. 8
      apps/explorer/lib/explorer/helper.ex
  10. 22
      apps/explorer/lib/explorer/migrator/sanitize_incorrect_nft_token_transfers.ex
  11. 22
      apps/explorer/lib/explorer/migrator/token_transfer_token_type.ex
  12. 26
      apps/explorer/lib/explorer/utility/microservice.ex
  13. 4
      apps/explorer/test/explorer/chain_test.exs

@ -5,6 +5,7 @@
### Features
- [#9490](https://github.com/blockscout/blockscout/pull/9490) - Add blob transaction counter and filter in block view
- [#9473](https://github.com/blockscout/blockscout/pull/9473) - Add user_op interpretation
- [#9461](https://github.com/blockscout/blockscout/pull/9461) - Fetch blocks without internal transactions backwards
- [#9460](https://github.com/blockscout/blockscout/pull/9460) - Optimism chain type
- [#9409](https://github.com/blockscout/blockscout/pull/9409) - ETH JSON RPC extension

@ -351,6 +351,7 @@ defmodule BlockScoutWeb.ApiRouter do
scope "/account-abstraction" do
get("/operations/:operation_hash_param", V2.Proxy.AccountAbstractionController, :operation)
get("/operations/:operation_hash_param/summary", V2.Proxy.AccountAbstractionController, :summary)
get("/bundlers/:address_hash_param", V2.Proxy.AccountAbstractionController, :bundler)
get("/bundlers", V2.Proxy.AccountAbstractionController, :bundlers)
get("/factories/:address_hash_param", V2.Proxy.AccountAbstractionController, :factory)

@ -30,8 +30,9 @@ defmodule BlockScoutWeb.API.V2.FallbackController do
@vyper_smart_contract_is_not_supported "Vyper smart-contracts are not supported by SolidityScan"
@unverified_smart_contract "Smart-contract is unverified"
@empty_response "Empty response"
@tx_interpreter_service_disabled "Transaction Interpretation Service is not enabled"
@tx_interpreter_service_disabled "Transaction Interpretation Service is disabled"
@disabled "API endpoint is disabled"
@service_disabled "Service is disabled"
def call(conn, {:format, _params}) do
Logger.error(fn ->
@ -297,4 +298,17 @@ defmodule BlockScoutWeb.API.V2.FallbackController do
|> put_view(ApiView)
|> render(:message, %{message: @disabled})
end
def call(conn, {:error, :disabled}) do
conn
|> put_status(501)
|> put_view(ApiView)
|> render(:message, %{message: @service_disabled})
end
def call(conn, {code, response}) when is_integer(code) do
conn
|> put_status(code)
|> json(response)
end
end

@ -2,7 +2,7 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do
use BlockScoutWeb, :controller
alias BlockScoutWeb.API.V2.Helper
alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService
alias Explorer.Chain
alias Explorer.MicroserviceInterfaces.AccountAbstraction
@ -20,6 +20,36 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do
|> process_response(conn)
end
@doc """
Function to handle GET requests to `/api/v2/proxy/account-abstraction/operations/:user_operation_hash_param/summary` endpoint.
"""
@spec summary(Plug.Conn.t(), map()) ::
{:error | :format | :tx_interpreter_enabled | non_neg_integer(), any()} | Plug.Conn.t()
def summary(conn, %{"operation_hash_param" => operation_hash_string, "just_request_body" => "true"}) do
with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_transaction_hash(operation_hash_string)},
{200, %{"hash" => _} = user_op} <- AccountAbstraction.get_user_ops_by_hash(operation_hash_string) do
conn
|> json(TransactionInterpretationService.get_user_op_request_body(user_op))
end
end
def summary(conn, %{"operation_hash_param" => operation_hash_string}) do
with {:format, {:ok, _operation_hash}} <- {:format, Chain.string_to_transaction_hash(operation_hash_string)},
{:tx_interpreter_enabled, true} <- {:tx_interpreter_enabled, TransactionInterpretationService.enabled?()},
{200, %{"hash" => _} = user_op} <- AccountAbstraction.get_user_ops_by_hash(operation_hash_string) do
{response, code} =
case TransactionInterpretationService.interpret_user_operation(user_op) do
{:ok, response} -> {response, 200}
{:error, %Jason.DecodeError{}} -> {%{error: "Error while tx interpreter response decoding"}, 500}
{{:error, error}, code} -> {%{error: error}, code}
end
conn
|> put_status(code)
|> json(response)
end
end
@doc """
Function to handle GET requests to `/api/v2/proxy/account-abstraction/bundlers/:address_hash_param` endpoint.
"""
@ -188,12 +218,21 @@ defmodule BlockScoutWeb.API.V2.Proxy.AccountAbstractionController do
{:error, :disabled} ->
conn
|> put_status(501)
|> json(extended_info(%{message: "Service is disabled"}))
|> json(%{message: "Service is disabled"})
{status_code, response} ->
final_json = response |> extended_info() |> try_to_decode_call_data()
conn
|> put_status(status_code)
|> json(extended_info(response))
|> json(final_json)
end
end
defp try_to_decode_call_data(%{"call_data" => _call_data} = user_op) do
{_mock_tx, _decoded_input, decoded_input_json} = TransactionInterpretationService.decode_user_op_calldata(user_op)
Map.put(user_op, "decoded_call_data", decoded_input_json)
end
defp try_to_decode_call_data(response), do: response
end

@ -4,11 +4,12 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
"""
alias BlockScoutWeb.API.V2.{Helper, TokenView, TransactionView}
alias Ecto.Association.NotLoaded
alias Explorer.Chain
alias Explorer.Chain.Transaction
alias Explorer.Chain.{Data, Log, TokenTransfer, Transaction}
alias HTTPoison.Response
import Explorer.Utility.Microservice, only: [base_url: 2]
import Explorer.Utility.Microservice, only: [base_url: 2, check_enabled: 2]
require Logger
@ -17,15 +18,18 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
@api_true api?: true
@items_limit 50
@spec interpret(Transaction.t()) ::
@doc """
Interpret transaction or user operation
"""
@spec interpret(Transaction.t() | map(), (Transaction.t() -> any()) | (map() -> any())) ::
{{:error, :disabled | binary()}, integer()}
| {:error, Jason.DecodeError.t()}
| {:ok, any}
def interpret(transaction) do
| {:ok, any()}
def interpret(transaction_or_map, request_builder \\ &prepare_request_body/1) do
if enabled?() do
url = interpret_url()
body = prepare_request_body(transaction)
body = request_builder.(transaction_or_map)
http_post_request(url, body)
else
@ -33,10 +37,33 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
end
end
@doc """
Interpret user operation
"""
@spec interpret_user_operation(map()) ::
{{:error, :disabled | binary()}, integer()}
| {:error, Jason.DecodeError.t()}
| {:ok, any()}
def interpret_user_operation(user_operation) do
interpret(user_operation, &prepare_request_body_from_user_op/1)
end
@doc """
Build the request body as for the tx interpreter POST request.
"""
@spec get_request_body(Transaction.t()) :: map()
def get_request_body(transaction) do
prepare_request_body(transaction)
end
@doc """
Build the request body as for the tx interpreter POST request.
"""
@spec get_user_op_request_body(map()) :: map()
def get_user_op_request_body(user_op) do
prepare_request_body_from_user_op(user_op)
end
defp http_post_request(url, body) do
headers = [{"Content-Type", "application/json"}]
@ -63,11 +90,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
defp http_response_code({:ok, %Response{status_code: status_code}}), do: status_code
defp http_response_code(_), do: 500
defp config do
Application.get_env(:block_scout_web, __MODULE__)
end
def enabled?, do: config()[:enabled]
def enabled?, do: check_enabled(:block_scout_web, __MODULE__) == :ok
defp interpret_url do
base_url(:block_scout_web, __MODULE__) <> "/transactions/summary"
@ -100,7 +123,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
hash: transaction.hash,
type: transaction.type,
value: transaction.value,
method: TransactionView.method_name(transaction, decoded_input),
method: TransactionView.method_name(transaction, TransactionView.format_decoded_input(decoded_input)),
status: transaction.status,
actions: TransactionView.transaction_actions(transaction.transaction_actions),
tx_types: TransactionView.tx_types(transaction),
@ -131,6 +154,51 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
|> Enum.map(&TransactionView.prepare_token_transfer(&1, nil, decoded_input))
end
defp user_op_to_logs_and_token_transfers(user_op, decoded_input) do
log_options =
[
necessity_by_association: %{
[address: :names] => :optional,
[address: :smart_contract] => :optional,
address: :optional
},
limit: @items_limit
]
|> Keyword.merge(@api_true)
logs = Log.user_op_to_logs(user_op, log_options)
decoded_logs = TransactionView.decode_logs(logs, false)
prepared_logs =
logs
|> Enum.zip(decoded_logs)
|> Enum.map(fn {log, decoded_log} ->
TransactionView.prepare_log(log, user_op["transaction_hash"], decoded_log, true)
end)
token_transfer_options =
[
necessity_by_association: %{
[from_address: :smart_contract] => :optional,
[to_address: :smart_contract] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
:token => :optional
}
]
|> Keyword.merge(@api_true)
prepared_token_transfers =
logs
|> TokenTransfer.logs_to_token_transfers(token_transfer_options)
|> Chain.flat_1155_batch_token_transfers()
|> Enum.take(@items_limit)
|> Enum.map(&TransactionView.prepare_token_transfer(&1, nil, decoded_input))
{prepared_logs, prepared_token_transfers}
end
defp prepare_logs(transaction) do
full_options =
[
@ -210,4 +278,55 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
address.hash,
true
)
defp prepare_request_body_from_user_op(user_op) do
{mock_tx, decoded_input, decoded_input_json} = decode_user_op_calldata(user_op)
{prepared_logs, prepared_token_transfers} = user_op_to_logs_and_token_transfers(user_op, decoded_input)
{:ok, from_address_hash} = Chain.string_to_address_hash(user_op["sender"])
from_address = Chain.hash_to_address(from_address_hash, [])
%{
data: %{
to: nil,
from: Helper.address_with_info(nil, from_address, from_address_hash, true),
hash: user_op["hash"],
type: 0,
value: "0",
method: TransactionView.method_name(mock_tx, TransactionView.format_decoded_input(decoded_input), true),
status: user_op["status"],
actions: [],
tx_types: [],
raw_input: user_op["call_data"],
decoded_input: decoded_input_json,
token_transfers: prepared_token_transfers
},
logs_data: %{items: prepared_logs}
}
end
@doc """
Decodes user_op["call_data"] and return {mock_tx, decoded_input, decoded_input_json}
"""
@spec decode_user_op_calldata(map()) :: {Transaction.t(), tuple(), map()}
def decode_user_op_calldata(user_op) do
{:ok, input} = Data.cast(user_op["call_data"])
{:ok, op_hash} = Chain.string_to_transaction_hash(user_op["hash"])
mock_tx = %Transaction{
to_address: %NotLoaded{},
input: input,
hash: op_hash
}
skip_sig_provider? = false
{decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(mock_tx, skip_sig_provider?, @api_true)
decoded_input_json = decoded_input |> TransactionView.format_decoded_input() |> TransactionView.decoded_input()
{mock_tx, decoded_input, decoded_input_json}
end
end

@ -960,7 +960,7 @@ defmodule Explorer.Chain do
:contracts_creation_transaction => :optional
}
],
query_decompiled_code_flag \\ true
query_decompiled_code_flag \\ false
) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})

@ -314,4 +314,21 @@ defmodule Explorer.Chain.Log do
|> limit(1)
|> Chain.select_repo(options).one()
end
@doc """
Fetches logs by user operation.
"""
@spec user_op_to_logs(map(), Keyword.t()) :: [t()]
def user_op_to_logs(user_op, options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
limit = Keyword.get(options, :limit, 50)
__MODULE__
|> where([log], log.block_hash == ^user_op["block_hash"] and log.transaction_hash == ^user_op["transaction_hash"])
|> where([log], log.index >= ^user_op["user_logs_start_index"])
|> order_by([log], asc: log.index)
|> limit(^min(user_op["user_logs_count"], limit))
|> Chain.join_associations(necessity_by_association)
|> Chain.select_repo(options).all()
end
end

@ -400,4 +400,57 @@ defmodule Explorer.Chain.TokenTransfer do
|> where([tt, token: token], token.type == "ERC-721")
|> preload([tt, token: token], [{:token, token}])
end
@doc """
To be used in migrators
"""
@spec encode_token_transfer_ids([{Hash.t(), Hash.t(), non_neg_integer()}]) :: binary()
def encode_token_transfer_ids(ids) do
encoded_values =
ids
|> Enum.reduce("", fn {t_hash, b_hash, log_index}, acc ->
acc <> "('#{hash_to_query_string(t_hash)}', '#{hash_to_query_string(b_hash)}', #{log_index}),"
end)
|> String.trim_trailing(",")
"(#{encoded_values})"
end
defp hash_to_query_string(hash) do
s_hash =
hash
|> to_string()
|> String.trim_leading("0")
"\\#{s_hash}"
end
@doc """
Fetches token transfers from logs.
"""
@spec logs_to_token_transfers([Log.t()], Keyword.t()) :: [TokenTransfer.t()]
def logs_to_token_transfers(logs, options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
logs
|> logs_to_token_transfers_query()
|> limit(^Enum.count(logs))
|> Chain.join_associations(necessity_by_association)
|> Chain.select_repo(options).all()
end
defp logs_to_token_transfers_query(query \\ __MODULE__, logs)
defp logs_to_token_transfers_query(query, [log | tail]) do
query
|> or_where(
[tt],
tt.transaction_hash == ^log.transaction_hash and tt.block_hash == ^log.block_hash and tt.log_index == ^log.index
)
|> logs_to_token_transfers_query(tail)
end
defp logs_to_token_transfers_query(query, []) do
query
end
end

@ -136,10 +136,12 @@ defmodule Explorer.Helper do
@doc """
Validate url
"""
@spec valid_url?(String.t()) :: boolean
def valid_url?(string) do
@spec valid_url?(String.t()) :: boolean()
def valid_url?(string) when is_binary(string) do
uri = URI.parse(string)
uri.scheme != nil && uri.host =~ "."
!is_nil(uri.scheme) && !is_nil(uri.host)
end
def valid_url?(_), do: false
end

@ -130,27 +130,7 @@ defmodule Explorer.Migrator.SanitizeIncorrectNFTTokenTransfers do
"""
DELETE
FROM token_transfers tt
WHERE (tt.transaction_hash, tt.block_hash, tt.log_index) IN #{encode_token_transfer_ids(token_transfer_ids)}
WHERE (tt.transaction_hash, tt.block_hash, tt.log_index) IN #{TokenTransfer.encode_token_transfer_ids(token_transfer_ids)}
"""
end
defp encode_token_transfer_ids(ids) do
encoded_values =
ids
|> Enum.reduce("", fn {t_hash, b_hash, log_index}, acc ->
acc <> "('#{hash_to_query_string(t_hash)}', '#{hash_to_query_string(b_hash)}', #{log_index}),"
end)
|> String.trim_trailing(",")
"(#{encoded_values})"
end
defp hash_to_query_string(hash) do
s_hash =
hash
|> to_string()
|> String.trim_leading("0")
"\\#{s_hash}"
end
end

@ -54,27 +54,7 @@ defmodule Explorer.Migrator.TokenTransferTokenType do
FROM tokens t, blocks b
WHERE tt.block_hash = b.hash
AND tt.token_contract_address_hash = t.contract_address_hash
AND (tt.transaction_hash, tt.block_hash, tt.log_index) IN #{encode_token_transfer_ids(token_transfer_ids)};
AND (tt.transaction_hash, tt.block_hash, tt.log_index) IN #{TokenTransfer.encode_token_transfer_ids(token_transfer_ids)};
"""
end
defp encode_token_transfer_ids(ids) do
encoded_values =
ids
|> Enum.reduce("", fn {t_hash, b_hash, log_index}, acc ->
acc <> "('#{hash_to_query_string(t_hash)}', '#{hash_to_query_string(b_hash)}', #{log_index}),"
end)
|> String.trim_trailing(",")
"(#{encoded_values})"
end
defp hash_to_query_string(hash) do
s_hash =
hash
|> to_string()
|> String.trim_leading("0")
"\\#{s_hash}"
end
end

@ -2,14 +2,26 @@ defmodule Explorer.Utility.Microservice do
@moduledoc """
Module is responsible for common utils related to microservices.
"""
alias Explorer.Helper
@doc """
Returns base url of the microservice or nil if it is invalid or not set
"""
@spec base_url(atom(), atom()) :: false | nil | binary()
def base_url(application \\ :explorer, module) do
url = Application.get_env(application, module)[:service_url]
if String.ends_with?(url, "/") do
url
|> String.slice(0..(String.length(url) - 2))
else
url
cond do
not Helper.valid_url?(url) ->
nil
String.ends_with?(url, "/") ->
url
|> String.slice(0..(String.length(url) - 2))
true ->
url
end
end
@ -17,8 +29,8 @@ defmodule Explorer.Utility.Microservice do
Returns :ok if Application.get_env(:explorer, module)[:enabled] is true (module is enabled)
"""
@spec check_enabled(atom) :: :ok | {:error, :disabled}
def check_enabled(module) do
if Application.get_env(:explorer, module)[:enabled] do
def check_enabled(application \\ :explorer, module) do
if Application.get_env(application, module)[:enabled] && base_url(application, module) do
:ok
else
{:error, :disabled}

@ -896,7 +896,7 @@ defmodule Explorer.ChainTest do
address = insert(:address)
insert(:decompiled_smart_contract, address_hash: address.hash)
{:ok, found_address} = Chain.hash_to_address(address.hash)
{:ok, found_address} = Chain.hash_to_address(address.hash, [], true)
assert found_address.has_decompiled_code?
end
@ -1384,7 +1384,7 @@ defmodule Explorer.ChainTest do
}
}
test "with valid data", %{json_rpc_named_arguments: json_rpc_named_arguments} do
test "with valid data", %{json_rpc_named_arguments: _json_rpc_named_arguments} do
{:ok, first_topic} = Explorer.Chain.Hash.Full.cast(@first_topic_hex_string)
{:ok, second_topic} = Explorer.Chain.Hash.Full.cast(@second_topic_hex_string)
{:ok, third_topic} = Explorer.Chain.Hash.Full.cast(@third_topic_hex_string)

Loading…
Cancel
Save