diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex index 3178aaae5d..35975e90ec 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do import BlockScoutWeb.Chain, only: [split_list_by_page: 1, next_page_params: 4] import Explorer.PagingOptions, only: [default_paging_options: 0] - alias BlockScoutWeb.API.V2.{AdvancedFilterView, CSVExportController, TransactionView} + alias BlockScoutWeb.API.V2.{AdvancedFilterView, CSVExportController} alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{AdvancedFilter, ContractMethod, Data, Token, Transaction} alias Explorer.Chain.CSVExport.Helper, as: CSVHelper @@ -60,7 +60,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do {decoded_transactions, _abi_acc, methods_acc} = advanced_filters |> Enum.map(fn af -> %Transaction{to_address: af.to_address, input: af.input, hash: af.hash} end) - |> TransactionView.decode_transactions(true) + |> Transaction.decode_transactions(true, @api_true) next_page_params = next_page |> next_page_params(advanced_filters, Map.take(params, ["items_count"]), &paging_params/1) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex index 9de0d519a0..1826c67598 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex @@ -34,7 +34,7 @@ defmodule BlockScoutWeb.API.V2.UtilsController do @api_true ) - decoded_input_data = decoded_input |> TransactionView.format_decoded_input() |> TransactionView.decoded_input() + decoded_input_data = decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input() conn |> json(%{result: decoded_input_data}) 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 index 83b4abfa59..591598fd42 100644 --- 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 @@ -116,7 +116,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do skip_sig_provider? = false {decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true) - decoded_input_data = decoded_input |> TransactionView.format_decoded_input() |> TransactionView.decoded_input() + decoded_input_data = decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input() %{ data: %{ @@ -131,7 +131,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do hash: transaction.hash, type: transaction.type, value: transaction.value, - method: TransactionView.method_name(transaction, TransactionView.format_decoded_input(decoded_input)), + method: Transaction.method_name(transaction, Transaction.format_decoded_input(decoded_input)), status: transaction.status, actions: TransactionView.transaction_actions(transaction.transaction_actions), tx_types: TransactionView.tx_types(transaction), @@ -317,7 +317,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do hash: user_op_hash, type: 0, value: "0", - method: TransactionView.method_name(mock_tx, TransactionView.format_decoded_input(decoded_input), true), + method: Transaction.method_name(mock_tx, Transaction.format_decoded_input(decoded_input), true), status: user_op["status"], actions: [], tx_types: [], @@ -350,6 +350,6 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do {decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(mock_tx, skip_sig_provider?, @api_true) - {mock_tx, decoded_input, decoded_input |> TransactionView.format_decoded_input() |> TransactionView.decoded_input()} + {mock_tx, decoded_input, decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input()} end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex index 5e9c9a0423..eb602e3412 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex @@ -112,7 +112,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do method: if(advanced_filter.type != "coin_transfer", do: - TransactionView.method_name( + Transaction.method_name( %Transaction{ to_address: %Address{ hash: advanced_filter.token_transfer.token.contract_address_hash, @@ -123,7 +123,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterView do decoded_input ), else: - TransactionView.method_name( + Transaction.method_name( %Transaction{to_address: advanced_filter.to_address, input: advanced_filter.input}, decoded_input ) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex index 8bbee60b6f..5d334ee06c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex @@ -28,7 +28,7 @@ defmodule BlockScoutWeb.API.V2.SuaveView do wrapped_value = Map.get(transaction, :wrapped_value) {[wrapped_decoded_input], _, _} = - TransactionView.decode_transactions( + Transaction.decode_transactions( [ %Transaction{ to_address: wrapped_to_address, @@ -36,7 +36,8 @@ defmodule BlockScoutWeb.API.V2.SuaveView do hash: wrapped_hash } ], - false + false, + api?: true ) out_json @@ -76,7 +77,7 @@ defmodule BlockScoutWeb.API.V2.SuaveView do "value" => wrapped_value, "hash" => wrapped_hash, "method" => - TransactionView.method_name( + Transaction.method_name( %Transaction{to_address: wrapped_to_address, input: wrapped_input}, wrapped_decoded_input ), 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 d05a35e4c5..808ff21969 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 @@ -30,7 +30,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do watchlist_names: watchlist_names }) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = decode_transactions(transactions, true) + {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) %{ "items" => @@ -50,7 +50,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do watchlist_names: watchlist_names }) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = decode_transactions(transactions, true) + {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) transactions |> chain_type_transformations() @@ -62,7 +62,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def render("transactions.json", %{transactions: transactions, next_page_params: next_page_params, conn: conn}) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = decode_transactions(transactions, true) + {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) %{ "items" => @@ -82,7 +82,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def render("transactions.json", %{transactions: transactions, conn: conn}) do block_height = Chain.block_height(@api_true) - {decoded_transactions, _, _} = decode_transactions(transactions, true) + {decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true) transactions |> chain_type_transformations() @@ -92,7 +92,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do def render("transaction.json", %{transaction: transaction, conn: conn}) do block_height = Chain.block_height(@api_true) - {[decoded_input], _, _} = decode_transactions([transaction], false) + {[decoded_input], _, _} = Transaction.decode_transactions([transaction], false, @api_true) transaction |> chain_type_transformations() @@ -116,7 +116,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do - {decoded_transactions, _, _} = decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true) + {decoded_transactions, _, _} = + Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true) %{ "items" => @@ -128,7 +129,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end def render("token_transfers.json", %{token_transfers: token_transfers, conn: conn}) do - {decoded_transactions, _, _} = decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true) + {decoded_transactions, _, _} = + Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true) token_transfers |> Enum.zip(decoded_transactions) @@ -136,7 +138,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do end def render("token_transfer.json", %{token_transfer: token_transfer, conn: conn}) do - {[decoded_transaction], _, _} = decode_transactions([token_transfer.transaction], true) + {[decoded_transaction], _, _} = Transaction.decode_transactions([token_transfer.transaction], true, @api_true) prepare_token_transfer(token_transfer, conn, decoded_transaction) end @@ -231,18 +233,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do Enum.reverse(result) end - def decode_transactions(transactions, skip_sig_provider?) do - {results, abi_acc, methods_acc} = - Enum.reduce(transactions, {[], %{}, %{}}, fn transaction, {results, abi_acc, methods_acc} -> - {result, abi_acc, methods_acc} = - Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true, abi_acc, methods_acc) - - {[format_decoded_input(result) | results], abi_acc, methods_acc} - end) - - {Enum.reverse(results), abi_acc, methods_acc} - end - def prepare_token_transfer(token_transfer, _conn, decoded_input) do %{ "tx_hash" => token_transfer.transaction_hash, @@ -256,7 +246,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do do: block_timestamp(token_transfer.transaction), else: block_timestamp(token_transfer.block) ), - "method" => method_name(token_transfer.transaction, decoded_input, true), + "method" => Transaction.method_name(token_transfer.transaction, decoded_input, true), "block_hash" => to_string(token_transfer.block_hash), "block_number" => to_string(token_transfer.block_number), "log_index" => to_string(token_transfer.log_index) @@ -457,7 +447,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do "token_transfers_overflow" => token_transfers_overflow(transaction.token_transfers, single_tx?), "actions" => transaction_actions(transaction.transaction_actions), "exchange_rate" => Market.get_coin_exchange_rate().usd_value, - "method" => method_name(transaction, decoded_input), + "method" => Transaction.method_name(transaction, decoded_input), "tx_types" => tx_types(transaction), "tx_tag" => GetTransactionTags.get_transaction_tags(transaction.hash, current_user(single_tx? && conn)), "has_error_in_internal_txs" => transaction.has_error_in_internal_txs @@ -553,12 +543,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do defp format_status({:error, reason}), do: reason defp format_status(status), do: status - @spec format_decoded_input(any()) :: nil | map() | tuple() - def format_decoded_input({:error, _, []}), do: nil - def format_decoded_input({:error, _, candidates}), do: Enum.at(candidates, 0) - def format_decoded_input({:ok, _identifier, _text, _mapping} = decoded), do: decoded - def format_decoded_input(_), do: nil - defp format_decoded_log_input({:error, :could_not_decode}), do: nil defp format_decoded_log_input({:ok, _method_id, _text, _mapping} = decoded), do: decoded defp format_decoded_log_input({:error, _, candidates}), do: Enum.at(candidates, 0) @@ -607,32 +591,6 @@ defmodule BlockScoutWeb.API.V2.TransactionView do |> Timex.diff(right, :milliseconds) end - @doc """ - Return method name used in tx - """ - @spec method_name(Transaction.t(), any(), boolean()) :: binary() | nil - def method_name(_, _, skip_sc_check? \\ false) - - def method_name(_, {:ok, _method_id, text, _mapping}, _) do - Transaction.parse_method_name(text, false) - end - - def method_name( - %Transaction{to_address: to_address, input: %{bytes: <>}}, - _, - skip_sc_check? - ) do - if skip_sc_check? || Address.smart_contract?(to_address) do - "0x" <> Base.encode16(method_id, case: :lower) - else - nil - end - end - - def method_name(_, _, _) do - nil - end - @doc """ Returns array of token types for tx. """ diff --git a/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex b/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex index d332cd7dd1..88496d4cd3 100644 --- a/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex +++ b/apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex @@ -13,8 +13,14 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do {from_block, to_block} = Helper.block_from_period(from_period, to_period) exchange_rate = Market.get_coin_exchange_rate() - address_hash - |> fetch_transactions(from_block, to_block, filter_type, filter_value, Helper.paging_options()) + transactions = + address_hash + |> fetch_transactions(from_block, to_block, filter_type, filter_value, Helper.paging_options()) + + transactions + |> Transaction.decode_transactions(true, api?: true) + |> elem(0) + |> Enum.zip(transactions) |> to_csv_format(address_hash, exchange_rate) |> Helper.dump_to_stream() end @@ -22,7 +28,7 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do # sobelow_skip ["DOS.StringToAtom"] def fetch_transactions(address_hash, from_block, to_block, filter_type, filter_value, paging_options) do options = - [] + [necessity_by_association: %{[to_address: :smart_contract] => :optional}] |> DenormalizationHelper.extend_block_necessity(:required) |> Keyword.put(:paging_options, paging_options) |> Keyword.put(:from_block, from_block) @@ -35,7 +41,7 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do Transaction.address_to_transactions_without_rewards(address_hash, options) end - defp to_csv_format(transactions, address_hash, exchange_rate) do + defp to_csv_format(transactions_with_decoded_data, address_hash, exchange_rate) do row_names = [ "TxHash", "BlockNumber", @@ -50,11 +56,12 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do "ErrCode", "CurrentPrice", "TxDateOpeningPrice", - "TxDateClosingPrice" + "TxDateClosingPrice", + "MethodName" ] date_to_prices = - Enum.reduce(transactions, %{}, fn tx, acc -> + Enum.reduce(transactions_with_decoded_data, %{}, fn {_decoded_data, tx}, acc -> date = tx |> Transaction.block_timestamp() |> DateTime.to_date() if Map.has_key?(acc, date) do @@ -71,8 +78,8 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do end) transaction_lists = - transactions - |> Stream.map(fn transaction -> + transactions_with_decoded_data + |> Stream.map(fn {decoded_data, transaction} -> {opening_price, closing_price} = date_to_prices[DateTime.to_date(Transaction.block_timestamp(transaction))] [ @@ -89,7 +96,8 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do transaction.error, exchange_rate.usd_value, opening_price, - closing_price + closing_price, + Transaction.method_name(transaction, decoded_data) ] end) diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 2954241ab7..05916ff894 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -288,6 +288,7 @@ defmodule Explorer.Chain.Transaction do alias Explorer.{Chain, Helper, PagingOptions, Repo, SortingHelper} alias Explorer.Chain.{ + Address, Block, Block.Reward, ContractMethod, @@ -1900,4 +1901,59 @@ defmodule Explorer.Chain.Transaction do timeout: :infinity ) end + + @doc """ + Receives as input list of transactions and returns tuple {decoded_input_data, abi_acc, methods_acc} + Where + - `decoded_input_data` is list of results: either `{:ok, _identifier, _text, _mapping}` or `nil` + - `abi_acc` is list of all smart contracts ABIs fetched during decoding + - `methods_acc` is list of all smart contracts methods fetched from `contract_methods` table during decoding + """ + @spec decode_transactions([Transaction.t()], boolean(), Keyword.t()) :: {[any()], map(), map()} + def decode_transactions(transactions, skip_sig_provider?, opts) do + {results, abi_acc, methods_acc} = + Enum.reduce(transactions, {[], %{}, %{}}, fn transaction, {results, abi_acc, methods_acc} -> + {result, abi_acc, methods_acc} = + decoded_input_data(transaction, skip_sig_provider?, opts, abi_acc, methods_acc) + + {[format_decoded_input(result) | results], abi_acc, methods_acc} + end) + + {Enum.reverse(results), abi_acc, methods_acc} + end + + @doc """ + Receives as input result of decoded_input_data/5, returns either nil or decoded input in format: {:ok, _identifier, _text, _mapping} + """ + @spec format_decoded_input(any()) :: nil | tuple() + def format_decoded_input({:error, _, []}), do: nil + def format_decoded_input({:error, _, candidates}), do: Enum.at(candidates, 0) + def format_decoded_input({:ok, _identifier, _text, _mapping} = decoded), do: decoded + def format_decoded_input(_), do: nil + + @doc """ + Return method name used in tx + """ + @spec method_name(t(), any(), boolean()) :: binary() | nil + def method_name(_, _, skip_sc_check? \\ false) + + def method_name(_, {:ok, _method_id, text, _mapping}, _) do + parse_method_name(text, false) + end + + def method_name( + %__MODULE__{to_address: to_address, input: %{bytes: <>}}, + _, + skip_sc_check? + ) do + if skip_sc_check? || Address.smart_contract?(to_address) do + "0x" <> Base.encode16(method_id, case: :lower) + else + nil + end + end + + def method_name(_, _, _) do + nil + end end diff --git a/apps/explorer/test/explorer/chain/csv_export/address_transaction_csv_exporter_test.exs b/apps/explorer/test/explorer/chain/csv_export/address_transaction_csv_exporter_test.exs index 955cad7a06..536aa2df21 100644 --- a/apps/explorer/test/explorer/chain/csv_export/address_transaction_csv_exporter_test.exs +++ b/apps/explorer/test/explorer/chain/csv_export/address_transaction_csv_exporter_test.exs @@ -8,9 +8,34 @@ defmodule Explorer.Chain.AddressTransactionCsvExporterTest do test "exports address transactions to csv" do address = insert(:address) + insert(:contract_method, + identifier: Base.decode16!("731133e9", case: :lower), + abi: %{ + "constant" => false, + "inputs" => [ + %{"name" => "account", "type" => "address"}, + %{"name" => "id", "type" => "uint256"}, + %{"name" => "amount", "type" => "uint256"}, + %{"name" => "data", "type" => "bytes"} + ], + "name" => "mint", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + } + ) + + to_address = insert(:contract_address) + transaction = :transaction - |> insert(from_address: address) + |> insert( + from_address: address, + to_address: to_address, + input: + "0x731133e9000000000000000000000000bb36c792b9b45aaf8b848a1392b0d6559202729e000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001700000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000000000000000000000000000000000" + ) |> with_block() |> Repo.preload(:token_transfers) @@ -50,6 +75,8 @@ defmodule Explorer.Chain.AddressTransactionCsvExporterTest do [[], op_price], _, [[], cl_price], + _, + [[], method_name], _ ] -> %{ @@ -66,7 +93,8 @@ defmodule Explorer.Chain.AddressTransactionCsvExporterTest do error: error, current_price: cur_price, opening_price: op_price, - closing_price: cl_price + closing_price: cl_price, + method_name: method_name } end) @@ -84,6 +112,7 @@ defmodule Explorer.Chain.AddressTransactionCsvExporterTest do assert result.current_price assert result.opening_price assert result.closing_price + assert result.method_name == "mint" end test "fetches all transactions" do