feat: Add method name to transactions CSV export (#10579)

* feat: Add method name to transactions CSV export

* Add doc
pull/10603/head
nikitosing 3 months ago committed by GitHub
parent c9ddc19b22
commit 5785ea89be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 4
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/advanced_filter_controller.ex
  2. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex
  3. 8
      apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex
  4. 4
      apps/block_scout_web/lib/block_scout_web/views/api/v2/advanced_filter_view.ex
  5. 7
      apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex
  6. 66
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  7. 26
      apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex
  8. 56
      apps/explorer/lib/explorer/chain/transaction.ex
  9. 33
      apps/explorer/test/explorer/chain/csv_export/address_transaction_csv_exporter_test.exs

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

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

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

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

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

@ -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: <<method_id::binary-size(4), _::binary>>}},
_,
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.
"""

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

@ -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: <<method_id::binary-size(4), _::binary>>}},
_,
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

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

Loading…
Cancel
Save