From 005b1b20ac6918c21b2516b0eb60313ded4c06e3 Mon Sep 17 00:00:00 2001 From: Nikita Pozdniakov Date: Wed, 6 Dec 2023 13:55:55 +0300 Subject: [PATCH] Add /api/v2/transactions/{transaction_hash_param}/summary endpoint + Transaction Interpretation Service interface --- .github/workflows/config.yml | 26 ++-- CHANGELOG.md | 2 + .../lib/block_scout_web/api_router.ex | 1 + .../controllers/api/v2/fallback_controller.ex | 8 + .../api/v2/transaction_controller.ex | 92 ++++++----- .../transaction_interpretation.ex | 147 ++++++++++++++++++ .../views/api/v2/transaction_view.ex | 66 ++++---- apps/explorer/lib/explorer/helper.ex | 11 ++ .../rust_verifier_interface_behaviour.ex | 4 +- .../smart_contract/sig_provider_interface.ex | 4 +- .../lib/explorer/utility/microservice.ex | 15 ++ .../lib/explorer/utility/rust_service.ex | 15 -- .../lib/explorer/visualize/sol2uml.ex | 4 +- config/runtime.exs | 4 + 14 files changed, 296 insertions(+), 103 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex create mode 100644 apps/explorer/lib/explorer/utility/microservice.ex delete mode 100644 apps/explorer/lib/explorer/utility/rust_service.ex diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 247f01b0e8..2d672f0119 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -72,7 +72,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps- @@ -130,7 +130,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -154,7 +154,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -183,7 +183,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -227,7 +227,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -253,7 +253,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -282,7 +282,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -330,7 +330,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -376,7 +376,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -438,7 +438,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -498,7 +498,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -569,7 +569,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -637,7 +637,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_29-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" diff --git a/CHANGELOG.md b/CHANGELOG.md index a4ab195fdb..845eff7b0d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features +- [#8957](https://github.com/blockscout/blockscout/pull/8957) - Add Tx Interpreter Service integration + ### Fixes ### Chore diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index dee973786c..7880bd15af 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -216,6 +216,7 @@ defmodule BlockScoutWeb.ApiRouter do get("/:transaction_hash_param/logs", V2.TransactionController, :logs) get("/:transaction_hash_param/raw-trace", V2.TransactionController, :raw_trace) get("/:transaction_hash_param/state-changes", V2.TransactionController, :state_changes) + get("/:transaction_hash_param/summary", V2.TransactionController, :summary) end scope "/blocks" do diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index 691d8de75b..c7f1fc3692 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -26,6 +26,7 @@ defmodule BlockScoutWeb.API.V2.FallbackController do @address_not_found "Address not found" @address_is_not_smart_contract "Address is not smart-contract" @empty_response "Empty response" + @tx_interpreter_service_disabled "Transaction Interpretation Service is not enabled" def call(conn, {:format, _params}) do Logger.error(fn -> @@ -256,4 +257,11 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> put_view(ApiView) |> render(:message, %{message: @empty_response}) end + + def call(conn, {:tx_interpreter_enabled, false}) do + conn + |> put_status(404) + |> put_view(ApiView) + |> render(:message, %{message: @tx_interpreter_service_disabled}) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 43b63b6f69..f86c06c3cd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -23,8 +23,9 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ] alias BlockScoutWeb.AccessHelper + alias BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, as: TransactionInterpretationService alias BlockScoutWeb.Models.TransactionStateHelper - alias Explorer.Chain + alias Explorer.{Chain, Helper} alias Explorer.Chain.Zkevm.Reader alias Indexer.Fetcher.FirstTraceOnDemand @@ -36,7 +37,6 @@ defmodule BlockScoutWeb.API.V2.TransactionController do [created_contract_address: :token] => :optional, [from_address: :names] => :optional, [to_address: :names] => :optional, - # as far as I remember this needed for substituting implementation name in `to` address instead of is's real name (in transactions) [to_address: :smart_contract] => :optional } @@ -95,16 +95,11 @@ defmodule BlockScoutWeb.API.V2.TransactionController do necessity_by_association_with_actions end - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, - Chain.hash_to_transaction( - transaction_hash, - necessity_by_association: necessity_by_association, - api?: true - )}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params), + with {:ok, transaction, _transaction_hash} <- + validate_transaction(transaction_hash_string, params, + necessity_by_association: necessity_by_association, + api?: true + ), preloaded <- Chain.preload_token_transfers(transaction, @token_transfers_in_tx_necessity_by_association, @api_true, false) do conn @@ -183,11 +178,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec raw_trace(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def raw_trace(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do if is_nil(transaction.block_number) do conn |> put_status(200) @@ -216,11 +207,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec token_transfers(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def token_transfers(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do paging_options = paging_options(params) full_options = @@ -252,11 +239,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec internal_transactions(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def internal_transactions(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = @internal_transaction_necessity_by_association |> Keyword.merge(paging_options(params)) @@ -284,11 +267,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec logs(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def logs(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, Chain.hash_to_transaction(transaction_hash, @api_true)}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, _transaction, transaction_hash} <- validate_transaction(transaction_hash_string, params) do full_options = [ necessity_by_association: %{ @@ -323,16 +302,12 @@ defmodule BlockScoutWeb.API.V2.TransactionController do """ @spec state_changes(Plug.Conn.t(), map()) :: Plug.Conn.t() | {atom(), any()} def state_changes(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do - with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, - {:not_found, {:ok, transaction}} <- - {:not_found, - Chain.hash_to_transaction(transaction_hash, - necessity_by_association: - Map.merge(@transaction_necessity_by_association, %{[block: [miner: :names]] => :optional}), - api?: true - )}, - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), - {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + with {:ok, transaction, _transaction_hash} <- + validate_transaction(transaction_hash_string, params, + necessity_by_association: + Map.merge(@transaction_necessity_by_association, %{[block: [miner: :names]] => :optional}), + api?: true + ) do state_changes_plus_next_page = transaction |> TransactionStateHelper.state_changes(params |> paging_options() |> Keyword.merge(api?: true)) @@ -376,4 +351,37 @@ defmodule BlockScoutWeb.API.V2.TransactionController do }) end end + + @doc """ + Function to handle GET requests to `/api/v2/transactions/:transaction_hash_param/summary` endpoint. + """ + @spec summary(any, map) :: + {:format, :error} + | {:not_found, {:error, :not_found}} + | {:restricted_access, true} + | {:tx_interpreter_enabled, boolean} + | Plug.Conn.t() + def summary(conn, %{"transaction_hash_param" => transaction_hash_string} = params) do + with {:tx_interpreter_enabled, true} <- {:tx_interpreter_enabled, TransactionInterpretationService.enabled?()}, + {:ok, transaction, _transaction_hash} <- validate_transaction(transaction_hash_string, params) do + response = + case TransactionInterpretationService.interpret(transaction) do + {:ok, response} -> Helper.maybe_decode(response) + {:error, error} -> %{error: error} + end + + conn + |> json(response) + end + end + + defp validate_transaction(transaction_hash_string, params, options \\ @api_true) do + with {:format, {:ok, transaction_hash}} <- {:format, Chain.string_to_transaction_hash(transaction_hash_string)}, + {:not_found, {:ok, transaction}} <- + {:not_found, Chain.hash_to_transaction(transaction_hash, options)}, + {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.from_address_hash), params), + {:ok, false} <- AccessHelper.restricted_access?(to_string(transaction.to_address_hash), params) do + {:ok, transaction, transaction_hash} + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex new file mode 100644 index 0000000000..8ad149081d --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -0,0 +1,147 @@ +defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do + @moduledoc """ + Module to interact with Transaction Interpretation Service + """ + + alias BlockScoutWeb.API.V2.{Helper, TransactionView} + alias Explorer.Chain + alias Explorer.Chain.Transaction + alias HTTPoison.Response + + import Explorer.Utility.Microservice, only: [base_url: 2] + + require Logger + + @post_timeout :timer.minutes(5) + @request_error_msg "Error while sending request to Transaction Interpretation Service" + @api_true api?: true + @items_limit 50 + + @spec interpret(any) :: {:error, :disabled | binary} | {:ok, any} + def interpret(transaction) do + if enabled?() do + url = interpret_url() + + body = prepare_request_body(transaction) + + http_post_request(url, body) + else + {:error, :disabled} + end + end + + defp http_post_request(url, body) do + headers = [{"Content-Type", "application/json"}] + + case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %Response{body: body, status_code: 200}} -> + {:ok, body} + + error -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to microservice url: #{url}, body: #{inspect(body, limit: :infinity, printable_limit: :infinity)}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:error, @request_error_msg} + end + end + + defp config do + Application.get_env(:block_scout_web, __MODULE__) + end + + def enabled?, do: config()[:enabled] + + defp interpret_url do + base_url(:block_scout_web, __MODULE__) <> "/transactions/summary" + end + + defp prepare_request_body(transaction) do + preloaded_transaction = + Chain.select_repo(@api_true).preload(transaction, [ + :transaction_actions, + to_address: [:names, :smart_contract], + from_address: [:names, :smart_contract], + created_contract_address: [:names, :token, :smart_contract] + ]) + + skip_sig_provider? = true + {decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true) + + decoded_input_data = TransactionView.decoded_input(decoded_input) + + %{ + data: %{ + to: + Helper.address_with_info(nil, preloaded_transaction.to_address, preloaded_transaction.to_address_hash, true), + from: + Helper.address_with_info( + nil, + preloaded_transaction.from_address, + preloaded_transaction.from_address_hash, + true + ), + hash: transaction.hash, + type: transaction.type, + value: transaction.value, + method: TransactionView.method_name(transaction, decoded_input), + status: transaction.status, + actions: TransactionView.transaction_actions(transaction.transaction_actions), + tx_types: TransactionView.tx_types(transaction), + raw_input: transaction.input, + decoded_input: decoded_input_data, + token_transfers: prepare_token_transfers(preloaded_transaction, decoded_input) + }, + logs_data: %{items: prepare_logs(transaction)} + } + end + + defp prepare_token_transfers(transaction, decoded_input) do + full_options = + [ + necessity_by_association: %{ + [from_address: :smart_contract] => :optional, + [to_address: :smart_contract] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional + } + ] + |> Keyword.merge(@api_true) + + transaction.hash + |> Chain.transaction_to_token_transfers(full_options) + |> Chain.flat_1155_batch_token_transfers() + |> Enum.take(@items_limit) + |> Enum.map(&TransactionView.prepare_token_transfer(&1, nil, decoded_input)) + end + + defp prepare_logs(transaction) do + full_options = + [ + necessity_by_association: %{ + [address: :names] => :optional, + [address: :smart_contract] => :optional, + address: :optional + } + ] + |> Keyword.merge(@api_true) + + logs = + transaction.hash + |> Chain.transaction_to_logs(full_options) + |> Enum.take(@items_limit) + + decoded_logs = TransactionView.decode_logs(logs, true) + + logs + |> Enum.zip(decoded_logs) + |> Enum.map(fn {log, decoded_log} -> TransactionView.prepare_log(log, transaction.hash, decoded_log, true) end) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index e1ddea367b..3314b06e58 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -184,7 +184,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end - defp decode_logs(logs, skip_sig_provider?) do + @spec decode_logs([Log.t()], boolean) :: [tuple] + def decode_logs(logs, skip_sig_provider?) do {result, _, _} = Enum.reduce(logs, {[], %{}, %{}}, fn log, {results, contracts_acc, events_acc} -> {result, contracts_acc, events_acc} = @@ -283,12 +284,12 @@ defmodule BlockScoutWeb.API.V2.TransactionView do } end - def prepare_log(log, transaction_or_hash, decoded_log) do + def prepare_log(log, transaction_or_hash, decoded_log, tags_for_address_needed? \\ false) do decoded = process_decoded_log(decoded_log) %{ "tx_hash" => get_tx_hash(transaction_or_hash), - "address" => Helper.address_with_info(nil, log.address, log.address_hash, false), + "address" => Helper.address_with_info(nil, log.address, log.address_hash, tags_for_address_needed?), "topics" => [ log.first_topic, log.second_topic, @@ -573,9 +574,9 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def token_transfers_overflow(token_transfers, _), do: Enum.count(token_transfers) > Chain.get_token_transfers_per_transaction_preview_count() - defp transaction_actions(%NotLoaded{}), do: [] + def transaction_actions(%NotLoaded{}), do: [] - defp transaction_actions(actions) do + def transaction_actions(actions) do render("transaction_actions.json", %{actions: actions}) end @@ -617,7 +618,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end end - defp decoded_input(decoded_input) do + def decoded_input(decoded_input) do case decoded_input do {:ok, method_id, text, mapping} -> render(__MODULE__, "decoded_input.json", method_id: method_id, text: text, mapping: mapping, error?: false) @@ -695,17 +696,17 @@ defmodule BlockScoutWeb.API.V2.TransactionView do |> Timex.diff(right, :milliseconds) end - defp method_name(_, _, skip_sc_check? \\ false) + def method_name(_, _, skip_sc_check? \\ false) - defp method_name(_, {:ok, _method_id, text, _mapping}, _) do + def method_name(_, {:ok, _method_id, text, _mapping}, _) do Transaction.parse_method_name(text, false) end - defp method_name( - %Transaction{to_address: to_address, input: %{bytes: <>}}, - _, - skip_sc_check? - ) do + def method_name( + %Transaction{to_address: to_address, input: %{bytes: <>}}, + _, + skip_sc_check? + ) do if skip_sc_check? || Address.is_smart_contract(to_address) do "0x" <> Base.encode16(method_id, case: :lower) else @@ -713,13 +714,24 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end end - defp method_name(_, _, _) do + def method_name(_, _, _) do nil end - defp tx_types(tx, types \\ [], stage \\ :token_transfer) - - defp tx_types(%Transaction{token_transfers: token_transfers} = tx, types, :token_transfer) do + @spec tx_types( + Explorer.Chain.Transaction.t(), + list, + :coin_transfer + | :contract_call + | :contract_creation + | :rootstock_bridge + | :rootstock_remasc + | :token_creation + | :token_transfer + ) :: list + def tx_types(tx, types \\ [], stage \\ :token_transfer) + + def tx_types(%Transaction{token_transfers: token_transfers} = tx, types, :token_transfer) do types = if (!is_nil(token_transfers) && token_transfers != [] && !match?(%NotLoaded{}, token_transfers)) || tx.has_token_transfers do @@ -731,7 +743,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :token_creation) end - defp tx_types(%Transaction{created_contract_address: created_contract_address} = tx, types, :token_creation) do + def tx_types(%Transaction{created_contract_address: created_contract_address} = tx, types, :token_creation) do types = if match?(%Address{}, created_contract_address) && match?(%Token{}, created_contract_address.token) do [:token_creation | types] @@ -742,11 +754,11 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :contract_creation) end - defp tx_types( - %Transaction{to_address_hash: to_address_hash} = tx, - types, - :contract_creation - ) do + def tx_types( + %Transaction{to_address_hash: to_address_hash} = tx, + types, + :contract_creation + ) do types = if is_nil(to_address_hash) do [:contract_creation | types] @@ -757,7 +769,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :contract_call) end - defp tx_types(%Transaction{to_address: to_address} = tx, types, :contract_call) do + def tx_types(%Transaction{to_address: to_address} = tx, types, :contract_call) do types = if Address.is_smart_contract(to_address) do [:contract_call | types] @@ -768,7 +780,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :coin_transfer) end - defp tx_types(%Transaction{value: value} = tx, types, :coin_transfer) do + def tx_types(%Transaction{value: value} = tx, types, :coin_transfer) do types = if Decimal.compare(value.value, 0) == :gt do [:coin_transfer | types] @@ -779,7 +791,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :rootstock_remasc) end - defp tx_types(tx, types, :rootstock_remasc) do + def tx_types(tx, types, :rootstock_remasc) do types = if Transaction.is_rootstock_remasc_transaction(tx) do [:rootstock_remasc | types] @@ -790,7 +802,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do tx_types(tx, types, :rootstock_bridge) end - defp tx_types(tx, types, :rootstock_bridge) do + def tx_types(tx, types, :rootstock_bridge) do if Transaction.is_rootstock_bridge_transaction(tx) do [:rootstock_bridge | types] else diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 45d92a2830..20d65314b2 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -81,4 +81,15 @@ defmodule Explorer.Helper do _ -> %{error: data} end end + + @doc """ + Tries to decode binary to json, return either decoded object, or initial binary + """ + @spec maybe_decode(binary) :: any + def maybe_decode(data) do + case safe_decode_json(data) do + %{error: _} -> data + decoded -> decoded + end + end end diff --git a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex index 2466e6e1d5..2c5629d219 100644 --- a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex +++ b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface_behaviour.ex @@ -5,7 +5,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do defmacro __using__(_) do # credo:disable-for-next-line quote([]) do - alias Explorer.Utility.RustService + alias Explorer.Utility.Microservice alias HTTPoison.Response require Logger @@ -172,7 +172,7 @@ defmodule Explorer.SmartContract.RustVerifierInterfaceBehaviour do def base_api_url, do: "#{base_url()}" <> "/api/v2" def base_url do - RustService.base_url(Explorer.SmartContract.RustVerifierInterfaceBehaviour) + Microservice.base_url(Explorer.SmartContract.RustVerifierInterfaceBehaviour) end def enabled?, do: Application.get_env(:explorer, Explorer.SmartContract.RustVerifierInterfaceBehaviour)[:enabled] diff --git a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex index 92bba0d0ec..b97f9bcd1e 100644 --- a/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex +++ b/apps/explorer/lib/explorer/smart_contract/sig_provider_interface.ex @@ -3,7 +3,7 @@ defmodule Explorer.SmartContract.SigProviderInterface do Adapter for decoding events and function calls with https://github.com/blockscout/blockscout-rs/tree/main/sig-provider """ - alias Explorer.Utility.RustService + alias Explorer.Utility.Microservice alias HTTPoison.Response require Logger @@ -81,7 +81,7 @@ defmodule Explorer.SmartContract.SigProviderInterface do def base_api_url, do: "#{base_url()}" <> "/api/v1/abi" def base_url do - RustService.base_url(__MODULE__) + Microservice.base_url(__MODULE__) end def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] diff --git a/apps/explorer/lib/explorer/utility/microservice.ex b/apps/explorer/lib/explorer/utility/microservice.ex new file mode 100644 index 0000000000..6200f7f7ac --- /dev/null +++ b/apps/explorer/lib/explorer/utility/microservice.ex @@ -0,0 +1,15 @@ +defmodule Explorer.Utility.Microservice do + @moduledoc """ + Module is responsible for common utils related to microservices. + """ + 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 + end + end +end diff --git a/apps/explorer/lib/explorer/utility/rust_service.ex b/apps/explorer/lib/explorer/utility/rust_service.ex deleted file mode 100644 index 63f9499612..0000000000 --- a/apps/explorer/lib/explorer/utility/rust_service.ex +++ /dev/null @@ -1,15 +0,0 @@ -defmodule Explorer.Utility.RustService do - @moduledoc """ - Module is responsible for common utils related to rust microservices. - """ - def base_url(module) do - url = Application.get_env(:explorer, module)[:service_url] - - if String.ends_with?(url, "/") do - url - |> String.slice(0..(String.length(url) - 2)) - else - url - end - end -end diff --git a/apps/explorer/lib/explorer/visualize/sol2uml.ex b/apps/explorer/lib/explorer/visualize/sol2uml.ex index 10a6c2a9ef..459531a362 100644 --- a/apps/explorer/lib/explorer/visualize/sol2uml.ex +++ b/apps/explorer/lib/explorer/visualize/sol2uml.ex @@ -2,7 +2,7 @@ defmodule Explorer.Visualize.Sol2uml do @moduledoc """ Adapter for sol2uml visualizer with https://github.com/blockscout/blockscout-rs/blob/main/visualizer """ - alias Explorer.Utility.RustService + alias Explorer.Utility.Microservice alias HTTPoison.Response require Logger @@ -61,7 +61,7 @@ defmodule Explorer.Visualize.Sol2uml do def base_api_url, do: "#{base_url()}" <> "/api/v1" def base_url do - RustService.base_url(__MODULE__) + Microservice.base_url(__MODULE__) end def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] diff --git a/config/runtime.exs b/config/runtime.exs index ec0cd0427b..8fe99566ef 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -132,6 +132,10 @@ config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance, config :block_scout_web, BlockScoutWeb.API.V2, enabled: ConfigHelper.parse_bool_env_var("API_V2_ENABLED", "true") +config :block_scout_web, BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation, + service_url: System.get_env("MICROSERVICE_TX_INTERPRETATION_URL"), + enabled: ConfigHelper.parse_bool_env_var("MICROSERVICE_TX_INTERPRETATION_ENABLED") + # Configures Ueberauth's Auth0 auth provider config :ueberauth, Ueberauth.Strategy.Auth0.OAuth, domain: System.get_env("ACCOUNT_AUTH0_DOMAIN"),