diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex new file mode 100644 index 0000000000..3c22cd96c7 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/visualize_sol2uml_controller.ex @@ -0,0 +1,41 @@ +defmodule BlockScoutWeb.VisualizeSol2umlController do + use BlockScoutWeb, :controller + alias Explorer.Chain + alias Explorer.Visualize.Sol2uml + + def index(conn, %{"address" => address_hash_string}) do + address_options = [ + necessity_by_association: %{ + :contracts_creation_internal_transaction => :optional, + :names => :optional, + :smart_contract => :optional, + :token => :optional, + :contracts_creation_transaction => :optional + } + ] + with true <- Sol2uml.enabled?(), + true <- Chain.smart_contract_fully_verified?(address_hash_string), + {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.find_contract_address(address_hash, address_options, true) do + sources = address.smart_contract_additional_sources + |> Enum.map(fn (additional_source) -> {additional_source.file_name, additional_source.contract_source_code} end) + |> Enum.into(%{}) + |> Map.merge(%{ + address.smart_contract.file_path => address.smart_contract.contract_source_code + }) + params = %{ + sources: sources + } + case Sol2uml.visualize_contracts(params) do + {:ok, svg} -> render(conn, "index.html", address: address, svg: svg, error: nil) + {:error, error} -> render(conn, "index.html", address: address, svg: nil, error: error) + end + else + _ -> not_found(conn) + end + end + + def index(conn, params) do + not_found(conn) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex index a5956a5dca..cd898026ea 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex @@ -5,6 +5,7 @@ <% 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?() %>
<% is_proxy = BlockScoutWeb.AddressView.smart_contract_is_proxy?(@address) %> @@ -23,7 +24,7 @@
<%= render BlockScoutWeb.CommonComponentsView, "_info.html" %> <%= gettext("Contract is not verified. However, we found a verified contract with the same bytecode in Blockscout DB") %> <%= link( - metadata_for_verification.address_hash, + metadata_for_verification.address_hash, to: address_contract_path(@conn, :index, metadata_for_verification.address_hash)) %>.
<%= gettext("All metadata displayed below is from that contract. In order to verify current contract, click") %> <%= gettext("Verify & Publish") %> <%= gettext("button") %>
<%= link(gettext("Verify & Publish"), to: path, class: "button button-primary button-sm float-right ml-3", "data-test": "verify_and_publish") %> @@ -98,9 +99,20 @@

<%= target_contract.file_path || gettext "Contract source code" %>

- +
+ <%= if visualize_sol2uml_enabled do %> + +
+
new
+ + Sol2uml +
+
+ <% end %> + +
><%= target_contract.contract_source_code %>
         
@@ -171,7 +183,7 @@
<%= creation_code(@address) %>
- <% end %> + <% end %> <%= if fully_verified do %>

<%= gettext "Deployed ByteCode" %>

diff --git a/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex new file mode 100644 index 0000000000..f0d2551269 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex @@ -0,0 +1,16 @@ +
+
+
+
+

<%= gettext("UML diagram") %>

+ <%= gettext("For contract") %> <%= link( + @address.hash, + to: address_contract_path(@conn, :index, @address.hash) + ) %> +
+
+ +
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex b/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex new file mode 100644 index 0000000000..827deeeecd --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/visualize_sol2uml_view.ex @@ -0,0 +1,3 @@ +defmodule BlockScoutWeb.VisualizeSol2umlView do + use BlockScoutWeb, :view +end diff --git a/apps/block_scout_web/lib/block_scout_web/web_router.ex b/apps/block_scout_web/lib/block_scout_web/web_router.ex index e03bd10d43..c356489ff1 100644 --- a/apps/block_scout_web/lib/block_scout_web/web_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/web_router.ex @@ -494,6 +494,8 @@ defmodule BlockScoutWeb.WebRouter do get("/token-counters", Tokens.TokenController, :token_counters) + get("/vis/sol2uml", VisualizeSol2umlController, :index) + get("/*path", PageNotFoundController, :index) end end diff --git a/apps/explorer/lib/explorer/visualize/sol2uml.ex b/apps/explorer/lib/explorer/visualize/sol2uml.ex new file mode 100644 index 0000000000..d54f6c3681 --- /dev/null +++ b/apps/explorer/lib/explorer/visualize/sol2uml.ex @@ -0,0 +1,74 @@ +defmodule Explorer.Visualize.Sol2uml do + @moduledoc """ + Adapter for sol2uml visualizer with https://github.com/blockscout/blockscout-rs/blob/main/visualizer + """ + alias HTTPoison.Response + require Logger + + @post_timeout :infinity + @request_error_msg "Error while sending request to visualizer microservice" + + def visualize_contracts(body) do + http_post_request(visualize_contracts_url(), body) + end + + def http_post_request(url, body) do + headers = [{"Content-Type", "application/json"}] + + case HTTPoison.post(url, Jason.encode!(body), headers, recv_timeout: @post_timeout) do + {:ok, %Response{body: body, status_code: 200}} -> + proccess_visualizer_response(body) + + {:ok, %Response{body: body, status_code: _}} -> + proccess_visualizer_response(body) + + {:error, error} -> + old_truncate = Application.get_env(:logger, :truncate) + Logger.configure(truncate: :infinity) + + Logger.error(fn -> + [ + "Error while sending request to visualizer microservice. url: #{url}, body: #{inspect(body, limit: :infinity, printable_limit: :infinity)}: ", + inspect(error, limit: :infinity, printable_limit: :infinity) + ] + end) + + Logger.configure(truncate: old_truncate) + {:error, @request_error_msg} + end + end + + + def proccess_visualizer_response(body) when is_binary(body) do + case Jason.decode(body) do + {:ok, decoded} -> + proccess_visualizer_response(decoded) + + _ -> + {:error, body} + end + end + + def proccess_visualizer_response(%{"svg" => svg}) do + {:ok, svg} + end + + def proccess_visualizer_response(other), do: {:error, other} + + def visualize_contracts_url, do: "#{base_api_url()}" <> "/solidity:visualizeContracts" + + def base_api_url, do: "#{base_url()}" <> "/api/v1" + + def base_url do + url = Application.get_env(:explorer, __MODULE__)[:service_url] + + if String.ends_with?(url, "/") do + url + |> String.slice(0..(String.length(url) - 2)) + else + url + end + end + + def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled] +end diff --git a/config/runtime.exs b/config/runtime.exs index 4f4bad6979..d3b31abf7e 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -215,6 +215,10 @@ config :explorer, else: Explorer.Chain.Events.DBSender ) +config :explorer, Explorer.Visualize.Sol2uml, + service_url: System.get_env("VISUALIZE_SOL2UML_SERVICE_URL"), + enabled: System.get_env("VISUALIZE_SOL2UML_ENABLED") == "true" + config :explorer, Explorer.Chain.Events.Listener, enabled: if(disable_webapp == "true" && disable_indexer == "true",