diff --git a/apps/block_scout_web/assets/js/lib/smart_contract/functions.js b/apps/block_scout_web/assets/js/lib/smart_contract/functions.js index 6230972ba7..0c9ba96ad8 100644 --- a/apps/block_scout_web/assets/js/lib/smart_contract/functions.js +++ b/apps/block_scout_web/assets/js/lib/smart_contract/functions.js @@ -1,10 +1,10 @@ import $ from 'jquery' -import { connectSelector, disconnectSelector, getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers' +import { connectSelector, disconnectSelector, getCurrentAccountPromise, getContractABI, getMethodInputs, prepareMethodArgs } from './common_helpers' import { queryMethod, callMethod } from './interact' import { walletEnabled, connectToWallet, disconnectWallet, web3ModalInit } from './connect.js' import '../../pages/address' -const loadFunctions = (element, isCustomABI) => { +const loadFunctions = (element, isCustomABI, from) => { const $element = $(element) const url = $element.data('url') const hash = $element.data('hash') @@ -13,7 +13,7 @@ const loadFunctions = (element, isCustomABI) => { $.get( url, - { hash, type, action, is_custom_abi: isCustomABI }, + { hash, type, action, is_custom_abi: isCustomABI, from: from }, response => $element.html(response) ) .done(function () { @@ -96,11 +96,15 @@ const readWriteFunction = (element) => { const container = $('[data-smart-contract-functions]') if (container.length) { - loadFunctions(container, false) + getCurrentAccountPromise(window.web3 && window.web3.currentProvider).then((currentAccount) => { + loadFunctions(container, false, currentAccount)}, () => {loadFunctions(container, false, null) + }) } const customABIContainer = $('[data-smart-contract-functions-custom]') if (customABIContainer.length) { - loadFunctions(customABIContainer, true) + getCurrentAccountPromise(window.web3 && window.web3.currentProvider).then((currentAccount) => { + loadFunctions(container, false, currentAccount)}, () => {loadFunctions(container, true, null) + }) } 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 99ced87556..7f9cbeab2f 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 @@ -129,6 +129,15 @@ defmodule BlockScoutWeb.ApiRouter do get("/:address_hash/coin-balance-history-by-day", V2.AddressController, :coin_balance_history_by_day) end + scope "/smart-contracts" do + get("/:address_hash", V2.AddressController, :smart_contract) + get("/:address_hash/methods-read", V2.AddressController, :methods_read) + get("/:address_hash/methods-write", V2.AddressController, :methods_write) + get("/:address_hash/methods-read-proxy", V2.AddressController, :methods_read_proxy) + get("/:address_hash/methods-write-proxy", V2.AddressController, :methods_write_proxy) + get("/:address_hash/query-read-method", V2.AddressController, :query_read_method) + end + scope "/tokens" do get("/:address_hash", V2.TokenController, :token) get("/:address_hash/counters", V2.TokenController, :counters) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 806fcd0f37..e4a6113cb3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -12,9 +12,15 @@ defmodule BlockScoutWeb.API.V2.AddressController do import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1] + import Explorer.SmartContract.Solidity.Verifier, only: [parse_boolean: 1] + alias BlockScoutWeb.AccessHelpers - alias BlockScoutWeb.API.V2.{AddressView, BlockView, TransactionView} + alias BlockScoutWeb.AddressContractVerificationController, as: VerificationController + alias BlockScoutWeb.AddressView + alias BlockScoutWeb.API.V2.{BlockView, TransactionView} alias Explorer.{Chain, Market} + alias Explorer.Chain.SmartContract + alias Explorer.SmartContract.{Reader, Writer} alias Indexer.Fetcher.TokenBalanceOnDemand @transaction_necessity_by_association [ @@ -37,6 +43,14 @@ defmodule BlockScoutWeb.API.V2.AddressController do } ] + @smart_contract_address_options [ + necessity_by_association: %{ + :smart_contract => :optional + } + ] + + @burn_address "0x0000000000000000000000000000000000000000" + action_fallback(BlockScoutWeb.API.V2.FallbackController) def address(conn, %{"address_hash" => address_hash_string} = params) do @@ -249,7 +263,6 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) - |> put_view(AddressView) |> render(:coin_balances, %{coin_balances: coin_balances, next_page_params: next_page_params}) end end @@ -263,8 +276,139 @@ defmodule BlockScoutWeb.API.V2.AddressController do conn |> put_status(200) - |> put_view(AddressView) |> render(:coin_balances_by_day, %{coin_balances_by_day: balances_by_day}) end end + + def smart_contract(conn, %{"address_hash" => address_hash_string} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params), + _ <- VerificationController.check_and_verify(address_hash_string), + {:not_found, {:ok, address}} <- + {:not_found, Chain.find_contract_address(address_hash, @smart_contract_address_options, true)} do + conn + |> put_status(200) + |> render(:smart_contract, %{address: address}) + end + end + + def methods_read(conn, %{"address_hash" => address_hash_string, "is_custom_abi" => "true"} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params), + custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string), + {:not_found, true} <- {:not_found, AddressView.check_custom_abi_for_having_read_functions(custom_abi)} do + read_only_functions_from_abi = + Reader.read_only_functions_from_abi_with_sender(custom_abi.abi, address_hash, params["from"]) + + read_functions_required_wallet_from_abi = Reader.read_functions_required_wallet_from_abi(custom_abi.abi) + + conn + |> put_status(200) + |> json(read_only_functions_from_abi ++ read_functions_required_wallet_from_abi) + end + end + + def methods_read(conn, %{"address_hash" => address_hash_string} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do + read_only_functions_from_abi = Reader.read_only_functions(address_hash, params["from"]) |> debug("r only") + + read_functions_required_wallet_from_abi = + Reader.read_functions_required_wallet(address_hash) |> debug("r req wall") + + conn + |> put_status(200) + |> json(read_only_functions_from_abi ++ read_functions_required_wallet_from_abi) + end + end + + def methods_write(conn, %{"address_hash" => address_hash_string, "is_custom_abi" => "true"} = params) do + with {:format, {:ok, _address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params), + custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string), + {:not_found, true} <- {:not_found, AddressView.check_custom_abi_for_having_read_functions(custom_abi)} do + conn + |> put_status(200) + |> json(Writer.filter_write_functions(custom_abi.abi)) + end + end + + def methods_write(conn, %{"address_hash" => address_hash_string} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params) do + conn + |> put_status(200) + |> json(Writer.write_functions(address_hash)) + end + end + + def methods_read_proxy(conn, %{"address_hash" => address_hash_string} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params), + {:not_found, {:ok, address}} <- + {:not_found, Chain.find_contract_address(address_hash, @smart_contract_address_options)} do + implementation_address_hash_string = + address.smart_contract + |> SmartContract.get_implementation_address_hash() + |> Tuple.to_list() + |> List.first() || @burn_address + + conn + |> put_status(200) + |> json(Reader.read_only_functions_proxy(address_hash, implementation_address_hash_string, params["from"])) + end + end + + def methods_write_proxy(conn, %{"address_hash" => address_hash_string} = params) do + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params), + {:not_found, {:ok, address}} <- + {:not_found, Chain.find_contract_address(address_hash, @smart_contract_address_options)} do + implementation_address_hash_string = + address.smart_contract + |> SmartContract.get_implementation_address_hash() + |> Tuple.to_list() + |> List.first() || @burn_address + + conn + |> put_status(200) + |> json(Writer.write_functions_proxy(implementation_address_hash_string)) + end + end + + def query_read_method( + conn, + %{"address_hash" => address_hash_string, "is_custom_abi" => custom_abi, "contract_type" => type, "args" => args} = + params + ) do + custom_abi = + if parse_boolean(params["is_custom_abi"]), do: AddressView.fetch_custom_abi(conn, params["id"]), else: nil + + contract_type = if type == "proxy", do: :proxy, else: :regular + + with {:format, {:ok, address_hash}} <- {:format, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelpers.restricted_access?(address_hash_string, params), + {:not_found, {:ok, _address}} <- {:not_found, Chain.find_contract_address(address_hash, [])} do + %{output: outputs, names: _names} = + if custom_abi do + Reader.query_function_with_names_custom_abi( + address_hash, + %{method_id: params["method_id"], args: args}, + params["from"], + custom_abi.abi + ) + else + Reader.query_function_with_names( + address_hash, + %{method_id: params["method_id"], args: args}, + contract_type, + params["from"] + ) + end + + conn + |> put_status(200) + |> json(outputs) + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex index 7d9d0964a9..50691dc9c3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex @@ -42,9 +42,9 @@ defmodule BlockScoutWeb.SmartContractController do end else if contract_type == "proxy" do - Reader.read_only_functions_proxy(address_hash, implementation_address_hash_string) + Reader.read_only_functions_proxy(address_hash, implementation_address_hash_string, params["from"]) else - Reader.read_only_functions(address_hash) + Reader.read_only_functions(address_hash, params["from"]) end end @@ -101,7 +101,7 @@ defmodule BlockScoutWeb.SmartContractController do def index(conn, _), do: not_found(conn) - defp custom_abi_render(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action}) do + defp custom_abi_render(conn, %{"hash" => address_hash_string, "type" => contract_type, "action" => action} = params) do with custom_abi <- AddressView.fetch_custom_abi(conn, address_hash_string), false <- is_nil(custom_abi), abi <- custom_abi.abi, @@ -110,7 +110,7 @@ defmodule BlockScoutWeb.SmartContractController do if action == "write" do Writer.filter_write_functions(abi) else - Reader.read_only_functions_from_abi(abi, address_hash) + Reader.read_only_functions_from_abi_with_sender(abi, address_hash, params["from"]) end read_functions_required_wallet = diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex index ce7b4dc00b..f8b01b5660 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex @@ -83,11 +83,11 @@ defmodule BlockScoutWeb.AddressContractView do end end - defp decode_data("0x" <> encoded_data, types) do + def decode_data("0x" <> encoded_data, types) do decode_data(encoded_data, types) end - defp decode_data(encoded_data, types) do + def decode_data(encoded_data, types) do encoded_data |> Base.decode16!(case: :mixed) |> TypeDecoder.decode_raw(types) diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex index 6d43e493e1..729e67b5a2 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex @@ -1,7 +1,8 @@ defmodule BlockScoutWeb.API.V2.AddressView do use BlockScoutWeb, :view - alias BlockScoutWeb.AddressView + alias ABI.FunctionSelector + alias BlockScoutWeb.{ABIEncodedValueView, AddressContractView, AddressView} alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView} alias BlockScoutWeb.API.V2.Helper alias Explorer.{Chain, Market} @@ -32,6 +33,10 @@ defmodule BlockScoutWeb.API.V2.AddressView do Enum.map(coin_balances_by_day, &prepare_coin_balance_history_by_day_entry/1) end + def render("smart_contract.json", %{address: address}) do + prepare_smart_contract(address) + end + def prepare_address(address, conn \\ nil) do base_info = Helper.address_with_info(conn, address, address.hash) is_proxy = AddressView.smart_contract_is_proxy?(address) @@ -55,6 +60,9 @@ defmodule BlockScoutWeb.API.V2.AddressView do creation_tx = creator_hash && AddressView.transaction_hash(address) token = address.token && TokenView.render("token.json", %{token: Market.add_price(address.token)}) + write_custom_abi? = AddressView.has_address_custom_abi_with_write_functions?(conn, address.hash) + read_custom_abi? = AddressView.has_address_custom_abi_with_read_functions?(conn, address.hash) + Map.merge(base_info, %{ "creator_address_hash" => creator_hash && Address.checksum(creator_hash), "creation_tx_hash" => creation_tx, @@ -63,7 +71,13 @@ defmodule BlockScoutWeb.API.V2.AddressView do "exchange_rate" => exchange_rate, "implementation_name" => implementation_name, "implementation_address" => implementation_address, - "block_number_balance_updated_at" => address.fetched_coin_balance_block_number + "block_number_balance_updated_at" => address.fetched_coin_balance_block_number, + "has_custom_methods_read" => read_custom_abi?, + "has_custom_methods_write" => write_custom_abi?, + "has_methods_read" => AddressView.smart_contract_with_read_only_functions?(address) || read_custom_abi?, + "has_methods_write" => AddressView.smart_contract_with_write_functions?(address) || write_custom_abi?, + "has_methods_read_proxy" => is_proxy, + "has_methods_write_proxy" => AddressView.smart_contract_with_write_functions?(address) && is_proxy }) end @@ -91,4 +105,100 @@ defmodule BlockScoutWeb.API.V2.AddressView do "value" => coin_balance_by_day.value } end + + def prepare_smart_contract(address) do + minimal_proxy_template = Chain.get_minimal_proxy_template(address.hash) + + metadata_for_verification = + minimal_proxy_template || Chain.get_address_verified_twin_contract(address.hash).verified_contract + + smart_contract_verified = AddressView.smart_contract_verified?(address) + additional_sources_from_twin = Chain.get_address_verified_twin_contract(address.hash).additional_sources + fully_verified = Chain.smart_contract_fully_verified?(address.hash) + + additional_sources = + if smart_contract_verified, do: address.smart_contract_additional_sources, else: additional_sources_from_twin + + visualize_sol2uml_enabled = Explorer.Visualize.Sol2uml.enabled?() + target_contract = if smart_contract_verified, do: address.smart_contract, else: metadata_for_verification + + %{ + "verified_twin_address_hash" => metadata_for_verification && metadata_for_verification.address_hash, + "is_verified" => smart_contract_verified, + "is_changed_bytecode" => smart_contract_verified && address.smart_contract.is_changed_bytecode, + "is_partially_verified" => address.smart_contract.partially_verified && smart_contract_verified, + "is_fully_verified" => fully_verified, + "is_verified_via_sourcify" => address.smart_contract.verified_via_sourcify && smart_contract_verified, + "is_vyper_contract" => target_contract.is_vyper_contract, + "minimal_proxy_address_hash" => + minimal_proxy_template && Address.checksum(metadata_for_verification.address_hash), + "sourcify_repo_url" => + if(address.smart_contract.verified_via_sourcify && smart_contract_verified, + do: AddressContractView.sourcify_repo_url(address.hash, address.smart_contract.partially_verified) + ), + "can_be_visualized_via_sol2uml" => + visualize_sol2uml_enabled && !target_contract.is_vyper_contract && !is_nil(target_contract.abi), + "name" => target_contract && target_contract.name, + "compiler_version" => target_contract.compiler_version, + "optimization_enabled" => if(target_contract.is_vyper_contract, do: nil, else: target_contract.optimization), + "optimization_runs" => target_contract.optimization_runs, + "evm_version" => target_contract.evm_version, + "verified_at" => target_contract.inserted_at, + "abi" => target_contract.abi, + "source_code" => target_contract.contract_source_code, + "file_path" => target_contract.file_path, + "additional_sources" => Enum.map(additional_sources, &prepare_additional_sourse/1), + "compiler_settings" => target_contract.compiler_settings, + "external_libraries" => target_contract.external_libraries, + "constructor_args" => target_contract.constructor_arguments, + "decoded_constructor_args" => + format_constructor_arguments(target_contract.abi, target_contract.constructor_arguments) + } + |> Map.merge(bytecode_info(address)) + + # |> + end + + defp bytecode_info(address) do + case AddressContractView.contract_creation_code(address) do + {:selfdestructed, init} -> + %{ + "is_self_destructed" => true, + "deployed_bytecode" => nil, + "creation_bytecode" => init + } + + {:ok, contract_code} -> + %{ + "is_self_destructed" => false, + "deployed_bytecode" => contract_code, + "creation_bytecode" => AddressContractView.creation_code(address) + } + end + end + + defp prepare_additional_sourse(source) do + %{ + "source_code" => source.contract_source_code, + "file_path" => source.file_name + } + end + + def format_constructor_arguments(abi, constructor_arguments) do + constructor_abi = Enum.find(abi, fn el -> el["type"] == "constructor" && el["inputs"] != [] end) + + input_types = Enum.map(constructor_abi["inputs"], &FunctionSelector.parse_specification_type/1) + + result = + constructor_arguments + |> AddressContractView.decode_data(input_types) + |> Enum.zip(constructor_abi["inputs"]) + |> Enum.map(fn {value, %{"type" => type} = input_arg} -> + {ABIEncodedValueView.value_json(type, value), input_arg} + end) + + result + rescue + _ -> nil + end end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 6dc1808e31..a134414407 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -81,6 +81,7 @@ defmodule BlockScoutWeb.API.V2.Helper do def is_smart_contract(_), do: false def is_verified(%Address{smart_contract: nil}), do: false + def is_verified(%Address{smart_contract: %{metadata_from_verified_twin: true}}), do: false def is_verified(%Address{smart_contract: %NotLoaded{}}), do: nil def is_verified(%Address{smart_contract: _}), do: true diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index 0d0c8d5b2d..bf386414c8 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -217,7 +217,7 @@ defmodule Explorer.SmartContract.Reader do ] """ @spec read_only_functions(Hash.t()) :: [%{}] - def read_only_functions(contract_address_hash) do + def read_only_functions(contract_address_hash, from \\ nil) do abi = contract_address_hash |> Chain.address_hash_to_smart_contract() @@ -228,11 +228,11 @@ defmodule Explorer.SmartContract.Reader do [] _ -> - read_only_functions_from_abi(abi, contract_address_hash) + read_only_functions_from_abi_with_sender(abi, contract_address_hash, from) end end - def read_only_functions_proxy(contract_address_hash, implementation_address_hash_string) do + def read_only_functions_proxy(contract_address_hash, implementation_address_hash_string, from \\ nil) do implementation_abi = Chain.get_implementation_abi(implementation_address_hash_string) case implementation_abi do @@ -240,7 +240,7 @@ defmodule Explorer.SmartContract.Reader do [] _ -> - read_only_functions_from_abi(implementation_abi, contract_address_hash) + read_only_functions_from_abi_with_sender(implementation_abi, contract_address_hash, from) end end @@ -279,15 +279,15 @@ defmodule Explorer.SmartContract.Reader do end end - def read_only_functions_from_abi([_ | _] = abi, contract_address_hash) do + def read_only_functions_from_abi_with_sender([_ | _] = abi, contract_address_hash, from) do abi_with_method_id = get_abi_with_method_id(abi) abi_with_method_id |> Enum.filter(&Helper.queriable_method?(&1)) - |> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false)) + |> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false, from)) end - def read_only_functions_from_abi(_, _), do: [] + def read_only_functions_from_abi_with_sender(_, _, _), do: [] def read_functions_required_wallet_from_abi([_ | _] = abi) do abi_with_method_id = get_abi_with_method_id(abi) @@ -346,7 +346,7 @@ defmodule Explorer.SmartContract.Reader do "tuple[#{tuple_types}]" end - def fetch_current_value_from_blockchain(function, abi, contract_address_hash, leave_error_as_map) do + def fetch_current_value_from_blockchain(function, abi, contract_address_hash, leave_error_as_map, from \\ nil) do values = case function do %{"inputs" => []} -> @@ -355,7 +355,7 @@ defmodule Explorer.SmartContract.Reader do outputs = function["outputs"] contract_address_hash - |> query_verified_contract(%{method_id => normalize_args(args)}, leave_error_as_map, abi) + |> query_verified_contract(%{method_id => normalize_args(args)}, from, leave_error_as_map, abi) |> link_outputs_and_values(outputs, method_id) _ ->