diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex index 959add1e09..228fb91a0b 100644 --- a/apps/explorer/lib/explorer/token/metadata_retriever.ex +++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex @@ -13,7 +13,6 @@ defmodule Explorer.Token.MetadataRetriever do @no_uri_error "no uri" @vm_execution_error "VM execution error" - @ipfs_protocol "ipfs://" @invalid_base64_data "invalid data:application/json;base64" # https://eips.ethereum.org/EIPS/eip-1155#metadata @@ -515,31 +514,11 @@ defmodule Explorer.Token.MetadataRetriever do end end - # CIDv0 IPFS links # https://docs.ipfs.tech/concepts/content-addressing/#version-0-v0 - defp fetch_json_from_uri({:ok, ["Qm" <> _ = result]}, _, token_id, hex_token_id, from_base_uri?) do - if String.length(result) == 46 do - ipfs? = true - fetch_json_from_uri({:ok, [ipfs_link(result)]}, ipfs?, token_id, hex_token_id, from_base_uri?) - else - Logger.warning(["Unknown metadata format result #{inspect(result)}."], fetcher: :token_instances) - - {:error, truncate_error(result)} - end - end - defp fetch_json_from_uri({:ok, ["'" <> token_uri]}, ipfs?, token_id, hex_token_id, from_base_uri?) do token_uri = token_uri |> String.split("'") |> List.first() fetch_metadata_inner(token_uri, ipfs?, token_id, hex_token_id, from_base_uri?) end - defp fetch_json_from_uri({:ok, ["http://" <> _ = token_uri]}, ipfs?, token_id, hex_token_id, from_base_uri?) do - fetch_metadata_inner(token_uri, ipfs?, token_id, hex_token_id, from_base_uri?) - end - - defp fetch_json_from_uri({:ok, ["https://" <> _ = token_uri]}, ipfs?, token_id, hex_token_id, from_base_uri?) do - fetch_metadata_inner(token_uri, ipfs?, token_id, hex_token_id, from_base_uri?) - end - defp fetch_json_from_uri( {:ok, [type = "data:application/json;utf8," <> json]}, ipfs?, @@ -587,35 +566,63 @@ defmodule Explorer.Token.MetadataRetriever do {:error, @invalid_base64_data} end - defp fetch_json_from_uri({:ok, ["#{@ipfs_protocol}ipfs/" <> right]}, _ipfs?, _token_id, hex_token_id, _from_base_uri?) do - fetch_from_ipfs(right, hex_token_id) + defp fetch_json_from_uri({:ok, [token_uri_string]}, ipfs?, token_id, hex_token_id, from_base_uri?) do + fetch_from_ipfs?(token_uri_string, ipfs?, token_id, hex_token_id, from_base_uri?) end - defp fetch_json_from_uri({:ok, ["ipfs/" <> right]}, _ipfs?, _token_id, hex_token_id, _from_base_uri?) do - fetch_from_ipfs(right, hex_token_id) - end + defp fetch_json_from_uri(uri, _ipfs?, _token_id, _hex_token_id, _from_base_uri?) do + Logger.warning(["Unknown metadata uri format #{inspect(uri)}."], fetcher: :token_instances) - defp fetch_json_from_uri({:ok, [@ipfs_protocol <> right]}, _ipfs?, _token_id, hex_token_id, _from_base_uri?) do - fetch_from_ipfs(right, hex_token_id) + {:error, "unknown metadata uri format"} end - defp fetch_json_from_uri({:ok, [json]}, _ipfs?, _token_id, hex_token_id, _from_base_uri?) do - json = ExplorerHelper.decode_json(json, true) + # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity + defp fetch_from_ipfs?(token_uri_string, ipfs?, token_id, hex_token_id, from_base_uri?) do + case URI.parse(token_uri_string) do + %URI{scheme: "ipfs", path: path} -> + resource_id = + case path do + "/ipfs/" <> resource_id -> + resource_id + + "/" <> resource_id -> + resource_id + end + + fetch_from_ipfs(resource_id, hex_token_id) + + %URI{scheme: _, path: "/ipfs/" <> resource_id} -> + fetch_from_ipfs(resource_id, hex_token_id) + + %URI{scheme: _, path: "ipfs/" <> resource_id} -> + fetch_from_ipfs(resource_id, hex_token_id) + + %URI{scheme: scheme} when not is_nil(scheme) -> + fetch_metadata_inner(token_uri_string, ipfs?, token_id, hex_token_id, from_base_uri?) + + %URI{path: path} -> + case path do + "Qm" <> <<_::binary-size(44)>> = resource_id -> + fetch_from_ipfs(resource_id, hex_token_id) - check_type(json, hex_token_id) + # todo: rewrite for strict CID v1 support + "bafybe" <> _ = resource_id -> + fetch_from_ipfs(resource_id, hex_token_id) + + _ -> + json = ExplorerHelper.decode_json(token_uri_string, true) + + check_type(json, hex_token_id) + end + end rescue e -> - Logger.warning(["Unknown metadata format #{inspect(json)}.", Exception.format(:error, e, __STACKTRACE__)], + Logger.warning( + ["Unknown metadata format #{inspect(token_uri_string)}.", Exception.format(:error, e, __STACKTRACE__)], fetcher: :token_instances ) - {:error, "invalid json"} - end - - defp fetch_json_from_uri(uri, _ipfs?, _token_id, _hex_token_id, _from_base_uri?) do - Logger.warning(["Unknown metadata uri format #{inspect(uri)}."], fetcher: :token_instances) - - {:error, "unknown metadata uri format"} + {:error, "invalid token_uri_string"} end defp fetch_json_from_json_string(json, ipfs?, token_id, hex_token_id, from_base_uri?, type) do diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs index 97d59bd1dd..1b73ff2225 100644 --- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs +++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs @@ -885,7 +885,7 @@ defmodule Explorer.Token.MetadataRetrieverTest do }} end - test "fetches image from ipfs link directly", %{bypass: bypass} do + test "fetches image from ipfs link directly" do path = "/ipfs/bafybeig6nlmyzui7llhauc52j2xo5hoy4lzp6442lkve5wysdvjkizxonu" json = """ @@ -894,14 +894,19 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ - Bypass.expect(bypass, "GET", path, fn conn -> - Conn.resp(conn, 200, json) + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + Explorer.Mox.HTTPoison + |> expect(:get, fn "https://ipfs.io/ipfs/bafybeig6nlmyzui7llhauc52j2xo5hoy4lzp6442lkve5wysdvjkizxonu", + _headers, + _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: json}} end) data = {:ok, [ - "http://localhost:#{bypass.port}#{path}" + path ]} assert {:ok, @@ -912,7 +917,7 @@ defmodule Explorer.Token.MetadataRetrieverTest do }} == MetadataRetriever.fetch_json(data) end - test "Fetches metadata from ipfs", %{bypass: bypass} do + test "Fetches metadata from ipfs" do path = "/ipfs/bafybeid4ed2ua7fwupv4nx2ziczr3edhygl7ws3yx6y2juon7xakgj6cfm/51.json" json = """ @@ -921,14 +926,19 @@ defmodule Explorer.Token.MetadataRetrieverTest do } """ - Bypass.expect(bypass, "GET", path, fn conn -> - Conn.resp(conn, 200, json) + Application.put_env(:explorer, :http_adapter, Explorer.Mox.HTTPoison) + + Explorer.Mox.HTTPoison + |> expect(:get, fn "https://ipfs.io/ipfs/bafybeid4ed2ua7fwupv4nx2ziczr3edhygl7ws3yx6y2juon7xakgj6cfm/51.json", + _headers, + _options -> + {:ok, %HTTPoison.Response{status_code: 200, body: json}} end) data = {:ok, [ - "http://localhost:#{bypass.port}#{path}" + path ]} {:ok, @@ -937,6 +947,7 @@ defmodule Explorer.Token.MetadataRetrieverTest do }} = MetadataRetriever.fetch_json(data) assert "ipfs://bafybeihxuj3gxk7x5p36amzootyukbugmx3pw7dyntsrohg3se64efkuga/51.png" == Map.get(metadata, "image") + Application.put_env(:explorer, :http_adapter, HTTPoison) end test "Fetches metadata from '${url}'", %{bypass: bypass} do diff --git a/cspell.json b/cspell.json index 7eda2f9f16..6d3d91b6b3 100644 --- a/cspell.json +++ b/cspell.json @@ -41,6 +41,7 @@ "badhash", "badnumber", "badpassword", + "bafybe", "bafybeid", "bafybeig", "bafybeihxuj",