perf: refactor tx data decoding with fewer DB queries (#10842)

* perf: refactor tx data decoding with fewer DB queries

* fix: tests

* chore: more refactor

* chore: fix merge conflicts
pull/10985/head
Kirill Fedoseev 1 month ago committed by GitHub
parent c408fc482d
commit 6d8aa194c3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 17
      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/token_transfer_controller.ex
  3. 2
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/utils_controller.ex
  4. 4
      apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex
  5. 2
      apps/block_scout_web/lib/block_scout_web/views/api/v2/suave_view.ex
  6. 16
      apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex
  7. 3
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  8. 2
      apps/block_scout_web/test/block_scout_web/controllers/transaction_token_transfer_controller_test.exs
  9. 6
      apps/explorer/lib/explorer/chain.ex
  10. 9
      apps/explorer/lib/explorer/chain/contract_method.ex
  11. 1
      apps/explorer/lib/explorer/chain/csv_export/address_transaction_csv_exporter.ex
  12. 2
      apps/explorer/lib/explorer/chain/log.ex
  13. 49
      apps/explorer/lib/explorer/chain/smart_contract/proxy.ex
  14. 8
      apps/explorer/lib/explorer/chain/smart_contract/proxy/models/implementation.ex
  15. 313
      apps/explorer/lib/explorer/chain/transaction.ex
  16. 7
      apps/explorer/test/explorer/chain/smart_contract/proxy/models/implementation_test.exs
  17. 46
      apps/explorer/test/explorer/chain/smart_contract/proxy_test.exs
  18. 16
      apps/explorer/test/explorer/chain/transaction_test.exs

@ -57,7 +57,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do
{advanced_filters, next_page} = split_list_by_page(advanced_filters_plus_one)
{decoded_transactions, _abi_acc, methods_acc} =
decoded_transactions =
advanced_filters
|> Enum.map(fn af -> %Transaction{to_address: af.to_address, input: af.input, hash: af.hash} end)
|> Transaction.decode_transactions(true, @api_true)
@ -69,7 +69,7 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do
advanced_filters: advanced_filters,
decoded_transactions: decoded_transactions,
search_params: %{
method_ids: method_id_to_name_from_params(full_options[:methods] || [], methods_acc),
method_ids: method_id_to_name_from_params(full_options[:methods] || [], decoded_transactions),
tokens: contract_address_hash_to_token_from_params(full_options[:token_contract_address_hashes])
},
next_page_params: next_page_params
@ -142,22 +142,19 @@ defmodule BlockScoutWeb.API.V2.AdvancedFilterController do
render(conn, :methods, methods: @methods)
end
defp method_id_to_name_from_params(prepared_method_ids, methods_acc) do
defp method_id_to_name_from_params(prepared_method_ids, decoded_transactions) do
{decoded_method_ids, method_ids_to_find} =
Enum.reduce(prepared_method_ids, {%{}, []}, fn method_id, {decoded, to_decode} ->
{:ok, method_id_hash} = Data.cast(method_id)
trimmed_method_id = method_id_hash.bytes |> Base.encode16(case: :lower)
case {Map.get(@methods_id_to_name_map, method_id),
methods_acc
|> Map.get(method_id_hash.bytes, [])
|> Enum.find(
&match?(%ContractMethod{abi: %{"type" => "function", "name" => name}} when is_binary(name), &1)
)} do
decoded_transactions |> Enum.find(&match?({:ok, ^trimmed_method_id, _, _}, &1))} do
{name, _} when is_binary(name) ->
{Map.put(decoded, method_id, name), to_decode}
{_, %ContractMethod{abi: %{"type" => "function", "name" => name}}} when is_binary(name) ->
{Map.put(decoded, method_id, name), to_decode}
{_, {:ok, _, function_signature, _}} when is_binary(function_signature) ->
{Map.put(decoded, method_id, function_signature |> String.split("(") |> Enum.at(0)), to_decode}
{nil, nil} ->
{decoded, [method_id_hash.bytes | to_decode]}

@ -58,7 +58,7 @@ defmodule BlockScoutWeb.API.V2.TokenTransferController do
end)
|> Enum.uniq()
{decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true)
decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
decoded_transactions_map =
transactions

@ -25,7 +25,7 @@ defmodule BlockScoutWeb.API.V2.UtilsController do
updated_smart_contract
end
{decoded_input, _abi_acc, _methods_acc} =
decoded_input =
Transaction.decoded_input_data(
%Transaction{
input: data,

@ -140,7 +140,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
end)
skip_sig_provider? = false
{decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true)
decoded_input = Transaction.decoded_input_data(transaction, skip_sig_provider?, @api_true)
decoded_input_data = decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input()
@ -383,7 +383,7 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do
skip_sig_provider? = false
{decoded_input, _abi_acc, _methods_acc} = Transaction.decoded_input_data(mock_tx, skip_sig_provider?, @api_true)
decoded_input = Transaction.decoded_input_data(mock_tx, skip_sig_provider?, @api_true)
{mock_tx, decoded_input, decoded_input |> Transaction.format_decoded_input() |> TransactionView.decoded_input()}
end

@ -27,7 +27,7 @@ defmodule BlockScoutWeb.API.V2.SuaveView do
wrapped_max_fee_per_gas = Map.get(transaction, :wrapped_max_fee_per_gas)
wrapped_value = Map.get(transaction, :wrapped_value)
{[wrapped_decoded_input], _, _} =
[wrapped_decoded_input] =
Transaction.decode_transactions(
[
%Transaction{

@ -29,7 +29,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
watchlist_names: watchlist_names
}) do
block_height = Chain.block_height(@api_true)
{decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true)
decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
%{
"items" =>
@ -49,7 +49,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
watchlist_names: watchlist_names
}) do
block_height = Chain.block_height(@api_true)
{decoded_transactions, _, _} = Transaction.decode_transactions(transactions, true, @api_true)
decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
transactions
|> chain_type_transformations()
@ -61,7 +61,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, _, _} = Transaction.decode_transactions(transactions, true, @api_true)
decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
%{
"items" =>
@ -81,7 +81,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, _, _} = Transaction.decode_transactions(transactions, true, @api_true)
decoded_transactions = Transaction.decode_transactions(transactions, true, @api_true)
transactions
|> chain_type_transformations()
@ -91,7 +91,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], _, _} = Transaction.decode_transactions([transaction], false, @api_true)
[decoded_input] = Transaction.decode_transactions([transaction], false, @api_true)
transaction
|> chain_type_transformations()
@ -115,7 +115,7 @@ 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, _, _} =
decoded_transactions =
Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true)
%{
@ -128,7 +128,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
end
def render("token_transfers.json", %{token_transfers: token_transfers, conn: conn}) do
{decoded_transactions, _, _} =
decoded_transactions =
Transaction.decode_transactions(Enum.map(token_transfers, fn tt -> tt.transaction end), true, @api_true)
token_transfers
@ -137,7 +137,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
end
def render("token_transfer.json", %{token_transfer: token_transfer, conn: conn}) do
{[decoded_transaction], _, _} = Transaction.decode_transactions([token_transfer.transaction], true, @api_true)
[decoded_transaction] = Transaction.decode_transactions([token_transfer.transaction], true, @api_true)
TokenTransferView.prepare_token_transfer(token_transfer, conn, decoded_transaction)
end

@ -395,8 +395,7 @@ defmodule BlockScoutWeb.TransactionView do
end
def decoded_input_data(transaction) do
{result, _, _} = Transaction.decoded_input_data(transaction, [])
result
Transaction.decoded_input_data(transaction, [])
end
def decoded_revert_reason(revert_reason, transaction, options) do

@ -159,6 +159,8 @@ defmodule BlockScoutWeb.TransactionTokenTransferControllerTest do
end
test "preloads to_address smart contract verified", %{conn: conn} do
TestHelper.get_eip1967_implementation_zero_addresses()
transaction = insert(:transaction_to_verified_contract)
conn = get(conn, transaction_token_transfer_path(BlockScoutWeb.Endpoint, :index, transaction.hash))

@ -1095,7 +1095,11 @@ defmodule Explorer.Chain do
"""
@spec hashes_to_addresses([Hash.Address.t()], [necessity_by_association_option | api?]) :: [Address.t()]
def hashes_to_addresses(hashes, options \\ []) when is_list(hashes) do
def hashes_to_addresses(hashes, options \\ [])
def hashes_to_addresses([], _), do: []
def hashes_to_addresses(hashes, options) when is_list(hashes) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
query =

@ -109,14 +109,19 @@ defmodule Explorer.Chain.ContractMethod do
@doc """
Finds contract methods by selector id
"""
@spec find_contract_methods(binary(), [Chain.api?()]) :: [__MODULE__.t()]
@spec find_contract_methods([binary()], [Chain.api?()]) :: [__MODULE__.t()]
def find_contract_methods(method_ids, options)
def find_contract_methods([], _), do: []
def find_contract_methods(method_ids, options) do
query =
from(
contract_method in __MODULE__,
distinct: contract_method.identifier,
where: contract_method.abi["type"] == "function",
where: contract_method.identifier in ^method_ids
where: contract_method.identifier in ^method_ids,
order_by: [asc: contract_method.identifier, asc: contract_method.inserted_at]
)
Chain.select_repo(options).all(query)

@ -19,7 +19,6 @@ defmodule Explorer.Chain.CSVExport.AddressTransactionCsvExporter do
transactions
|> Transaction.decode_transactions(true, api?: true)
|> elem(0)
|> Enum.zip(transactions)
|> to_csv_format(address_hash, exchange_rate)
|> Helper.dump_to_stream()

@ -224,7 +224,7 @@ defmodule Explorer.Chain.Log do
else
case Chain.find_contract_address(address_hash, address_options, false) do
{:ok, %{smart_contract: smart_contract}} ->
full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, %{}, true, options)
full_abi = Proxy.combine_proxy_implementation_abi(smart_contract, options)
{full_abi, Map.put(acc, address_hash, full_abi)}
_ ->

@ -498,54 +498,15 @@ defmodule Explorer.Chain.SmartContract.Proxy do
@doc """
Returns combined ABI from proxy and implementation smart-contracts
"""
@spec combine_proxy_implementation_abi(any(), map(), boolean(), any()) :: SmartContract.abi()
@spec combine_proxy_implementation_abi(any(), any()) :: SmartContract.abi()
def combine_proxy_implementation_abi(
smart_contract,
proxy_implementation_addresses_map \\ %{},
fetch_proxy?,
options \\ []
)
def combine_proxy_implementation_abi(
%SmartContract{abi: abi} = smart_contract,
proxy_implementation_addresses_map,
fetch_proxy?,
options
)
when not is_nil(abi) do
implementation_abi =
get_implementation_abi(smart_contract, options, proxy_implementation_addresses_map, fetch_proxy?)
if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi
end
def combine_proxy_implementation_abi(smart_contract, proxy_implementation_addresses_map, fetch_proxy?, options) do
get_implementation_abi(smart_contract, options, proxy_implementation_addresses_map, fetch_proxy?)
end
defp get_implementation_abi(smart_contract, options, proxy_implementation_addresses_map, fetch_proxy?) do
if fetch_proxy? do
Proxy.get_implementation_abi_from_proxy(smart_contract, options)
else
implementations =
proxy_implementation_addresses_map
|> Map.get(smart_contract.address_hash)
parse_abi_from_proxy_implementations(implementations)
end
end
defp parse_abi_from_proxy_implementations(nil), do: []
) do
proxy_abi = (smart_contract && smart_contract.abi) || []
implementation_abi = Proxy.get_implementation_abi_from_proxy(smart_contract, options)
defp parse_abi_from_proxy_implementations(implementations) do
implementations
|> Enum.reduce([], fn implementation, acc ->
if implementation.smart_contract && implementation.smart_contract.abi do
acc ++ implementation.smart_contract.abi
else
acc
end
end)
proxy_abi ++ implementation_abi
end
defp find_input_by_name(inputs, name) do

@ -94,8 +94,12 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation do
@doc """
Returns all implementations for the given smart-contract address hashes
"""
@spec get_proxy_implementations_for_multiple_proxies([Hash.Address.t()], Keyword.t()) :: __MODULE__.t() | nil
def get_proxy_implementations_for_multiple_proxies(proxy_address_hashes, options \\ []) do
@spec get_proxy_implementations_for_multiple_proxies([Hash.Address.t()], Keyword.t()) :: [__MODULE__.t()]
def get_proxy_implementations_for_multiple_proxies(proxy_address_hashes, options \\ [])
def get_proxy_implementations_for_multiple_proxies([], _), do: []
def get_proxy_implementations_for_multiple_proxies(proxy_address_hashes, options) do
proxy_address_hashes
|> get_proxy_implementations_by_multiple_hashes_query()
|> select_repo(options).all()

@ -303,7 +303,6 @@ defmodule Explorer.Chain.Transaction do
Data,
DenormalizationHelper,
Hash,
SmartContract,
SmartContract.Proxy,
TokenTransfer,
Transaction,
@ -737,17 +736,14 @@ defmodule Explorer.Chain.Transaction do
{:ok, identifier, text, mapping}
_ ->
{result, _, _} =
decoded_input_data(
%Transaction{
to_address: smart_contract,
hash: hash,
input: %Data{bytes: binary_revert_reason}
},
options
)
result
decoded_input_data(
%Transaction{
to_address: smart_contract,
hash: hash,
input: %Data{bytes: binary_revert_reason}
},
options
)
end
_ ->
@ -760,23 +756,19 @@ defmodule Explorer.Chain.Transaction do
NotLoaded.t() | Transaction.t(),
boolean(),
[Chain.api?()],
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
) ::
{error_type | success_type, full_abi_acc, methods_acc}
when full_abi_acc: map(),
methods_acc: map(),
proxy_implementation_addresses_map: map(),
methods_map,
proxy_implementation_abi_map
) :: error_type | success_type
when methods_map: map(),
proxy_implementation_abi_map: map(),
error_type: {:error, any()} | {:error, :contract_not_verified | :contract_verified, list()},
success_type: {:ok | binary(), any()} | {:ok, binary(), binary(), list()}
def decoded_input_data(
tx,
skip_sig_provider? \\ false,
options,
full_abi_acc \\ %{},
methods_acc \\ %{},
proxy_implementation_addresses_map \\ %{}
methods_map \\ %{},
proxy_implementation_abi_map \\ %{}
)
# skip decoding if there is no to_address
@ -784,27 +776,25 @@ defmodule Explorer.Chain.Transaction do
%__MODULE__{to_address: nil},
_,
_,
full_abi_acc,
methods_acc,
_proxy_implementation_addresses_map
_,
_
),
do: {{:error, :no_to_address}, full_abi_acc, methods_acc}
do: {:error, :no_to_address}
# skip decoding if transaction is not loaded
def decoded_input_data(%NotLoaded{}, _, _, full_abi_acc, methods_acc, _proxy_implementation_addresses_map),
do: {{:error, :not_loaded}, full_abi_acc, methods_acc}
def decoded_input_data(%NotLoaded{}, _, _, _, _),
do: {:error, :not_loaded}
# skip decoding if input is empty
def decoded_input_data(
%__MODULE__{input: %{bytes: bytes}},
_,
_,
full_abi_acc,
methods_acc,
_proxy_implementation_addresses_map
_,
_
)
when bytes in [nil, <<>>] do
{{:error, :no_input_data}, full_abi_acc, methods_acc}
{:error, :no_input_data}
end
# skip decoding if to_address is not a contract unless DECODE_NOT_A_CONTRACT_CALLS is set
@ -813,11 +803,10 @@ defmodule Explorer.Chain.Transaction do
%__MODULE__{to_address: %{contract_code: nil}},
_,
_,
full_abi_acc,
methods_acc,
_proxy_implementation_addresses_map
_,
_
),
do: {{:error, :not_a_contract_call}, full_abi_acc, methods_acc}
do: {:error, :not_a_contract_call}
end
# if to_address's smart_contract is nil reduce to the case when to_address is not loaded
@ -829,9 +818,8 @@ defmodule Explorer.Chain.Transaction do
},
skip_sig_provider?,
options,
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
methods_map,
proxy_implementation_abi_map
) do
decoded_input_data(
%__MODULE__{
@ -841,9 +829,8 @@ defmodule Explorer.Chain.Transaction do
},
skip_sig_provider?,
options,
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
methods_map,
proxy_implementation_abi_map
)
end
@ -856,9 +843,8 @@ defmodule Explorer.Chain.Transaction do
},
skip_sig_provider?,
options,
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
methods_map,
proxy_implementation_abi_map
) do
decoded_input_data(
%__MODULE__{
@ -868,9 +854,8 @@ defmodule Explorer.Chain.Transaction do
},
skip_sig_provider?,
options,
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
methods_map,
proxy_implementation_abi_map
)
end
@ -883,33 +868,26 @@ defmodule Explorer.Chain.Transaction do
},
skip_sig_provider?,
options,
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
methods_map,
_proxy_implementation_abi_map
) do
{methods, methods_acc} =
method_id
|> check_methods_cache(methods_acc, options)
methods = check_methods_cache(method_id, methods_map, options)
candidates =
methods
|> Enum.flat_map(fn candidate ->
case do_decoded_input_data(
data,
%SmartContract{abi: [candidate.abi], address_hash: nil},
hash,
options,
%{},
proxy_implementation_addresses_map
[candidate.abi],
hash
) do
{{:ok, _, _, _} = decoded, _} -> [decoded]
{:ok, _, _, _} = decoded -> [decoded]
_ -> []
end
end)
{{:error, :contract_not_verified,
if(candidates == [], do: decode_function_call_via_sig_provider(input, hash, skip_sig_provider?), else: candidates)},
full_abi_acc, methods_acc}
{:error, :contract_not_verified,
if(candidates == [], do: decode_function_call_via_sig_provider(input, hash, skip_sig_provider?), else: candidates)}
end
# if to_address is not loaded and input is not a method call return error
@ -917,11 +895,10 @@ defmodule Explorer.Chain.Transaction do
%__MODULE__{to_address: %NotLoaded{}},
_,
_,
full_abi_acc,
methods_acc,
_proxy_implementation_addresses_map
_,
_
) do
{{:error, :contract_not_verified, []}, full_abi_acc, methods_acc}
{:error, :contract_not_verified, []}
end
def decoded_input_data(
@ -932,20 +909,14 @@ defmodule Explorer.Chain.Transaction do
},
skip_sig_provider?,
options,
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
methods_map,
proxy_implementation_abi_map
) do
case do_decoded_input_data(
data,
smart_contract,
hash,
options,
full_abi_acc,
proxy_implementation_addresses_map
) do
full_abi = check_full_abi_cache(smart_contract, proxy_implementation_abi_map, options)
case do_decoded_input_data(data, full_abi, hash) do
# In some cases transactions use methods of some unpredictable contracts, so we can try to look up for method in a whole DB
{{:error, error}, full_abi_acc} when error in [:could_not_decode, :no_matching_function] ->
{:error, error} when error in [:could_not_decode, :no_matching_function] ->
case decoded_input_data(
%__MODULE__{
to_address: %NotLoaded{},
@ -954,22 +925,21 @@ defmodule Explorer.Chain.Transaction do
},
skip_sig_provider?,
options,
full_abi_acc,
methods_acc,
proxy_implementation_addresses_map
methods_map,
proxy_implementation_abi_map
) do
{{:error, :contract_not_verified, []}, full_abi_acc, methods_acc} ->
{decode_function_call_via_sig_provider_wrapper(input, hash, skip_sig_provider?), full_abi_acc, methods_acc}
{:error, :contract_not_verified, []} ->
decode_function_call_via_sig_provider_wrapper(input, hash, skip_sig_provider?)
{{:error, :contract_not_verified, candidates}, full_abi_acc, methods_acc} ->
{{:error, :contract_verified, candidates}, full_abi_acc, methods_acc}
{:error, :contract_not_verified, candidates} ->
{:error, :contract_verified, candidates}
{_, full_abi_acc, methods_acc} ->
{{:error, :could_not_decode}, full_abi_acc, methods_acc}
_ ->
{:error, :could_not_decode}
end
{output, full_abi_acc} ->
{output, full_abi_acc, methods_acc}
output ->
output
end
end
@ -983,24 +953,13 @@ defmodule Explorer.Chain.Transaction do
end
end
defp do_decoded_input_data(
data,
smart_contract,
hash,
options,
full_abi_acc,
proxy_implementation_addresses_map \\ %{}
) do
{full_abi, full_abi_acc} =
check_full_abi_cache(smart_contract, full_abi_acc, options, proxy_implementation_addresses_map)
{with(
{:ok, {selector, values}} <- find_and_decode(full_abi, data, hash),
{:ok, mapping} <- selector_mapping(selector, values, hash),
identifier <- Base.encode16(selector.method_id, case: :lower),
text <- function_call(selector.function, mapping),
do: {:ok, identifier, text, mapping}
), full_abi_acc}
defp do_decoded_input_data(data, full_abi, hash) do
with {:ok, {selector, values}} <- find_and_decode(full_abi, data, hash),
{:ok, mapping} <- selector_mapping(selector, values, hash),
identifier <- Base.encode16(selector.method_id, case: :lower),
text <- function_call(selector.function, mapping) do
{:ok, identifier, text, mapping}
end
end
defp decode_function_call_via_sig_provider(%{bytes: data} = input, hash, skip_sig_provider?) do
@ -1010,8 +969,7 @@ defmodule Explorer.Chain.Transaction do
true <- is_list(result),
false <- Enum.empty?(result),
abi <- [result |> List.first() |> Map.put("outputs", []) |> Map.put("type", "function")],
{{:ok, _, _, _} = candidate, _} <-
do_decoded_input_data(data, %SmartContract{abi: abi, address_hash: nil}, hash, [], %{}) do
{:ok, _, _, _} = candidate <- do_decoded_input_data(data, abi, hash) do
[candidate]
else
_ ->
@ -1019,39 +977,22 @@ defmodule Explorer.Chain.Transaction do
end
end
defp check_methods_cache(method_id, methods_acc, options) do
if Map.has_key?(methods_acc, method_id) do
{methods_acc[method_id], methods_acc}
else
candidates_query = ContractMethod.find_contract_method_query(method_id, 1)
result =
candidates_query
|> Chain.select_repo(options).all()
{result, Map.put(methods_acc, method_id, result)}
end
defp check_methods_cache(method_id, methods_map, options) do
Map.get_lazy(methods_map, method_id, fn ->
method_id
|> ContractMethod.find_contract_method_query(1)
|> Chain.select_repo(options).all()
end)
end
defp check_full_abi_cache(
%{address_hash: address_hash} = smart_contract,
full_abi_acc,
options,
proxy_implementation_addresses_map
smart_contract,
proxy_implementation_abi_map,
options
) do
if !is_nil(address_hash) && Map.has_key?(full_abi_acc, address_hash) do
{full_abi_acc[address_hash], full_abi_acc}
else
full_abi =
Proxy.combine_proxy_implementation_abi(
smart_contract,
proxy_implementation_addresses_map,
false,
options
)
{full_abi, Map.put(full_abi_acc, address_hash, full_abi)}
end
Map.get_lazy(proxy_implementation_abi_map, smart_contract, fn ->
Proxy.combine_proxy_implementation_abi(smart_contract, options)
end)
end
def get_method_name(
@ -1071,10 +1012,10 @@ defmodule Explorer.Chain.Transaction do
true,
[]
) do
{{:error, :contract_not_verified, [{:ok, _method_id, decoded_func, _}]}, _, _} ->
{:error, :contract_not_verified, [{:ok, _method_id, decoded_func, _}]} ->
parse_method_name(decoded_func)
{{:error, :contract_not_verified, []}, _, _} ->
{:error, :contract_not_verified, []} ->
"0x" <> Base.encode16(method_id, case: :lower)
_ ->
@ -2000,35 +1941,59 @@ defmodule Explorer.Chain.Transaction do
end
@doc """
Receives as input list of transactions and returns tuple {decoded_input_data, abi_acc, methods_acc}
Receives as input list of transactions and returns decoded_input_data
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()}
@spec decode_transactions([Transaction.t()], boolean(), Keyword.t()) :: [nil | {:ok, String.t(), String.t(), map()}]
def decode_transactions(transactions, skip_sig_provider?, opts) do
proxy_implementation_addresses_map = combine_proxy_implementation_addresses_map(transactions)
{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,
proxy_implementation_addresses_map
)
proxy_implementation_abi_map = combine_proxy_implementation_abi_map(transactions)
{[format_decoded_input(result) | results], abi_acc, methods_acc}
# first we assemble an empty methods map, so that decoded_input_data will skip ContractMethod.t() lookup and decoding
empty_methods_map =
transactions
|> Enum.flat_map(fn
%{input: <<method_id::binary-size(4), _::binary>>} -> [method_id]
_ -> []
end)
|> Enum.into(%{}, &{&1, []})
{Enum.reverse(results), abi_acc, methods_acc}
# try to decode transaction using full abi data from proxy_implementation_abi_map
decoded_transactions =
transactions
|> Enum.map(fn transaction ->
transaction
|> decoded_input_data(skip_sig_provider?, opts, empty_methods_map, proxy_implementation_abi_map)
|> format_decoded_input()
end)
|> Enum.zip(transactions)
# assemble a new methods map from methods in non-decoded transactions
methods_map =
decoded_transactions
|> Enum.flat_map(fn
{nil, %{input: <<method_id::binary-size(4), _::binary>>}} -> [method_id]
_ -> []
end)
|> Enum.uniq()
|> ContractMethod.find_contract_methods(opts)
|> Enum.into(%{}, &{&1.identifier, [&1]})
# decode remaining transaction using methods map
decoded_transactions
|> Enum.map(fn
{nil, transaction} ->
transaction
|> Map.put(:to_address, %NotLoaded{})
|> decoded_input_data(skip_sig_provider?, opts, methods_map, proxy_implementation_abi_map)
|> format_decoded_input()
{decoded, _} ->
decoded
end)
end
defp combine_proxy_implementation_addresses_map(transactions) do
defp combine_proxy_implementation_abi_map(transactions) do
# parse unique address hashes of smart-contracts from to_address and created_contract_address properties of the transactions list
unique_to_address_hashes =
transactions
@ -2043,32 +2008,34 @@ defmodule Explorer.Chain.Transaction do
multiple_proxy_implementations =
Implementation.get_proxy_implementations_for_multiple_proxies(unique_to_address_hashes)
# query from the DB address objects with smart_contract preload for all found above implementation addresses
implementation_addresses_with_smart_contracts =
# query from the DB address objects with smart_contract preload for all found above proxy and implementation addresses
addresses_with_smart_contracts =
multiple_proxy_implementations
|> Enum.flat_map(fn proxy_implementations -> proxy_implementations.address_hashes end)
|> Enum.concat(unique_to_address_hashes)
|> Chain.hashes_to_addresses(necessity_by_association: %{smart_contract: :optional})
|> Enum.into(%{}, &{&1.hash, &1})
# combine map %{proxy_address_hash => the list of implementations as Address.t() object with preloaded SmartContract.t()}
# combine map %{proxy_address_hash => combined proxy abi}
multiple_proxy_implementations
|> Enum.reduce(%{}, fn proxy_implementations, proxy_implementation_addresses_map ->
implementation_addresses_with_smart_contract_preload =
proxy_implementations.address_hashes
|> Enum.map(fn implementation_address_hash ->
Map.get(implementation_addresses_with_smart_contracts, implementation_address_hash)
|> Enum.into(%{}, fn proxy_implementations ->
full_abi =
[proxy_implementations.proxy_address_hash | proxy_implementations.address_hashes]
|> Enum.map(&Map.get(addresses_with_smart_contracts, &1))
|> Enum.flat_map(fn
%{smart_contract: %{abi: abi}} when is_list(abi) -> abi
_ -> []
end)
|> Enum.filter(&(!is_nil(&1)))
proxy_implementation_addresses_map
|> Map.put(proxy_implementations.proxy_address_hash, implementation_addresses_with_smart_contract_preload)
{proxy_implementations.proxy_address_hash, full_abi}
end)
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()
@spec format_decoded_input(any()) :: nil | {:ok, String.t(), String.t(), map()}
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

@ -102,6 +102,13 @@ defmodule Explorer.Chain.SmartContract.Proxy.Models.Implementation.Test do
assert implementation_1.updated_at == implementation_2.updated_at &&
contract_1.updated_at == contract_2.updated_at
proxy =
:explorer
|> Application.get_env(:proxy)
|> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20))
Application.put_env(:explorer, :proxy, proxy)
end
test "get_implementation/1 for twins contract" do

@ -131,36 +131,36 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
}
]
test "combine_proxy_implementation_abi/4 returns empty [] abi if proxy abi is null" do
test "combine_proxy_implementation_abi/2 returns empty [] abi if proxy abi is null" do
proxy_contract_address = insert(:contract_address)
assert Proxy.combine_proxy_implementation_abi(
%SmartContract{address_hash: proxy_contract_address.hash, abi: nil},
%{},
false
) ==
assert Proxy.combine_proxy_implementation_abi(%SmartContract{address_hash: proxy_contract_address.hash, abi: nil}) ==
[]
end
test "combine_proxy_implementation_abi/4 returns [] abi for unverified proxy" do
test "combine_proxy_implementation_abi/2 returns [] abi for unverified proxy" do
TestHelper.get_eip1967_implementation_zero_addresses()
proxy_contract_address = insert(:contract_address)
smart_contract =
insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123")
assert Proxy.combine_proxy_implementation_abi(smart_contract, %{}, false) == []
assert Proxy.combine_proxy_implementation_abi(smart_contract) == []
end
test "combine_proxy_implementation_abi/4 returns proxy abi if implementation is not verified" do
test "combine_proxy_implementation_abi/2 returns proxy abi if implementation is not verified" do
TestHelper.get_eip1967_implementation_zero_addresses()
proxy_contract_address = insert(:contract_address)
smart_contract =
insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
assert Proxy.combine_proxy_implementation_abi(smart_contract, %{}, false) == @proxy_abi
assert Proxy.combine_proxy_implementation_abi(smart_contract) == @proxy_abi
end
test "combine_proxy_implementation_abi/4 returns proxy + implementation abi if implementation is verified" do
test "combine_proxy_implementation_abi/2 returns proxy + implementation abi if implementation is verified" do
proxy_contract_address = insert(:contract_address)
proxy_smart_contract =
@ -176,9 +176,6 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
name: "impl"
)
implementation_contract_address_with_smart_contract_preload =
implementation_contract_address |> Repo.preload(:smart_contract)
insert(:proxy_implementation,
proxy_address_hash: proxy_contract_address.hash,
proxy_type: "eip1167",
@ -186,19 +183,7 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
names: [implementation_smart_contract.name]
)
_implementation_contract_address_hash_string =
Base.encode16(implementation_contract_address.hash.bytes, case: :lower)
proxy_implementation_addresses_map =
%{}
|> Map.put(proxy_contract_address.hash, [implementation_contract_address_with_smart_contract_preload])
combined_abi =
Proxy.combine_proxy_implementation_abi(
proxy_smart_contract,
proxy_implementation_addresses_map,
false
)
combined_abi = Proxy.combine_proxy_implementation_abi(proxy_smart_contract)
assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == false
assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == false
@ -482,6 +467,13 @@ defmodule Explorer.Chain.SmartContract.ProxyTest do
assert Proxy.proxy_contract?(smart_contract)
verify!(EthereumJSONRPC.Mox)
proxy =
:explorer
|> Application.get_env(:proxy)
|> Keyword.replace(:fallback_cached_implementation_data_ttl, :timer.seconds(20))
Application.put_env(:explorer, :proxy, proxy)
end
defp eip_1967_beacon_proxy_mock_requests(

@ -252,7 +252,7 @@ defmodule Explorer.Chain.TransactionTest do
test "that a transaction that is not a contract call returns a commensurate error" do
transaction = insert(:transaction)
assert {{:error, :not_a_contract_call}, _, _} = Transaction.decoded_input_data(transaction, [])
assert {:error, :not_a_contract_call} = Transaction.decoded_input_data(transaction, [])
end
test "that a contract call transaction that has no verified contract returns a commensurate error" do
@ -261,20 +261,24 @@ defmodule Explorer.Chain.TransactionTest do
|> insert(to_address: insert(:contract_address), input: "0x1234567891")
|> Repo.preload(to_address: :smart_contract)
assert {{:error, :contract_not_verified, []}, _, _} = Transaction.decoded_input_data(transaction, [])
assert {:error, :contract_not_verified, []} = Transaction.decoded_input_data(transaction, [])
end
test "that a contract call transaction that has a verified contract returns the decoded input data" do
TestHelper.get_eip1967_implementation_zero_addresses()
transaction =
:transaction_to_verified_contract
|> insert()
|> Repo.preload(to_address: :smart_contract)
assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 50}]}, _, _} =
assert {:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 50}]} =
Transaction.decoded_input_data(transaction, [])
end
test "that a contract call will look up a match in contract_methods table" do
TestHelper.get_eip1967_implementation_zero_addresses()
:transaction_to_verified_contract
|> insert()
|> Repo.preload(to_address: :smart_contract)
@ -291,11 +295,13 @@ defmodule Explorer.Chain.TransactionTest do
|> insert(to_address: contract.address, input: "0x" <> input_data)
|> Repo.preload(to_address: :smart_contract)
assert {{:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 10}]}, _, _} =
assert {:ok, "60fe47b1", "set(uint256 x)", [{"x", "uint256", 10}]} =
Transaction.decoded_input_data(transaction, [])
end
test "arguments name in function call replaced with argN if it's empty string" do
TestHelper.get_eip1967_implementation_zero_addresses()
contract =
insert(:smart_contract,
contract_code_md5: "123",
@ -323,7 +329,7 @@ defmodule Explorer.Chain.TransactionTest do
|> insert(to_address: contract.address, input: "0x" <> input_data)
|> Repo.preload(to_address: :smart_contract)
assert {{:ok, "60fe47b1", "set(uint256 arg0)", [{"arg0", "uint256", 10}]}, _, _} =
assert {:ok, "60fe47b1", "set(uint256 arg0)", [{"arg0", "uint256", 10}]} =
Transaction.decoded_input_data(transaction, [])
end
end

Loading…
Cancel
Save