diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex index 8a8036f2b4..d2aa6fb07b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/contract_controller.ex @@ -7,7 +7,7 @@ defmodule BlockScoutWeb.API.RPC.ContractController do def listcontracts(conn, params) do with pagination_options <- Helpers.put_pagination_options(%{}, params), - {:params, {:ok, options}} <- {:params, add_filter(pagination_options, params)} do + {:params, {:ok, options}} <- {:params, add_filters(pagination_options, params)} do options_with_defaults = options |> Map.put_new(:page_number, 0) @@ -71,7 +71,8 @@ defmodule BlockScoutWeb.API.RPC.ContractController do Chain.list_verified_contracts(page_size, offset) :decompiled -> - Chain.list_decompiled_contracts(page_size, offset) + not_decompiled_with_version = Map.get(opts, :not_decompiled_with_version) + Chain.list_decompiled_contracts(page_size, offset, not_decompiled_with_version) :unverified -> Chain.list_unverified_contracts(page_size, offset) @@ -84,6 +85,12 @@ defmodule BlockScoutWeb.API.RPC.ContractController do end end + defp add_filters(options, params) do + options + |> add_filter(params) + |> add_not_decompiled_with_version(params) + end + defp add_filter(options, params) do with {:param, {:ok, value}} <- {:param, Map.fetch(params, "filter")}, {:validation, {:ok, filter}} <- {:validation, contracts_filter(value)} do @@ -94,6 +101,17 @@ defmodule BlockScoutWeb.API.RPC.ContractController do end end + defp add_not_decompiled_with_version({:ok, options}, params) do + case Map.fetch(params, "not_decompiled_with_version") do + {:ok, value} -> {:ok, Map.put(options, :not_decompiled_with_version, value)} + :error -> {:ok, options} + end + end + + defp add_not_decompiled_with_version(options, _params) do + options + end + defp contracts_filter(nil), do: {:ok, nil} defp contracts_filter(1), do: {:ok, :verified} defp contracts_filter(2), do: {:ok, :decompiled} diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex index 75bb51fa50..b1db31a8f3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/rpc_translator.ex @@ -66,6 +66,7 @@ defmodule BlockScoutWeb.API.RPC.RPCTranslator do def call_controller(conn, controller, action) do {:ok, controller.call(conn, action)} rescue - Conn.WrapperError -> :error + Conn.WrapperError -> + :error end end diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 4162a77e70..07b7b5056b 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -1719,6 +1719,12 @@ defmodule BlockScoutWeb.Etherscan do type: "string", description: "verified|decompiled|unverified|not_decompiled, or 1|2|3|4 respectively. This requests only contracts with that status." + }, + %{ + key: "not_decompiled_with_version", + type: "string", + description: + "Ensures that none of the returned contracts were decompiled with the provided version. Ignored unless filtering for decompiled contracts." } ], responses: [ diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex index fbe1813dce..c2c593a313 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex @@ -36,19 +36,27 @@ defmodule BlockScoutWeb.API.RPC.ContractView do end defp prepare_source_code_contract(contract, _) do + decompiled_smart_contract = latest_decompiled_smart_contract(contract.decompiled_smart_contracts) + %{ "Address" => to_string(contract.address_hash), "SourceCode" => contract.contract_source_code, "ABI" => Jason.encode!(contract.abi), "ContractName" => contract.name, - "DecompiledSourceCode" => decompiled_source_code(contract.decompiled_smart_contract), - "DecompilerVersion" => decompiler_version(contract.decompiled_smart_contract), + "DecompiledSourceCode" => decompiled_source_code(decompiled_smart_contract), + "DecompilerVersion" => decompiler_version(decompiled_smart_contract), "CompilerVersion" => contract.compiler_version, "OptimizationUsed" => if(contract.optimization, do: "1", else: "0") } end - defp prepare_contract(%Address{hash: hash, smart_contract: nil, decompiled_smart_contract: decompiled_smart_contract}) do + defp prepare_contract(%Address{ + hash: hash, + smart_contract: nil, + decompiled_smart_contracts: decompiled_smart_contracts + }) do + decompiled_smart_contract = latest_decompiled_smart_contract(decompiled_smart_contracts) + %{ "Address" => to_string(hash), "SourceCode" => "", @@ -64,8 +72,10 @@ defmodule BlockScoutWeb.API.RPC.ContractView do defp prepare_contract(%Address{ hash: hash, smart_contract: %SmartContract{} = contract, - decompiled_smart_contract: decompiled_smart_contract + decompiled_smart_contracts: decompiled_smart_contracts }) do + decompiled_smart_contract = latest_decompiled_smart_contract(decompiled_smart_contracts) + %{ "Address" => to_string(hash), "SourceCode" => contract.contract_source_code, @@ -78,6 +88,12 @@ defmodule BlockScoutWeb.API.RPC.ContractView do } end + defp latest_decompiled_smart_contract([]), do: nil + + defp latest_decompiled_smart_contract(contracts) do + Enum.max_by(contracts, fn contract -> DateTime.to_unix(contract.inserted_at) end) + end + defp decompiled_source_code(nil), do: "Contract source code not decompiled." defp decompiled_source_code(%DecompiledSmartContract{decompiled_source_code: decompiled_source_code}) do diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs index 766a691a62..d299d6bc73 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/rpc/contract_controller_test.exs @@ -157,6 +157,60 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do ] end + test "filtering for only decompiled contracts, with a decompiled with version filter", %{params: params, conn: conn} do + insert(:decompiled_smart_contract, decompiler_version: "foobar") + smart_contract = insert(:decompiled_smart_contract, decompiler_version: "bizbuz") + + response = + conn + |> get("/api", Map.merge(params, %{"filter" => "decompiled", "not_decompiled_with_version" => "foobar"})) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"] == [ + %{ + "ABI" => "Contract source code not verified", + "Address" => to_string(smart_contract.address_hash), + "CompilerVersion" => "", + "ContractName" => "", + "DecompiledSourceCode" => smart_contract.decompiled_source_code, + "DecompilerVersion" => "bizbuz", + "OptimizationUsed" => "", + "SourceCode" => "" + } + ] + end + + test "filtering for only decompiled contracts, with a decompiled with version filter, where another decompiled version exists", + %{params: params, conn: conn} do + non_match = insert(:decompiled_smart_contract, decompiler_version: "foobar") + insert(:decompiled_smart_contract, decompiler_version: "bizbuz", address_hash: non_match.address_hash) + smart_contract = insert(:decompiled_smart_contract, decompiler_version: "bizbuz") + + response = + conn + |> get("/api", Map.merge(params, %{"filter" => "decompiled", "not_decompiled_with_version" => "foobar"})) + |> json_response(200) + + assert response["message"] == "OK" + assert response["status"] == "1" + + assert response["result"] == [ + %{ + "ABI" => "Contract source code not verified", + "Address" => to_string(smart_contract.address_hash), + "CompilerVersion" => "", + "ContractName" => "", + "DecompiledSourceCode" => smart_contract.decompiled_source_code, + "DecompilerVersion" => "bizbuz", + "OptimizationUsed" => "", + "SourceCode" => "" + } + ] + end + test "filtering for only not_decompiled (and by extension not verified contracts)", %{params: params, conn: conn} do insert(:decompiled_smart_contract) insert(:smart_contract) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index fcbd813ef4..34f755f276 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -2666,19 +2666,38 @@ defmodule Explorer.Chain do Repo.all(query, timeout: :infinity) end - def list_decompiled_contracts(limit, offset) do + def list_decompiled_contracts(limit, offset, not_decompiled_with_version \\ nil) do query = from( address in Address, - join: decompiled_smart_contract in DecompiledSmartContract, - on: decompiled_smart_contract.address_hash == address.hash, - preload: [{:decompiled_smart_contract, decompiled_smart_contract}, :smart_contract], + where: + fragment( + "EXISTS (SELECT 1 FROM decompiled_smart_contracts WHERE decompiled_smart_contracts.address_hash = ?)", + address.hash + ), + preload: [:decompiled_smart_contracts, :smart_contract], order_by: [asc: address.inserted_at], limit: ^limit, offset: ^offset ) - Repo.all(query) + query + |> filter_decompiled_with_version(not_decompiled_with_version) + |> Repo.all() + end + + defp filter_decompiled_with_version(query, nil) do + query + end + + defp filter_decompiled_with_version(query, not_decompiled_with_version) do + from(address in query, + left_join: decompiled_smart_contract in DecompiledSmartContract, + on: decompiled_smart_contract.decompiler_version == ^not_decompiled_with_version, + on: decompiled_smart_contract.address_hash == address.hash, + where: is_nil(decompiled_smart_contract.id), + distinct: [address.hash] + ) end def list_verified_contracts(limit, offset) do @@ -2688,7 +2707,7 @@ defmodule Explorer.Chain do where: not is_nil(address.contract_code), join: smart_contract in SmartContract, on: smart_contract.address_hash == address.hash, - preload: [{:smart_contract, smart_contract}, :decompiled_smart_contract], + preload: [{:smart_contract, smart_contract}, :decompiled_smart_contracts], order_by: [asc: address.inserted_at], limit: ^limit, offset: ^offset @@ -2702,7 +2721,7 @@ defmodule Explorer.Chain do from( address in Address, where: not is_nil(address.contract_code), - preload: [:smart_contract, :decompiled_smart_contract], + preload: [:smart_contract, :decompiled_smart_contracts], order_by: [asc: address.inserted_at], limit: ^limit, offset: ^offset @@ -2719,7 +2738,7 @@ defmodule Explorer.Chain do on: smart_contract.address_hash == address.hash, where: not is_nil(address.contract_code), where: is_nil(smart_contract.address_hash), - preload: [{:smart_contract, smart_contract}, :decompiled_smart_contract], + preload: [{:smart_contract, smart_contract}, :decompiled_smart_contracts], order_by: [asc: address.inserted_at], limit: ^limit, offset: ^offset @@ -2732,11 +2751,16 @@ defmodule Explorer.Chain do query = from( address in Address, + where: + fragment( + "NOT EXISTS (SELECT 1 FROM decompiled_smart_contracts WHERE decompiled_smart_contracts.address_hash = ?)", + address.hash + ), left_join: smart_contract in SmartContract, on: smart_contract.address_hash == address.hash, left_join: decompiled_smart_contract in DecompiledSmartContract, on: decompiled_smart_contract.address_hash == address.hash, - preload: [smart_contract: smart_contract, decompiled_smart_contract: decompiled_smart_contract], + preload: [:smart_contract, :decompiled_smart_contracts], where: not is_nil(address.contract_code), where: is_nil(smart_contract.address_hash), where: is_nil(decompiled_smart_contract.address_hash), diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 7fd71d6af6..d4b5bc8174 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -78,7 +78,6 @@ defmodule Explorer.Chain.Address do field(:has_decompiled_code?, :boolean, virtual: true) has_one(:smart_contract, SmartContract) - has_one(:decompiled_smart_contract, DecompiledSmartContract) has_one(:token, Token, foreign_key: :contract_address_hash) has_one( diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index a117a77353..937c1d422f 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -209,8 +209,8 @@ defmodule Explorer.Chain.SmartContract do field(:constructor_arguments, :string) field(:abi, {:array, :map}) - has_one( - :decompiled_smart_contract, + has_many( + :decompiled_smart_contracts, DecompiledSmartContract, foreign_key: :address_hash ) @@ -227,7 +227,7 @@ defmodule Explorer.Chain.SmartContract do end def preload_decompiled_smart_contract(contract) do - Repo.preload(contract, :decompiled_smart_contract) + Repo.preload(contract, :decompiled_smart_contracts) end def changeset(%__MODULE__{} = smart_contract, attrs) do