diff --git a/CHANGELOG.md b/CHANGELOG.md index 92c76dff99..2d8b458a9d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ ### Features - [#2862](https://github.com/poanetwork/blockscout/pull/2862) - Coin total supply from DB API endpoint +- [#2857](https://github.com/poanetwork/blockscout/pull/2857) - Extend getsourcecode API view with new output fields - [#2822](https://github.com/poanetwork/blockscout/pull/2822) - Estimated address count on the main page, if cache is empty - [#2821](https://github.com/poanetwork/blockscout/pull/2821) - add autodetection of constructor arguments - [#2825](https://github.com/poanetwork/blockscout/pull/2825) - separate token transfers and transactions 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 8f996a2bcc..7d92c8d5a2 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 @@ -4,6 +4,8 @@ defmodule BlockScoutWeb.API.RPC.ContractView do alias BlockScoutWeb.API.RPC.RPCView alias Explorer.Chain.{Address, DecompiledSmartContract, SmartContract} + defguardp is_empty_string(input) when input == "" or input == nil + def render("listcontracts.json", %{contracts: contracts}) do contracts = Enum.map(contracts, &prepare_contract/1) @@ -35,7 +37,11 @@ defmodule BlockScoutWeb.API.RPC.ContractView do "CompilerVersion" => "", "DecompiledSourceCode" => "", "DecompilerVersion" => decompiler_version(nil), - "OptimizationUsed" => "" + "OptimizationUsed" => "", + "OptimizationRuns" => "", + "EVMVersion" => "", + "ConstructorArguments" => "", + "ExternalLibraries" => "" } end @@ -43,6 +49,66 @@ defmodule BlockScoutWeb.API.RPC.ContractView do decompiled_smart_contract = latest_decompiled_smart_contract(address.decompiled_smart_contracts) contract = address.smart_contract || %{} + optimization = Map.get(contract, :optimization, "") + + contract_output = %{ + "Address" => to_string(address.hash) + } + + contract_output + |> set_decompiled_contract_data(decompiled_smart_contract) + |> set_optimization_runs(contract, optimization) + |> set_constructor_arguments(contract) + |> set_external_libraries(contract) + |> set_verified_contract_data(contract, address, optimization) + end + + defp set_decompiled_contract_data(contract_output, decompiled_smart_contract) do + if decompiled_smart_contract do + contract_output + |> Map.put_new(:DecompiledSourceCode, decompiled_source_code(decompiled_smart_contract)) + |> Map.put_new(:DecompilerVersion, decompiler_version(decompiled_smart_contract)) + else + contract_output + end + end + + defp set_optimization_runs(contract_output, contract, optimization) do + optimization_runs = Map.get(contract, :optimization_runs, "") + + if optimization && optimization != "" do + contract_output + |> Map.put_new(:OptimizationRuns, optimization_runs) + else + contract_output + end + end + + defp set_constructor_arguments(contract_output, %{constructor_arguments: arguments}) when is_empty_string(arguments), + do: contract_output + + defp set_constructor_arguments(contract_output, %{constructor_arguments: arguments}) do + contract_output + |> Map.put_new(:ConstructorArguments, arguments) + end + + defp set_external_libraries(contract_output, contract) do + external_libraries = Map.get(contract, :external_libraries, []) + + if Enum.count(external_libraries) > 0 do + external_libraries_without_id = + Enum.map(external_libraries, fn %{name: name, address_hash: address_hash} -> + %{"name" => name, "address_hash" => address_hash} + end) + + contract_output + |> Map.put_new(:ExternalLibraries, external_libraries_without_id) + else + contract_output + end + end + + defp set_verified_contract_data(contract_output, contract, address, optimization) do contract_abi = if is_nil(address.smart_contract) do "Contract source code not verified" @@ -51,27 +117,28 @@ defmodule BlockScoutWeb.API.RPC.ContractView do end contract_optimization = - case Map.get(contract, :optimization, "") do + case optimization do true -> - "1" + "true" false -> - "0" + "false" "" -> "" end - %{ - "Address" => to_string(address.hash), - "SourceCode" => Map.get(contract, :contract_source_code, ""), - "ABI" => contract_abi, - "ContractName" => Map.get(contract, :name, ""), - "DecompiledSourceCode" => decompiled_source_code(decompiled_smart_contract), - "DecompilerVersion" => decompiler_version(decompiled_smart_contract), - "CompilerVersion" => Map.get(contract, :compiler_version, ""), - "OptimizationUsed" => contract_optimization - } + if Map.equal?(contract, %{}) do + contract_output + else + contract_output + |> Map.put_new(:SourceCode, Map.get(contract, :contract_source_code, "")) + |> Map.put_new(:ABI, contract_abi) + |> Map.put_new(:ContractName, Map.get(contract, :name, "")) + |> Map.put_new(:CompilerVersion, Map.get(contract, :compiler_version, "")) + |> Map.put_new(:OptimizationUsed, contract_optimization) + |> Map.put_new(:EVMVersion, Map.get(contract, :evm_version, "")) + end end defp prepare_contract(%Address{ @@ -80,10 +147,7 @@ defmodule BlockScoutWeb.API.RPC.ContractView do }) do %{ "Address" => to_string(hash), - "ABI" => "Contract source code not verified", - "ContractName" => "", - "CompilerVersion" => "", - "OptimizationUsed" => "" + "ABI" => "Contract source code not verified" } end 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 37ce57f508..5c12c1e5db 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 @@ -1,5 +1,6 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do use BlockScoutWeb.ConnCase + alias Explorer.Chain.SmartContract alias Explorer.{Chain, Factory} describe "listcontracts" do @@ -70,10 +71,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(address.hash) } ] @@ -95,10 +93,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(address.hash) } ] @@ -124,10 +119,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(address.hash) } ] @@ -174,10 +166,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(decompiled_smart_contract.address_hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(decompiled_smart_contract.address_hash) } ] @@ -199,10 +188,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(smart_contract.address_hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(smart_contract.address_hash) } ] @@ -225,10 +211,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert %{ "ABI" => "Contract source code not verified", - "Address" => to_string(smart_contract.address_hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(smart_contract.address_hash) } in response["result"] refute to_string(non_match.address_hash) in Enum.map(response["result"], &Map.get(&1, "Address")) @@ -251,10 +234,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(contract_address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(contract_address.hash) } ] @@ -281,10 +261,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do assert response["result"] == [ %{ "ABI" => "Contract source code not verified", - "Address" => to_string(contract_address.hash), - "CompilerVersion" => "", - "ContractName" => "", - "OptimizationUsed" => "" + "Address" => to_string(contract_address.hash) } ] @@ -423,7 +400,11 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do "CompilerVersion" => "", "OptimizationUsed" => "", "DecompiledSourceCode" => "", - "DecompilerVersion" => "" + "DecompilerVersion" => "", + "ConstructorArguments" => "", + "EVMVersion" => "", + "ExternalLibraries" => "", + "OptimizationRuns" => "" } ] @@ -439,7 +420,7 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do end test "with a verified contract address", %{conn: conn} do - contract = insert(:smart_contract, optimization: true) + contract = insert(:smart_contract, optimization: true, optimization_runs: 200, evm_version: "default") params = %{ "module" => "contract", @@ -456,12 +437,156 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do "ABI" => Jason.encode!(contract.abi), "ContractName" => contract.name, "CompilerVersion" => contract.compiler_version, - "DecompiledSourceCode" => "Contract source code not decompiled.", # The contract's optimization value is true, so the expected value # for `OptimizationUsed` is "1". If it was false, the expected value # would be "0". - "DecompilerVersion" => "", - "OptimizationUsed" => "1" + "OptimizationUsed" => "true", + "OptimizationRuns" => 200, + "EVMVersion" => "default" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with constructor arguments", %{conn: conn} do + contract = + insert(:smart_contract, + optimization: true, + optimization_runs: 200, + evm_version: "default", + constructor_arguments: + "00000000000000000000000008e7592ce0d7ebabf42844b62ee6a878d4e1913e000000000000000000000000e1b6037da5f1d756499e184ca15254a981c92546" + ) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => to_string(contract.address_hash) + } + + expected_result = [ + %{ + "Address" => to_string(contract.address_hash), + "SourceCode" => + "/**\n* Submitted for verification at blockscout.com on #{contract.inserted_at}\n*/\n" <> + contract.contract_source_code, + "ABI" => Jason.encode!(contract.abi), + "ContractName" => contract.name, + "CompilerVersion" => contract.compiler_version, + "OptimizationUsed" => "true", + "OptimizationRuns" => 200, + "EVMVersion" => "default", + "ConstructorArguments" => + "00000000000000000000000008e7592ce0d7ebabf42844b62ee6a878d4e1913e000000000000000000000000e1b6037da5f1d756499e184ca15254a981c92546" + } + ] + + assert response = + conn + |> get("/api", params) + |> json_response(200) + + assert response["result"] == expected_result + assert response["status"] == "1" + assert response["message"] == "OK" + assert :ok = ExJsonSchema.Validator.validate(getsourcecode_schema(), response) + end + + test "with external library", %{conn: conn} do + smart_contract_bytecode = + "0x608060405234801561001057600080fd5b5060df8061001f6000396000f3006080604052600436106049576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff16806360fe47b114604e5780636d4ce63c146078575b600080fd5b348015605957600080fd5b5060766004803603810190808035906020019092919050505060a0565b005b348015608357600080fd5b50608a60aa565b6040518082815260200191505060405180910390f35b8060008190555050565b600080549050905600a165627a7a7230582040d82a7379b1ee1632ad4d8a239954fd940277b25628ead95259a85c5eddb2120029" + + created_contract_address = + insert( + :address, + hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + contract_code: smart_contract_bytecode + ) + + transaction = + :transaction + |> insert() + |> with_block() + + insert( + :internal_transaction_create, + transaction: transaction, + index: 0, + created_contract_address: created_contract_address, + created_contract_code: smart_contract_bytecode, + block_number: transaction.block_number, + transaction_index: transaction.index + ) + + valid_attrs = %{ + address_hash: "0x0f95fa9bc0383e699325f2658d04e8d96d87b90c", + name: "Test", + compiler_version: "0.4.23", + contract_source_code: + "pragma solidity ^0.4.23; contract SimpleStorage {uint storedData; function set(uint x) public {storedData = x; } function get() public constant returns (uint) {return storedData; } }", + abi: [ + %{ + "constant" => false, + "inputs" => [%{"name" => "x", "type" => "uint256"}], + "name" => "set", + "outputs" => [], + "payable" => false, + "stateMutability" => "nonpayable", + "type" => "function" + }, + %{ + "constant" => true, + "inputs" => [], + "name" => "get", + "outputs" => [%{"name" => "", "type" => "uint256"}], + "payable" => false, + "stateMutability" => "view", + "type" => "function" + } + ], + optimization: true, + optimization_runs: 200, + evm_version: "default" + } + + external_libraries = [ + %SmartContract.ExternalLibrary{:address_hash => "0xb18aed9518d735482badb4e8b7fd8d2ba425ce95", :name => "Test"}, + %SmartContract.ExternalLibrary{:address_hash => "0x283539e1b1daf24cdd58a3e934d55062ea663c3f", :name => "Test2"} + ] + + {:ok, %SmartContract{} = contract} = Chain.create_smart_contract(valid_attrs, external_libraries) + + params = %{ + "module" => "contract", + "action" => "getsourcecode", + "address" => to_string(contract.address_hash) + } + + expected_result = [ + %{ + "Address" => to_string(contract.address_hash), + "SourceCode" => + "/**\n* Submitted for verification at blockscout.com on #{contract.inserted_at}\n*/\n" <> + contract.contract_source_code, + "ABI" => Jason.encode!(contract.abi), + "ContractName" => contract.name, + "CompilerVersion" => contract.compiler_version, + "OptimizationUsed" => "true", + "OptimizationRuns" => 200, + "EVMVersion" => "default", + "ExternalLibraries" => [ + %{"name" => "Test", "address_hash" => "0xb18aed9518d735482badb4e8b7fd8d2ba425ce95"}, + %{"name" => "Test2", "address_hash" => "0x283539e1b1daf24cdd58a3e934d55062ea663c3f"} + ] } ] @@ -508,9 +633,8 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do "ABI" => Jason.encode!(contract_code_info.abi), "ContractName" => contract_code_info.name, "CompilerVersion" => contract_code_info.version, - "DecompiledSourceCode" => "Contract source code not decompiled.", - "DecompilerVersion" => "", - "OptimizationUsed" => "0" + "OptimizationUsed" => "false", + "EVMVersion" => nil } assert response["status"] == "1" @@ -578,9 +702,9 @@ defmodule BlockScoutWeb.API.RPC.ContractControllerTest do contract_source_code assert result["ContractName"] == name - assert result["DecompiledSourceCode"] == "Contract source code not decompiled." - assert result["DecompilerVersion"] == "" - assert result["OptimizationUsed"] == "1" + assert result["DecompiledSourceCode"] == nil + assert result["DecompilerVersion"] == nil + assert result["OptimizationUsed"] == "true" assert :ok = ExJsonSchema.Validator.validate(verify_schema(), response) end end