diff --git a/CHANGELOG.md b/CHANGELOG.md index 920d3e4c7d..00cf765d8c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,7 +8,7 @@ - [#1815](https://github.com/poanetwork/blockscout/pull/1815) - able to search without prefix "0x" - [#1813](https://github.com/poanetwork/blockscout/pull/1813) - add total blocks counter to the main page - [#1806](https://github.com/poanetwork/blockscout/pull/1806) - verify contracts with a post request -- [#1857](https://github.com/poanetwork/blockscout/pull/1857) - Re-implement Geth JS internal transaction tracer in Elixir +- [#1857](https://github.com/poanetwork/blockscout/pull/1857) - Re-implement Geth JS internal transaction tracer in Elixir - [#1859](https://github.com/poanetwork/blockscout/pull/1859) - feat: show raw transaction traces ### Fixes @@ -21,6 +21,9 @@ - [#1869](https://github.com/poanetwork/blockscout/pull/1869) - Fix output and gas extraction in JS tracer for Geth - [#1868](https://github.com/poanetwork/blockscout/pull/1868) - fix: logs list endpoint performance - [#1822](https://github.com/poanetwork/blockscout/pull/1822) - Fix style breaks in decompiled contract code view +- [#1885](https://github.com/poanetwork/blockscout/pull/1885) - highlight reserved words in decompiled code +- [#1896](https://github.com/poanetwork/blockscout/pull/1896) - re-query tokens in top nav automplete +- [#1881](https://github.com/poanetwork/blockscout/pull/1881) - fix: store solc versions locally for performance ### Chore @@ -204,4 +207,3 @@ - [https://github.com/poanetwork/blockscout/pull/1532](https://github.com/poanetwork/blockscout/pull/1532) - Upgrade elixir to 1.8.1 - [https://github.com/poanetwork/blockscout/pull/1553](https://github.com/poanetwork/blockscout/pull/1553) - Dockerfile: remove 1.7.1 version pin FROM bitwalker/alpine-elixir-phoenix - [https://github.com/poanetwork/blockscout/pull/1465](https://github.com/poanetwork/blockscout/pull/1465) - Resolve lodash security alert - diff --git a/README.md b/README.md index a2df24f85c..289a3d35ed 100644 --- a/README.md +++ b/README.md @@ -139,7 +139,7 @@ The [development stack page](https://github.com/poanetwork/blockscout/wiki/Devel ``` * If using Chrome, Enable `chrome://flags/#allow-insecure-localhost`. - 9. Start Phoenix Server. + 9. Run the Phoenix Server from the root directory of your application. `mix phx.server` Now you can visit [`localhost:4000`](http://localhost:4000) from your browser. diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex index 032ab22930..f7392a6b39 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex @@ -112,9 +112,8 @@ "data-test": "search_input" ], [ url: "#{chain_path(@conn, :token_autocomplete)}?q=", - prepop: true, - minChars: 3, - maxItems: 8, + limit: 0, + minChars: 2, value: "contract_address_hash", label: "contract_address_hash", descrSearch: true, diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex index 1418255594..f80a0ded99 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_decompiled_contract_view.ex @@ -2,51 +2,231 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do use BlockScoutWeb, :view @colors %{ - "\e[95m" => "136, 0, 0", + "\e[95m" => "", # red - "\e[91m" => "236, 89, 58", + "\e[91m" => "", # gray - "\e[38;5;8m" => "111, 110, 111", + "\e[38;5;8m" => "", # green - "\e[32m" => "57, 115, 0", + "\e[32m" => "", # yellowgreen - "\e[93m" => "57, 115, 0", + "\e[93m" => "", # yellow - "\e[92m" => "119, 232, 81", + "\e[92m" => "", # red - "\e[94m" => "136, 0, 0" + "\e[94m" => "" } + @comment_start "#" + + @reserved_words_types [ + "var", + "bool", + "string", + "int", + "uint", + "int8", + "uint8", + "int16", + "uint16", + "int24", + "uint24", + "int32", + "uint32", + "int40", + "uint40", + "int48", + "uint48", + "int56", + "uint56", + "int64", + "uint64", + "int72", + "uint72", + "int80", + "uint80", + "int88", + "uint88", + "int96", + "uint96", + "int104", + "uint104", + "int112", + "uint112", + "int120", + "uint120", + "int128", + "uint128", + "int136", + "uint136", + "int144", + "uint144", + "int152", + "uint152", + "int160", + "uint160", + "int168", + "uint168", + "int176", + "uint176", + "int184", + "uint184", + "int192", + "uint192", + "int200", + "uint200", + "int208", + "uint208", + "int216", + "uint216", + "int224", + "uint224", + "int232", + "uint232", + "int240", + "uint240", + "int248", + "uint248", + "int256", + "uint256", + "byte", + "bytes", + "bytes1", + "bytes2", + "bytes3", + "bytes4", + "bytes5", + "bytes6", + "bytes7", + "bytes8", + "bytes9", + "bytes10", + "bytes11", + "bytes12", + "bytes13", + "bytes14", + "bytes15", + "bytes16", + "bytes17", + "bytes18", + "bytes19", + "bytes20", + "bytes21", + "bytes22", + "bytes23", + "bytes24", + "bytes25", + "bytes26", + "bytes27", + "bytes28", + "bytes29", + "bytes30", + "bytes31", + "bytes32", + "true", + "false", + "enum", + "struct", + "mapping", + "address" + ] + + @reserved_words_keywords [ + "def", + "require", + "revert", + "return", + "assembly", + "memory", + "mem" + ] + + @modifiers [ + "payable", + "public", + "view", + "pure", + "returns", + "internal" + ] + + @reserved_words @reserved_words_keywords ++ @reserved_words_types + + @reserved_words_regexp ([@comment_start | @reserved_words] ++ @modifiers) + |> Enum.reduce("", fn el, acc -> acc <> "|" <> el end) + |> Regex.compile!() + def highlight_decompiled_code(code) do {_, result} = @colors |> Enum.reduce(code, fn {symbol, rgb}, acc -> - String.replace(acc, symbol, "") + String.replace(acc, symbol, rgb) end) |> String.replace("\e[1m", "") |> String.replace("ยป", "»") |> String.replace("\e[0m", "") |> String.split(~r/\|\<\/span\>/, include_captures: true, trim: true) - |> Enum.reduce({"", []}, fn part, {style, acc} -> - new_style = - cond do - String.contains?(part, " part - part == "" -> "" - true -> style - end - - new_part = new_part(part, new_style) - - {new_style, [new_part | acc]} - end) + |> add_styles_to_every_line() result |> Enum.reduce("", fn part, acc -> part <> acc end) + |> add_styles_to_reserved_words() |> add_line_numbers() end + defp add_styles_to_every_line(lines) do + lines + |> Enum.reduce({"", []}, fn part, {style, acc} -> + new_style = + cond do + String.contains?(part, " part + part == "" -> "" + true -> style + end + + new_part = new_part(part, new_style) + + {new_style, [new_part | acc]} + end) + end + + defp add_styles_to_reserved_words(code) do + code + |> String.split("\n") + |> Enum.map(fn line -> + add_styles_to_line(line) + end) + |> Enum.reduce("", fn el, acc -> + acc <> el <> "\n" + end) + end + + defp add_styles_to_line(line) do + parts = + line + |> String.split(@reserved_words_regexp, + include_captures: true + ) + + comment_position = Enum.find_index(parts, fn part -> part == "#" end) + + parts + |> Enum.with_index() + |> Enum.map(fn {el, index} -> + cond do + !(is_nil(comment_position) || comment_position > index) -> el + el in @reserved_words -> "" <> el <> "" + el in @modifiers -> "" <> el <> "" + true -> el + end + end) + |> Enum.reduce("", fn el, acc -> + acc <> el + end) + end + def sort_contracts_by_version(decompiled_contracts) do decompiled_contracts |> Enum.sort_by(& &1.decompiler_version) @@ -76,18 +256,12 @@ defmodule BlockScoutWeb.AddressDecompiledContractView do part true -> - result = - part - |> String.split("\n") - |> Enum.reduce("", fn p, a -> - a <> new_style <> p <> "\n" - end) - - if String.ends_with?(part, "\n") do - result - else - String.slice(result, 0..-2) - end + part + |> String.split("\n") + |> Enum.reduce("", fn p, a -> + a <> new_style <> p <> "\n" + end) + |> String.slice(0..-2) end end end diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 8ee6c84113..d38ebaced2 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -714,7 +714,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/layout/_topnav.html.eex:111 -#: lib/block_scout_web/templates/layout/_topnav.html.eex:129 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:128 msgid "Search" msgstr "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 4612b16a72..067af0cd66 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -714,7 +714,7 @@ msgstr "" #, elixir-format #: lib/block_scout_web/templates/layout/_topnav.html.eex:111 -#: lib/block_scout_web/templates/layout/_topnav.html.eex:129 +#: lib/block_scout_web/templates/layout/_topnav.html.eex:128 msgid "Search" msgstr "" diff --git a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs index c3ff123584..8f1636ba06 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_decompiled_contract_view_test.exs @@ -56,7 +56,7 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do result = AddressDecompiledContractView.highlight_decompiled_code(code) assert result == - " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n #\n # I failed with these:\n # - unknowne77c646d(?)\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)\n # All the rest is below.\n #\n\n\n # Storage definitions and getters\n\n def storage:\n allowance is uint256 => uint256 # mask(256, 0) at storage #2\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4\n\n def allowance(address _owner, address _spender) payable: 64\n return allowance[sha3(((320 - 1) and (320 - 1) and _owner), 1), ((320 - 1) and _spender and (320 - 1))]\n\n\n #\n # Regular functions - see Tutorial for understanding quirks of the code\n #\n\n\n # folder failed in this function - may be terribly long, sorry\n def unknownc47d033b(?) payable: not cd[4]:\n revert\n else:\n mem[0]cd[4]\n mem[32] = 4\n mem[96] = bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n return bool(stor4[((320 - 1) and (320 - 1) and cd[4])])\n\n def _fallback() payable: # default function\n revert\n\n" + " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n #\n # I failed with these:\n # - unknowne77c646d(?)\n # - transferFromWithData(address _from, address _to, uint256 _value, bytes _data)\n # All the rest is below.\n #\n\n\n # Storage definitions and getters\n\n def storage:\n allowance is uint256 => uint256 # mask(256, 0) at storage #2\n stor4 is uint256 => uint8 # mask(8, 0) at storage #4\n\n def allowance(address _owner, address _spender) payable: 64\n return allowance[_owner_spender(320 - 1))]\n\n\n #\n # Regular functions - see Tutorial for understanding quirks of the code\n #\n\n\n # folder failed in this function - may be terribly long, sorry\n def unknownc47d033b(?) payable: not cd[4]:\n revert\n else:\n mem[0]cd[4]\n mem[32] = 4\n mem[96] = bool(stor4[cd[4])])\n return bool(stor4[cd[4])])\n\n def _fallback() payable: # default function\n revert\n\n\n" end test "adds style span to every line" do @@ -70,7 +70,7 @@ defmodule BlockScoutWeb.AddressDecompiledContractViewTest do """ assert AddressDecompiledContractView.highlight_decompiled_code(code) == - " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n\n" + " #\n # eveem.org 6 Feb 2019\n # Decompiled source of 0x00Bd9e214FAb74d6fC21bf1aF34261765f57e875\n #\n # Let's make the world open source\n # \n\n\n" end end diff --git a/apps/explorer/.gitignore b/apps/explorer/.gitignore index f4cd5728cc..bf75b19355 100644 --- a/apps/explorer/.gitignore +++ b/apps/explorer/.gitignore @@ -1 +1,2 @@ -priv/.recovery \ No newline at end of file +priv/.recovery +priv/solc_compilers/ diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex index 47b30cfa2b..b4f8589a57 100644 --- a/apps/explorer/lib/explorer/application.ex +++ b/apps/explorer/lib/explorer/application.ex @@ -26,6 +26,7 @@ defmodule Explorer.Application do Supervisor.Spec.worker(SpandexDatadog.ApiServer, [datadog_opts()]), Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor), Supervisor.child_spec({Task.Supervisor, name: Explorer.TaskSupervisor}, id: Explorer.TaskSupervisor), + Explorer.SmartContract.SolcDownloader, {Registry, keys: :duplicate, name: Registry.ChainEvents, id: Registry.ChainEvents}, {Admin.Recovery, [[], [name: Admin.Recovery]]}, {TransactionCountCache, [[], []]} diff --git a/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex b/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex new file mode 100644 index 0000000000..1aa81d2488 --- /dev/null +++ b/apps/explorer/lib/explorer/smart_contract/solc_downloader.ex @@ -0,0 +1,92 @@ +defmodule Explorer.SmartContract.SolcDownloader do + @moduledoc """ + Checks to see if the requested solc compiler version exists, and if not it + downloads and stores the file. + """ + use GenServer + + alias Explorer.SmartContract.Solidity.CompilerVersion + + @latest_compiler_refetch_time :timer.minutes(30) + + def ensure_exists(version) do + path = file_path(version) + + if File.exists?(path) do + path + else + {:ok, compiler_versions} = CompilerVersion.fetch_versions() + + if version in compiler_versions do + GenServer.call(__MODULE__, {:ensure_exists, version}, 60_000) + else + false + end + end + end + + def start_link(_) do + GenServer.start_link(__MODULE__, [], name: __MODULE__) + end + + # sobelow_skip ["Traversal"] + @impl true + def init([]) do + File.mkdir(compiler_dir()) + + {:ok, []} + end + + # sobelow_skip ["Traversal"] + @impl true + def handle_call({:ensure_exists, version}, _from, state) do + path = file_path(version) + + if fetch?(version, path) do + temp_path = file_path("#{version}-tmp") + + contents = download(version) + + file = File.open!(temp_path, [:write, :exclusive]) + + IO.binwrite(file, contents) + + File.rename(temp_path, path) + end + + {:reply, path, state} + end + + defp fetch?("latest", path) do + case File.stat(path) do + {:error, :enoent} -> + true + + {:ok, %{mtime: mtime}} -> + last_modified = NaiveDateTime.from_erl!(mtime) + diff = Timex.diff(NaiveDateTime.utc_now(), last_modified, :milliseconds) + + diff > @latest_compiler_refetch_time + end + end + + defp fetch?(_, path) do + not File.exists?(path) + end + + defp file_path(version) do + Path.join(compiler_dir(), "#{version}.js") + end + + defp compiler_dir do + Application.app_dir(:explorer, "priv/solc_compilers/") + end + + defp download(version) do + download_path = "https://ethereum.github.io/solc-bin/bin/soljson-#{version}.js" + + download_path + |> HTTPoison.get!([], timeout: 60_000) + |> Map.get(:body) + end +end diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex b/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex index e7f6090707..72c5bae186 100644 --- a/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex +++ b/apps/explorer/lib/explorer/smart_contract/solidity/code_compiler.ex @@ -3,6 +3,8 @@ defmodule Explorer.SmartContract.Solidity.CodeCompiler do Module responsible to compile the Solidity code of a given Smart Contract. """ + alias Explorer.SmartContract.SolcDownloader + @new_contract_name "New.sol" @allowed_evm_versions ["homestead", "tangerineWhistle", "spuriousDragon", "byzantium", "constantinople", "petersburg"] @@ -79,31 +81,36 @@ defmodule Explorer.SmartContract.Solidity.CodeCompiler do "byzantium" end - {response, _status} = - System.cmd( - "node", - [ - Application.app_dir(:explorer, "priv/compile_solc.js"), - code, - compiler_version, - optimize_value(optimize), - optimization_runs, - @new_contract_name, - external_libs_string, - checked_evm_version - ] - ) - - with {:ok, contracts} <- Jason.decode(response), - %{"abi" => abi, "evm" => %{"deployedBytecode" => %{"object" => bytecode}}} <- - get_contract_info(contracts, name) do - {:ok, %{"abi" => abi, "bytecode" => bytecode, "name" => name}} - else - {:error, %Jason.DecodeError{}} -> - {:error, :compilation} - - error -> - parse_error(error) + path = SolcDownloader.ensure_exists(compiler_version) + + if path do + {response, _status} = + System.cmd( + "node", + [ + Application.app_dir(:explorer, "priv/compile_solc.js"), + code, + compiler_version, + optimize_value(optimize), + optimization_runs, + @new_contract_name, + external_libs_string, + checked_evm_version, + path + ] + ) + + with {:ok, contracts} <- Jason.decode(response), + %{"abi" => abi, "evm" => %{"deployedBytecode" => %{"object" => bytecode}}} <- + get_contract_info(contracts, name) do + {:ok, %{"abi" => abi, "bytecode" => bytecode, "name" => name}} + else + {:error, %Jason.DecodeError{}} -> + {:error, :compilation} + + error -> + parse_error(error) + end end end diff --git a/apps/explorer/priv/compile_solc.js b/apps/explorer/priv/compile_solc.js index 7179ebda56..5aaf3f54d7 100755 --- a/apps/explorer/priv/compile_solc.js +++ b/apps/explorer/priv/compile_solc.js @@ -1,7 +1,5 @@ #!/usr/bin/env node -const solc = require('solc'); - var sourceCode = process.argv[2]; var version = process.argv[3]; var optimize = process.argv[4]; @@ -9,38 +7,38 @@ var optimizationRuns = parseInt(process.argv[5], 10); var newContractName = process.argv[6]; var externalLibraries = JSON.parse(process.argv[7]) var evmVersion = process.argv[8]; +var compilerVersionPath = process.argv[9]; + +var solc = require('solc') +var compilerSnapshot = require(compilerVersionPath); +var solc = solc.setupMethods(compilerSnapshot); -var compiled_code = solc.loadRemoteVersion(version, function (err, solcSnapshot) { - if (err) { - console.log(JSON.stringify(err.message)); - } else { - const input = { - language: 'Solidity', - sources: { - [newContractName]: { - content: sourceCode - } - }, - settings: { - evmVersion: evmVersion, - optimizer: { - enabled: optimize == '1', - runs: optimizationRuns - }, - libraries: { - [newContractName]: externalLibraries - }, - outputSelection: { - '*': { - '*': ['*'] - } - } +const input = { + language: 'Solidity', + sources: { + [newContractName]: { + content: sourceCode + } + }, + settings: { + evmVersion: evmVersion, + optimizer: { + enabled: optimize == '1', + runs: optimizationRuns + }, + libraries: { + [newContractName]: externalLibraries + }, + outputSelection: { + '*': { + '*': ['*'] } } - - const output = JSON.parse(solcSnapshot.compile(JSON.stringify(input))) - /** Older solc-bin versions don't use filename as contract key */ - const response = output.contracts[newContractName] || output.contracts[''] - console.log(JSON.stringify(response)); } -}); +} + + +const output = JSON.parse(solc.compile(JSON.stringify(input))) +/** Older solc-bin versions don't use filename as contract key */ +const response = output.contracts[newContractName] || output.contracts[''] +console.log(JSON.stringify(response));