diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d4e6f755f..1c18c34df3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,17 @@ - [#2294](https://github.com/poanetwork/blockscout/pull/2294) - add healthy block period checking endpoint ### Fixes +- [#2310](https://github.com/poanetwork/blockscout/pull/2310) - parse url for api docs - [#2299](https://github.com/poanetwork/blockscout/pull/2299) - fix interpolation in error message - [#2303](https://github.com/poanetwork/blockscout/pull/2303) - fix transaction csv download link - [#2304](https://github.com/poanetwork/blockscout/pull/2304) - footer grid fix for md resolution - [#2291](https://github.com/poanetwork/blockscout/pull/2291) - dashboard fix for md resolution, transactions load fix, block info row fix, addresses page issue, check mark issue ### Chore +- [#2305](https://github.com/poanetwork/blockscout/pull/2305) - Improve Address controllers - [#2302](https://github.com/poanetwork/blockscout/pull/2302) - fix names for xDai source - [#2289](https://github.com/poanetwork/blockscout/pull/2289) - Optional websockets for dev environment +- [#2307](https://github.com/poanetwork/blockscout/pull/2307) - add GoJoy to README ## 2.0.1-beta diff --git a/README.md b/README.md index 5dc91fb8ef..3c95a5d125 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ Currently available full-featured block explorers (Etherscan, Etherchain, Blockc | | | [Kotti Testnet](https://kottiexplorer.ethernode.io/) | | | | [Loom](http://plasma-blockexplorer.dappchains.com/) | | | | [Tenda](https://tenda.network) | +| | | [GoJoy Chain](https://gojoychain.com/) | Current BlockScout versions for hosted projects are available [on the forum](https://forum.poa.network/t/deployed-instances-on-blockscout-com/1938). diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex index 312edffcca..d43f99a382 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do + {:ok, address} <- Chain.hash_to_address(address_hash, [], false) do full_options = paging_options(params) coin_balances_plus_one = Chain.address_to_coin_balances(address_hash, full_options) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex index c4f9b11c3b..fd5813a1d4 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do + {:ok, address} <- Chain.hash_to_address(address_hash, [], false) do full_options = [ necessity_by_association: %{ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex index db76b9d447..55070b4cd8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_logs_controller.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.AddressLogsController do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do + {:ok, address} <- Chain.hash_to_address(address_hash, [], false) do logs_plus_one = Chain.address_to_logs(address, paging_options(params)) {results, next_page} = split_list_by_page(logs_plus_one) @@ -74,7 +74,7 @@ defmodule BlockScoutWeb.AddressLogsController do def search_logs(conn, %{"topic" => topic, "address_id" => address_hash_string} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do + {:ok, address} <- Chain.hash_to_address(address_hash, [], false) do topic = String.trim(topic) formatted_topic = if String.starts_with?(topic, "0x"), do: topic, else: "0x" <> topic diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex index 312b90176b..8d5a60a1ac 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex @@ -12,7 +12,7 @@ defmodule BlockScoutWeb.AddressTokenController do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash) do + {:ok, address} <- Chain.hash_to_address(address_hash, [], false) do tokens_plus_one = Chain.address_tokens_with_balance(address_hash, paging_options(params)) {tokens, next_page} = split_list_by_page(tokens_plus_one) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 51503fd7c1..f0ff5f3f12 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -28,8 +28,10 @@ defmodule BlockScoutWeb.AddressTransactionController do ] def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do + address_options = [necessity_by_association: %{:names => :optional}] + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.hash_to_address(address_hash, [:names], false) do + {:ok, address} <- Chain.hash_to_address(address_hash, address_options, false) do options = @transaction_necessity_by_association |> put_in([:necessity_by_association, :block], :required) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex index 42170cbfc9..84036923de 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_validation_controller.ex @@ -17,7 +17,7 @@ defmodule BlockScoutWeb.AddressValidationController do def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, address} <- Chain.find_or_insert_address_from_hash(address_hash) do + {:ok, address} <- Chain.find_or_insert_address_from_hash(address_hash, [], false) do full_options = Keyword.merge( [ diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex index 3b23d4099a..4de35fc012 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex @@ -43,8 +43,10 @@ defmodule BlockScoutWeb.Tokens.HolderController do end def index(conn, %{"token_id" => address_hash_string}) do + options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, token} <- Chain.token_from_address_hash(address_hash, [{:contract_address, :smart_contract}]) do + {:ok, token} <- Chain.token_from_address_hash(address_hash, options) do render( conn, "index.html", diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex index bc9111a34d..b9783b2c63 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex @@ -60,8 +60,10 @@ defmodule BlockScoutWeb.Tokens.InventoryController do end def index(conn, %{"token_id" => address_hash_string}) do + options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, token} <- Chain.token_from_address_hash(address_hash, [{:contract_address, :smart_contract}]) do + {:ok, token} <- Chain.token_from_address_hash(address_hash, options) do render( conn, "index.html", diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex index 050802da54..f6ae63f537 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex @@ -4,8 +4,10 @@ defmodule BlockScoutWeb.Tokens.ReadContractController do alias Explorer.{Chain, Market} def index(conn, %{"token_id" => address_hash_string}) do + options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, token} <- Chain.token_from_address_hash(address_hash, [{:contract_address, :smart_contract}]) do + {:ok, token} <- Chain.token_from_address_hash(address_hash, options) do render( conn, "index.html", diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex index f92bcf2ff1..9b75087c45 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex @@ -44,8 +44,10 @@ defmodule BlockScoutWeb.Tokens.TransferController do end def index(conn, %{"token_id" => address_hash_string}) do + options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), - {:ok, token} <- Chain.token_from_address_hash(address_hash, [{:contract_address, :smart_contract}]) do + {:ok, token} <- Chain.token_from_address_hash(address_hash, options) do render( conn, "index.html", diff --git a/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex index f9c280925a..40860e37bb 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api_docs_view.ex @@ -36,7 +36,10 @@ defmodule BlockScoutWeb.APIDocsView do def blockscout_url do if System.get_env("BLOCKSCOUT_HOST") do - "http://" <> System.get_env("BLOCKSCOUT_HOST") + %URI{host: host, scheme: scheme} = URI.parse(Endpoint.url()) + path = System.get_env("NETWORK_PATH") || "/" + + scheme <> "://" <> host <> path else Endpoint.url() end diff --git a/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs new file mode 100644 index 0000000000..8501f27abc --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/api_docs_view_test.exs @@ -0,0 +1,25 @@ +defmodule BlockScoutWeb.ApiDocsViewTest do + use BlockScoutWeb.ConnCase, async: true + + alias BlockScoutWeb.{APIDocsView, Endpoint} + + describe "blockscout_url/0" do + test "returns url with scheme and host without port" do + System.put_env("BLOCKSCOUT_HOST", "localhost") + System.put_env("NETWORK_PATH", "") + + assert APIDocsView.blockscout_url() == "http://localhost" + assert Endpoint.url() == "http://localhost:4002" + end + + test "returns url with scheme and host with path" do + System.put_env("BLOCKSCOUT_HOST", "localhost/chain/dog") + System.put_env("NETWORK_PATH", "/chain/dog") + + assert APIDocsView.blockscout_url() == "http://localhost/chain/dog" + assert Endpoint.url() == "http://localhost:4002" + + System.put_env("NETWORK_PATH", "") + end + end +end diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 93dca921b9..8ef228da30 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -273,12 +273,12 @@ defmodule Explorer.Chain do inner_join: transaction in assoc(log, :transaction), order_by: [desc: transaction.block_number, desc: transaction.index], preload: [:transaction], - where: - log.address_hash == ^address_hash and - (transaction.block_number < ^block_number or - (transaction.block_number == ^block_number and transaction.index > ^transaction_index) or - (transaction.block_number == ^block_number and transaction.index == ^transaction_index and - log.index > ^log_index)), + where: transaction.block_number < ^block_number, + or_where: transaction.block_number == ^block_number and transaction.index > ^transaction_index, + or_where: + transaction.block_number == ^block_number and transaction.index == ^transaction_index and + log.index > ^log_index, + where: log.address_hash == ^address_hash, limit: ^paging_options.page_size, select: log ) @@ -676,31 +676,40 @@ defmodule Explorer.Chain do iex> Explorer.Chain.hash_to_address(hash) {:error, :not_found} - Optionally accepts: - - a list of bindings to preload, just like `Ecto.Query.preload/3` - - a boolean to also fetch the `has_decompiled_code?` virtual field or not + ## Options + + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.Address.t/0` has no associated record for that association, + then the `t:Explorer.Chain.Address.t/0` will not be included in the list. + + Optionally it also accepts a boolean to fetch the `has_decompiled_code?` virtual field or not """ - @spec hash_to_address(Hash.Address.t(), [Macro.t()], boolean()) :: {:ok, Address.t()} | {:error, :not_found} + @spec hash_to_address(Hash.Address.t(), [necessity_by_association_option], boolean()) :: + {:ok, Address.t()} | {:error, :not_found} def hash_to_address( %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, - preloads \\ [ - :contracts_creation_internal_transaction, - :names, - :smart_contract, - :token, - :contracts_creation_transaction + options \\ [ + necessity_by_association: %{ + :contracts_creation_internal_transaction => :optional, + :names => :optional, + :smart_contract => :optional, + :token => :optional, + :contracts_creation_transaction => :optional + } ], query_decompiled_code_flag \\ true ) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + query = from( address in Address, - preload: ^preloads, where: address.hash == ^hash ) query + |> join_associations(necessity_by_association) |> with_decompiled_code_flag(hash, query_decompiled_code_flag) |> Repo.one() |> case do @@ -772,16 +781,39 @@ defmodule Explorer.Chain do iex> {:ok, %Explorer.Chain.Address{hash: found_hash}} = Explorer.Chain.hash_to_address(hash) iex> found_hash == hash true + + + ## Options + + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Explorer.Chain.Address.t/0` has no associated record for that association, + then the `t:Explorer.Chain.Address.t/0` will not be included in the list. + + Optionally it also accepts a boolean to fetch the `has_decompiled_code?` virtual field or not + """ - @spec find_or_insert_address_from_hash(Hash.Address.t()) :: {:ok, Address.t()} - def find_or_insert_address_from_hash(%Hash{byte_count: unquote(Hash.Address.byte_count())} = hash) do - case hash_to_address(hash) do + @spec find_or_insert_address_from_hash(Hash.Address.t(), [necessity_by_association_option], boolean()) :: + {:ok, Address.t()} + def find_or_insert_address_from_hash( + %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, + options \\ [ + necessity_by_association: %{ + :contracts_creation_internal_transaction => :optional, + :names => :optional, + :smart_contract => :optional, + :token => :optional, + :contracts_creation_transaction => :optional + } + ], + query_decompiled_code_flag \\ true + ) do + case hash_to_address(hash, options, query_decompiled_code_flag) do {:ok, address} -> {:ok, address} {:error, :not_found} -> create_address(%{hash: to_string(hash)}) - hash_to_address(hash) + hash_to_address(hash, options, query_decompiled_code_flag) end end @@ -2720,21 +2752,30 @@ defmodule Explorer.Chain do @doc """ Fetches a `t:Token.t/0` by an address hash. - Optionally accepts a list of bindings to preload, just like `Ecto.Query.preload/3` + ## Options + + * `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is + `:required`, and the `t:Token.t/0` has no associated record for that association, + then the `t:Token.t/0` will not be included in the list. """ - @spec token_from_address_hash(Hash.Address.t(), [Macro.t()]) :: {:ok, Token.t()} | {:error, :not_found} + @spec token_from_address_hash(Hash.Address.t(), [necessity_by_association_option]) :: + {:ok, Token.t()} | {:error, :not_found} def token_from_address_hash( %Hash{byte_count: unquote(Hash.Address.byte_count())} = hash, - preloads \\ [] + options \\ [] ) do + necessity_by_association = Keyword.get(options, :necessity_by_association, %{}) + query = from( token in Token, - where: token.contract_address_hash == ^hash, - preload: ^preloads + where: token.contract_address_hash == ^hash ) - case Repo.one(query) do + query + |> join_associations(necessity_by_association) + |> Repo.one() + |> case do nil -> {:error, :not_found} diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index cfb8b5ab14..bcc3843102 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -3456,9 +3456,9 @@ defmodule Explorer.ChainTest do smart_contract = build(:smart_contract) address = insert(:address, smart_contract: smart_contract) token = insert(:token, contract_address: address) + options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] - assert {:ok, result} = - Chain.token_from_address_hash(token.contract_address_hash, [{:contract_address, :smart_contract}]) + assert {:ok, result} = Chain.token_from_address_hash(token.contract_address_hash, options) assert smart_contract = result.contract_address.smart_contract end diff --git a/apps/indexer/lib/indexer/fetcher/token.ex b/apps/indexer/lib/indexer/fetcher/token.ex index e0992a450f..8c924e2ad2 100644 --- a/apps/indexer/lib/indexer/fetcher/token.ex +++ b/apps/indexer/lib/indexer/fetcher/token.ex @@ -52,7 +52,9 @@ defmodule Indexer.Fetcher.Token do @impl BufferedTask @decorate trace(name: "fetch", resource: "Indexer.Fetcher.Token.run/2", service: :indexer, tracer: Tracer) def run([token_contract_address], _json_rpc_named_arguments) do - case Chain.token_from_address_hash(token_contract_address, [{:contract_address, :smart_contract}]) do + options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] + + case Chain.token_from_address_hash(token_contract_address, options) do {:ok, %Token{} = token} -> catalog_token(token) end diff --git a/apps/indexer/lib/indexer/fetcher/token_updater.ex b/apps/indexer/lib/indexer/fetcher/token_updater.ex index 304f2f068c..f596687ff8 100644 --- a/apps/indexer/lib/indexer/fetcher/token_updater.ex +++ b/apps/indexer/lib/indexer/fetcher/token_updater.ex @@ -36,8 +36,10 @@ defmodule Indexer.Fetcher.TokenUpdater do @doc false def update_metadata(token_addresses) when is_list(token_addresses) do + options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}] + Enum.each(token_addresses, fn address -> - case Chain.token_from_address_hash(address, [{:contract_address, :smart_contract}]) do + case Chain.token_from_address_hash(address, options) do {:ok, %Token{cataloged: true} = token} -> update_metadata(token) end