diff --git a/.dialyzer-ignore b/.dialyzer-ignore index 44a7f6862a..1acfc06045 100644 --- a/.dialyzer-ignore +++ b/.dialyzer-ignore @@ -13,8 +13,6 @@ lib/block_scout_web/schema/types.ex:31 lib/phoenix/router.ex:324 lib/phoenix/router.ex:402 lib/explorer/smart_contract/reader.ex:435 -lib/explorer/exchange_rates/source.ex:139 -lib/explorer/exchange_rates/source.ex:142 lib/indexer/fetcher/polygon_edge.ex:737 lib/indexer/fetcher/polygon_edge/deposit_execute.ex:140 lib/indexer/fetcher/polygon_edge/deposit_execute.ex:184 diff --git a/.github/workflows/config.yml b/.github/workflows/config.yml index 31e89c223f..8ec57498b3 100644 --- a/.github/workflows/config.yml +++ b/.github/workflows/config.yml @@ -72,7 +72,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps- @@ -130,7 +130,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -154,7 +154,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -183,7 +183,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -227,7 +227,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -253,7 +253,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -282,7 +282,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -330,7 +330,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -376,7 +376,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -438,7 +438,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -498,7 +498,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -569,7 +569,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" @@ -637,7 +637,7 @@ jobs: path: | deps _build - key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_27-${{ hashFiles('mix.lock') }} + key: ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-mixlockhash_28-${{ hashFiles('mix.lock') }} restore-keys: | ${{ runner.os }}-${{ env.ELIXIR_VERSION }}-${{ env.OTP_VERSION }}-${{ env.MIX_ENV }}-deps-" diff --git a/CHANGELOG.md b/CHANGELOG.md index 9200b4a88e..c464c468cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,15 +2,15 @@ ## Current -- [#8924](https://github.com/blockscout/blockscout/pull/8924) - Delete invalid current token balances in OnDemand fetcher - ### Features +- [#8908](https://github.com/blockscout/blockscout/pull/8908) - Solidityscan report API endpoint - [#8900](https://github.com/blockscout/blockscout/pull/8900) - Add Compound proxy contract pattern - [#8611](https://github.com/blockscout/blockscout/pull/8611) - Implement sorting of smart contracts, address transactions ### Fixes +- [#8924](https://github.com/blockscout/blockscout/pull/8924) - Delete invalid current token balances in OnDemand fetcher - [#8922](https://github.com/blockscout/blockscout/pull/8922) - Allow call type to be in lowercase - [#8917](https://github.com/blockscout/blockscout/pull/8917) - Proxy detection hotfix in API v2 - [#8915](https://github.com/blockscout/blockscout/pull/8915) - smart-contract: delete embeds_many relation on replace diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index 56e66c7d0a..691d8de75b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -23,6 +23,9 @@ defmodule BlockScoutWeb.API.V2.FallbackController do @unauthorized "Unauthorized" @not_configured_api_key "API key not configured on the server" @wrong_api_key "Wrong API key" + @address_not_found "Address not found" + @address_is_not_smart_contract "Address is not smart-contract" + @empty_response "Empty response" def call(conn, {:format, _params}) do Logger.error(fn -> @@ -232,4 +235,25 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> put_view(ApiView) |> render(:message, %{message: @wrong_api_key}) end + + def call(conn, {:address, {:error, :not_found}}) do + conn + |> put_status(:not_found) + |> put_view(ApiView) + |> render(:message, %{message: @address_not_found}) + end + + def call(conn, {:is_smart_contract, result}) when is_nil(result) or result == false do + conn + |> put_status(:not_found) + |> put_view(ApiView) + |> render(:message, %{message: @address_is_not_smart_contract}) + end + + def call(conn, {:is_empty_response, true}) do + conn + |> put_status(500) + |> put_view(ApiView) + |> render(:message, %{message: @empty_response}) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex index 3fe7b47c26..4986c3a19a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/smart_contract_controller.ex @@ -12,9 +12,10 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do alias BlockScoutWeb.{AccessHelper, AddressView} alias Ecto.Association.NotLoaded alias Explorer.Chain - alias Explorer.Chain.SmartContract + alias Explorer.Chain.{Address, SmartContract} alias Explorer.SmartContract.{Reader, Writer} alias Explorer.SmartContract.Solidity.PublishHelper + alias Explorer.ThirdPartyIntegrations.SolidityScan @smart_contract_address_options [ necessity_by_association: %{ @@ -190,6 +191,29 @@ defmodule BlockScoutWeb.API.V2.SmartContractController do end end + @doc """ + /api/v2/smart-contracts/${address_hash_string}/solidityscan-report logic + """ + @spec solidityscan_report(Plug.Conn.t(), map()) :: + {:address, {:error, :not_found}} + | {:format_address, :error} + | {:is_empty_response, true} + | {:is_smart_contract, false | nil} + | {:restricted_access, true} + | Plug.Conn.t() + def solidityscan_report(conn, %{"address_hash" => address_hash_string} = params) do + with {:format_address, {:ok, address_hash}} <- {:format_address, Chain.string_to_address_hash(address_hash_string)}, + {:ok, false} <- AccessHelper.restricted_access?(address_hash_string, params), + {:address, {:ok, address}} <- {:address, Chain.hash_to_address(address_hash)}, + {:is_smart_contract, true} <- {:is_smart_contract, Address.is_smart_contract(address)}, + response = SolidityScan.solidityscan_request(address_hash_string), + {:is_empty_response, false} <- {:is_empty_response, is_nil(response)} do + conn + |> put_status(200) + |> json(response) + end + end + def smart_contracts_list(conn, params) do full_options = [necessity_by_association: %{[address: :token] => :optional, [address: :names] => :optional, address: :required}] diff --git a/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex index d3f1d5e162..ad8bf8ad96 100644 --- a/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/smart_contracts_api_v2_router.ex @@ -27,6 +27,7 @@ defmodule BlockScoutWeb.SmartContractsApiV2Router do get("/:address_hash/methods-read-proxy", V2.SmartContractController, :methods_read_proxy) get("/:address_hash/methods-write-proxy", V2.SmartContractController, :methods_write_proxy) post("/:address_hash/query-read-method", V2.SmartContractController, :query_read_method) + get("/:address_hash/solidityscan-report", V2.SmartContractController, :solidityscan_report) get("/verification/config", V2.VerificationController, :config) 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 9b82e01ede..9c490d9131 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 @@ -51,7 +51,7 @@ defmodule BlockScoutWeb.API.V2.Helper do defp address_with_info(%Address{} = address, _address_hash) do %{ "hash" => Address.checksum(address), - "is_contract" => is_smart_contract(address), + "is_contract" => Address.is_smart_contract(address), "name" => address_name(address), "implementation_name" => implementation_name(address), "is_verified" => is_verified(address) @@ -94,11 +94,6 @@ defmodule BlockScoutWeb.API.V2.Helper do def implementation_name(_), do: nil - def is_smart_contract(%Address{contract_code: nil}), do: false - def is_smart_contract(%Address{contract_code: _}), do: true - def is_smart_contract(%NotLoaded{}), do: nil - 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 diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex index c3e4d82887..ac8e395011 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/transaction_view.ex @@ -707,7 +707,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do _, skip_sc_check? ) do - if skip_sc_check? || Helper.is_smart_contract(to_address) do + if skip_sc_check? || Address.is_smart_contract(to_address) do "0x" <> Base.encode16(method_id, case: :lower) else nil @@ -760,7 +760,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do defp tx_types(%Transaction{to_address: to_address} = tx, types, :contract_call) do types = - if Helper.is_smart_contract(to_address) do + if Address.is_smart_contract(to_address) do [:contract_call | types] else types diff --git a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex index 6022c87fcc..b20cd898ed 100644 --- a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex +++ b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex @@ -5,16 +5,16 @@ defmodule Explorer.Account.Notifier.ForbiddenAddress do import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] + alias Explorer.Chain.Address + @blacklist [ burn_address_hash_string(), "0x000000000000000000000000000000000000dEaD" ] - alias Explorer.{AccessHelper, Repo} - alias Explorer.Chain.Token + alias Explorer.AccessHelper - import Ecto.Query, only: [from: 2] - import Explorer.Chain, only: [string_to_address_hash: 1] + import Explorer.Chain, only: [string_to_address_hash: 1, hash_to_address: 1] def check(address_string) when is_bitstring(address_string) do case format_address(address_string) do @@ -32,7 +32,7 @@ defmodule Explorer.Account.Notifier.ForbiddenAddress do {:error, "This address is blacklisted"} is_contract(address_hash) -> - {:error, "This address isn't personal"} + {:error, "This address isn't EOA"} match?({:restricted_access, true}, AccessHelper.restricted_access?(to_string(address_hash), %{})) -> {:error, "This address has restricted access"} @@ -43,14 +43,10 @@ defmodule Explorer.Account.Notifier.ForbiddenAddress do end defp is_contract(%Explorer.Chain.Hash{} = address_hash) do - query = - from( - token in Token, - where: token.contract_address_hash == ^address_hash - ) - - contract_addresses = Repo.all(query) - List.first(contract_addresses) + case hash_to_address(address_hash) do + {:error, :not_found} -> false + {:ok, address} -> Address.is_smart_contract(address) + end end defp format_address(address_hash_string) do diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index e8ea230059..96dc8c782c 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -7,6 +7,7 @@ defmodule Explorer.Chain.Address do use Explorer.Schema + alias Ecto.Association.NotLoaded alias Ecto.Changeset alias Explorer.{Chain, PagingOptions} @@ -332,6 +333,15 @@ defmodule Explorer.Chain.Address do end end + @doc """ + Checks if given address is smart-contract + """ + @spec is_smart_contract(any()) :: boolean() | nil + def is_smart_contract(%__MODULE__{contract_code: nil}), do: false + def is_smart_contract(%__MODULE__{contract_code: _}), do: true + def is_smart_contract(%NotLoaded{}), do: nil + def is_smart_contract(_), do: false + defp get_addresses(options) do accounts_with_n = fetch_top_addresses(options) diff --git a/apps/explorer/lib/explorer/exchange_rates/source.ex b/apps/explorer/lib/explorer/exchange_rates/source.ex index befa6560d0..8d50d4069b 100644 --- a/apps/explorer/lib/explorer/exchange_rates/source.ex +++ b/apps/explorer/lib/explorer/exchange_rates/source.ex @@ -6,6 +6,7 @@ defmodule Explorer.ExchangeRates.Source do alias Explorer.Chain.Hash alias Explorer.ExchangeRates.Source.CoinGecko alias Explorer.ExchangeRates.Token + alias Explorer.Helper alias HTTPoison.{Error, Response} @doc """ @@ -91,12 +92,6 @@ defmodule Explorer.ExchangeRates.Source do [{"Content-Type", "application/json"}] end - def decode_json(data) do - Jason.decode!(data) - rescue - _ -> data - end - def to_decimal(nil), do: nil def to_decimal(%Decimal{} = value), do: value @@ -135,32 +130,17 @@ defmodule Explorer.ExchangeRates.Source do {:error, %Error{reason: reason}} -> {:error, reason} - - {:error, :nxdomain} -> - {:error, "Source is not responsive"} - - {:error, _} -> - {:error, "Source unknown response"} end end defp parse_http_success_response(body) do - body_json = decode_json(body) + body_json = Helper.decode_json(body) - cond do - is_map(body_json) -> - {:ok, body_json} - - is_list(body_json) -> - {:ok, body_json} - - true -> - {:ok, body} - end + {:ok, body_json} end defp parse_http_error_response(body) do - body_json = decode_json(body) + body_json = Helper.decode_json(body) if is_map(body_json) do {:error, body_json["error"]} diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index 5004b1f267..45d92a2830 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -58,4 +58,27 @@ defmodule Explorer.Helper do Enum.map(list, fn el -> Map.put(el, preload_field, associated_elements[el[foreign_key_field]]) end) end + + @doc """ + Decode json + """ + @spec decode_json(any()) :: map() | list() | nil + def decode_json(nil), do: nil + + def decode_json(data) do + if String.valid?(data) do + safe_decode_json(data) + else + data + |> :unicode.characters_to_binary(:latin1) + |> safe_decode_json() + end + end + + defp safe_decode_json(data) do + case Jason.decode(data) do + {:ok, decoded} -> decoded + _ -> %{error: data} + end + end end diff --git a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex index e3d2bcde8a..3a0e898ff5 100644 --- a/apps/explorer/lib/explorer/smart_contract/compiler_version.ex +++ b/apps/explorer/lib/explorer/smart_contract/compiler_version.ex @@ -31,36 +31,25 @@ defmodule Explorer.SmartContract.CompilerVersion do end defp fetch_solc_versions do - if RustVerifierInterface.enabled?() do - RustVerifierInterface.get_versions_list() - else - headers = [{"Content-Type", "application/json"}] - - case HTTPoison.get(source_url(:solc), headers) do - {:ok, %{status_code: 200, body: body}} -> - {:ok, format_data(body, :solc)} - - {:ok, %{status_code: _status_code, body: body}} -> - {:error, decode_json(body)["error"]} - - {:error, %{reason: reason}} -> - {:error, reason} - end - end + fetch_compiler_versions(&RustVerifierInterface.get_versions_list/0, :solc) end defp fetch_vyper_versions do + fetch_compiler_versions(&RustVerifierInterface.vyper_get_versions_list/0, :vyper) + end + + defp fetch_compiler_versions(compiler_list_fn, compiler_type) do if RustVerifierInterface.enabled?() do - RustVerifierInterface.vyper_get_versions_list() + compiler_list_fn.() else headers = [{"Content-Type", "application/json"}] - case HTTPoison.get(source_url(:vyper), headers) do + case HTTPoison.get(source_url(compiler_type), headers) do {:ok, %{status_code: 200, body: body}} -> - {:ok, format_data(body, :vyper)} + {:ok, format_data(body, compiler_type)} {:ok, %{status_code: _status_code, body: body}} -> - {:error, decode_json(body)["error"]} + {:error, Helper.decode_json(body)["error"]} {:error, %{reason: reason}} -> {:error, reason} @@ -140,10 +129,6 @@ defmodule Explorer.SmartContract.CompilerVersion do end end - defp decode_json(json) do - Jason.decode!(json) - end - @spec source_url(:solc | :vyper) :: String.t() defp source_url(compiler) do case compiler do diff --git a/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex b/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex new file mode 100644 index 0000000000..40a5bffb97 --- /dev/null +++ b/apps/explorer/lib/explorer/third_party_integrations/solidityscan.ex @@ -0,0 +1,40 @@ +defmodule Explorer.ThirdPartyIntegrations.SolidityScan do + @moduledoc """ + Module for SolidityScan integration https://apidoc.solidityscan.com/solidityscan-security-api/solidityscan-other-apis/quickscan-api-v1 + """ + + alias Explorer.Helper + + @blockscout_platform_id "16" + @recv_timeout 60_000 + + @doc """ + Proxy request to solidityscan API endpoint for the given smart-contract + """ + @spec solidityscan_request(String.t()) :: any() + def solidityscan_request(address_hash_string) do + headers = [{"Authorization", "Token #{api_key()}"}] + + url = base_url(address_hash_string) + + case HTTPoison.get(url, headers, recv_timeout: @recv_timeout) do + {:ok, %HTTPoison.Response{status_code: 200, body: body}} -> + Helper.decode_json(body) + + _ -> + nil + end + end + + defp base_url(address_hash_string) do + "https://api.solidityscan.com/api/v1/quickscan/#{@blockscout_platform_id}/#{chain_id()}/#{address_hash_string}" + end + + defp chain_id do + Application.get_env(:explorer, __MODULE__)[:chain_id] + end + + defp api_key do + Application.get_env(:explorer, __MODULE__)[:api_key] + end +end diff --git a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex index b510fb355d..605a50547e 100644 --- a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex +++ b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex @@ -4,6 +4,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do """ use Tesla + alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.{Helper, RustVerifierInterface} alias HTTPoison.{Error, Response} alias Tesla.Multipart @@ -223,7 +224,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_verify_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do # Success status from native Sourcify server @@ -246,7 +247,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_check_by_address_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do [%{"status" => "perfect"}] -> @@ -264,11 +265,11 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_get_metadata_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do %{"message" => message, "errors" => errors} -> - {:error, "#{message}: #{decode_json(errors)}"} + {:error, "#{message}: #{ExplorerHelper.decode_json(errors)}"} metadata -> {:ok, metadata} @@ -276,11 +277,11 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end defp parse_get_metadata_any_http_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) case body_json do %{"message" => message, "errors" => errors} -> - {:error, "#{message}: #{decode_json(errors)}"} + {:error, "#{message}: #{ExplorerHelper.decode_json(errors)}"} %{"status" => status, "files" => metadata} -> {:ok, status, metadata} @@ -290,16 +291,23 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do end end + @invalid_json_response "invalid http error json response" defp parse_http_error_response(body) do - body_json = decode_json(body) + body_json = ExplorerHelper.decode_json(body) if is_map(body_json) do - {:error, body_json["error"]} + error = body_json["error"] + + parse_http_error_response_internal(error) else - {:error, body} + parse_http_error_response_internal(body) end end + defp parse_http_error_response_internal(nil), do: {:error, @invalid_json_response} + + defp parse_http_error_response_internal(data), do: {:error, data} + def parse_params_from_sourcify(address_hash_string, verification_metadata) do filtered_files = verification_metadata @@ -350,7 +358,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do defp parse_json_from_sourcify_for_insertion(verification_metadata_json) do %{"name" => _, "content" => content} = verification_metadata_json - content_json = decode_json(content) + content_json = ExplorerHelper.decode_json(content) compiler_version = "v" <> (content_json |> Map.get("compiler") |> Map.get("version")) abi = content_json |> Map.get("output") |> Map.get("abi") settings = Map.get(content_json, "settings") @@ -401,12 +409,6 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do |> Map.put("contract_source_code", content) end - def decode_json(data) do - Jason.decode!(data) - rescue - _ -> data - end - defp config(module, key) do :explorer |> Application.get_env(module) diff --git a/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex b/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex index 975b7bed5f..191d81a15d 100644 --- a/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex +++ b/apps/indexer/lib/indexer/fetcher/token_instance/metadata_retriever.ex @@ -5,6 +5,7 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do require Logger + alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.Reader alias HTTPoison.{Error, Response} @@ -130,7 +131,7 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do end defp fetch_json_from_uri({:ok, [json]}, hex_token_id) do - {:ok, json} = decode_json(json) + json = ExplorerHelper.decode_json(json) check_type(json, hex_token_id) rescue @@ -222,7 +223,7 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do check_type(json, nil) else - {:ok, json} = decode_json(body) + json = ExplorerHelper.decode_json(body) check_type(json, hex_token_id) end @@ -245,16 +246,6 @@ defmodule Indexer.Fetcher.TokenInstance.MetadataRetriever do content_type && String.starts_with?(content_type, "video/") end - defp decode_json(body) do - if String.valid?(body) do - Jason.decode(body) - else - body - |> :unicode.characters_to_binary(:latin1) - |> Jason.decode() - end - end - defp check_type(json, nil) when is_map(json) do {:ok, %{metadata: json}} end diff --git a/config/runtime.exs b/config/runtime.exs index 81459925ee..0f1ed1aa86 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -368,6 +368,10 @@ config :explorer, Explorer.ThirdPartyIntegrations.Sourcify, chain_id: System.get_env("CHAIN_ID"), repo_url: System.get_env("SOURCIFY_REPO_URL") || "https://repo.sourcify.dev/contracts" +config :explorer, Explorer.ThirdPartyIntegrations.SolidityScan, + chain_id: System.get_env("SOLIDITYSCAN_CHAIN_ID"), + api_key: System.get_env("SOLIDITYSCAN_API_TOKEN") + enabled? = ConfigHelper.parse_bool_env_var("MICROSERVICE_SC_VERIFIER_ENABLED") # or "eth_bytecode_db" type = System.get_env("MICROSERVICE_SC_VERIFIER_TYPE", "sc_verifier") diff --git a/cspell.json b/cspell.json index b76de4bdc9..72bfc7609c 100644 --- a/cspell.json +++ b/cspell.json @@ -534,7 +534,18 @@ "zindex", "zipcode", "zkbob", - "zkevm" + "zkevm", + "erts", + "Asfpp", + "Nerg", + "secp", + "qwertyuioiuytrewertyuioiuytrertyuio", + "urlset", + "lastmod", + "qitmeer", + "meer", + "DefiLlama", + "SOLIDITYSCAN" ], "enableFiletypes": [ "dotenv",