diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex
index e86c15880e60..9eb3c96ea58a 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex
@@ -1,6 +1,6 @@
<% metadata_for_verification = Chain.get_address_verified_twin_contract(@address_hash).verified_contract %>
<% changeset = (if assigns[:retrying], do: @changeset, else: SmartContract.merge_twin_contract_with_changeset(metadata_for_verification, @changeset)) |> SmartContract.address_to_checksum_address() %>
-<% fetch_constructor_arguments_automatically = if metadata_for_verification, do: true, else: changeset.changes.autodetect_constructor_args %>
+<% fetch_constructor_arguments_automatically = if metadata_for_verification, do: true, else: changeset.changes[:autodetect_constructor_args] || true %>
<% display_constructor_arguments_text_area = if fetch_constructor_arguments_automatically, do: "none", else: "block" %>
<%= render BlockScoutWeb.CommonComponentsView, "_channel_disconnected_message.html", text: gettext("Connection Lost") %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex
index 469380ca6e82..4cb36fc3a73f 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/address_write_contract/index.html.eex
@@ -62,5 +62,4 @@
<% end %>
-
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex
index 180d51d9d04d..c2655c48ecf7 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction_state/_state_change.html.eex
@@ -1,7 +1,6 @@
-
<% coin_or_transfer = if @coin_or_token_transfers == :coin, do: :coin, else: elem(List.first(@coin_or_token_transfers), 1)%>
<%= if coin_or_transfer != :coin and coin_or_transfer.token.type != "ERC-20" or has_diff?(@balance_diff) do %>
-
<%= if @address.hash == @burn_address_hash do %>
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 000000000000..b78d3ef514ec
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/templates/visualize_sol2uml/index.html.eex
@@ -0,0 +1,36 @@
+
+
+
+
+ <%= gettext("UML diagram") %>
+
+ <%= gettext("For contract") %>
+ <%= link to: address_contract_path(@conn, :index, @address), "data-test": "address_hash_link" do %>
+ <%= render(
+ BlockScoutWeb.AddressView,
+ "_responsive_hash.html",
+ address: @address,
+ contract: true,
+ use_custom_tooltip: false
+ ) %>
+ <% end %>
+
+
+
+
+
+
+
+
+
+
+
+ ![]()
+
+
+
+
+
+
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex
index 3a120c597f19..f6e676d7fcdf 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_flattened_code_view.ex
@@ -3,4 +3,5 @@ defmodule BlockScoutWeb.AddressContractVerificationViaFlattenedCodeView do
alias Explorer.Chain
alias Explorer.Chain.SmartContract
+ alias Explorer.SmartContract.RustVerifierInterface
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex
index 7ec8e9b7d989..12a80e5eb282 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_verification_via_multi_part_files_view.ex
@@ -3,4 +3,5 @@ defmodule BlockScoutWeb.AddressContractVerificationViaMultiPartFilesView do
alias Explorer.Chain
alias Explorer.Chain.SmartContract
+ alias Explorer.SmartContract.RustVerifierInterface
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
index 8856d14c3045..ce7b4dc00ba1 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
@@ -130,7 +130,7 @@ defmodule BlockScoutWeb.AddressContractView do
end
def creation_code(%Address{contracts_creation_internal_transaction: %InternalTransaction{}} = address) do
- address.contracts_creation_internal_transaction.input
+ address.contracts_creation_internal_transaction.init
end
def creation_code(%Address{contracts_creation_transaction: %Transaction{}} = address) do
diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
index f06c97e33443..e08ee9836b44 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex
@@ -242,9 +242,7 @@ defmodule BlockScoutWeb.AddressView do
@doc """
Returns the primary name of an address if available. If there is no names on address function performs preload of names association.
"""
- def primary_name(_, second_time? \\ false)
-
- def primary_name(%Address{names: [_ | _] = address_names}, _second_time?) do
+ def primary_name(%Address{names: [_ | _] = address_names}) do
case Enum.find(address_names, &(&1.primary == true)) do
nil ->
%Address.Name{name: name} = Enum.at(address_names, 0)
@@ -255,11 +253,20 @@ defmodule BlockScoutWeb.AddressView do
end
end
- def primary_name(%Address{names: _} = address, false) do
- primary_name(Repo.preload(address, [:names]), true)
+ def primary_name(%Address{names: %Ecto.Association.NotLoaded{}} = address) do
+ primary_name(Repo.preload(address, [:names]))
end
- def primary_name(%Address{names: _}, true), do: nil
+ def primary_name(%Address{names: _} = address) do
+ with false <- is_nil(address.contract_code),
+ twin <- Chain.get_verified_twin_contract(address),
+ false <- is_nil(twin) do
+ twin.name
+ else
+ _ ->
+ nil
+ end
+ end
def implementation_name(%Address{smart_contract: %{implementation_name: implementation_name}}),
do: implementation_name
@@ -308,22 +315,22 @@ defmodule BlockScoutWeb.AddressView do
def smart_contract_verified?(%Address{smart_contract: nil}), do: false
def smart_contract_with_read_only_functions?(%Address{smart_contract: %SmartContract{}} = address) do
- Enum.any?(address.smart_contract.abi, &is_read_function?(&1))
+ Enum.any?(address.smart_contract.abi || [], &is_read_function?(&1))
end
def smart_contract_with_read_only_functions?(%Address{smart_contract: nil}), do: false
def is_read_function?(function), do: Helper.queriable_method?(function) || Helper.read_with_wallet_method?(function)
- def smart_contract_is_proxy?(%Address{smart_contract: %SmartContract{}} = address) do
- Chain.proxy_contract?(address.hash, address.smart_contract.abi)
+ def smart_contract_is_proxy?(%Address{smart_contract: %SmartContract{} = smart_contract}) do
+ SmartContract.proxy_contract?(smart_contract)
end
def smart_contract_is_proxy?(%Address{smart_contract: nil}), do: false
def smart_contract_with_write_functions?(%Address{smart_contract: %SmartContract{}} = address) do
Enum.any?(
- address.smart_contract.abi,
+ address.smart_contract.abi || [],
&Writer.write_function?(&1)
)
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex
index ace6af7517b1..d1686cef1565 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/rpc/contract_view.ex
@@ -52,6 +52,18 @@ defmodule BlockScoutWeb.API.RPC.ContractView do
|> set_external_libraries(contract)
|> set_verified_contract_data(contract, address, optimization)
|> set_proxy_info(contract)
+ |> set_compiler_settings(contract)
+ end
+
+ defp set_compiler_settings(contract_output, contract) when contract == %{}, do: contract_output
+
+ defp set_compiler_settings(contract_output, contract) do
+ if is_nil(contract.compiler_settings) do
+ contract_output
+ else
+ contract_output
+ |> Map.put(:CompilerSettings, contract.compiler_settings)
+ end
end
defp set_proxy_info(contract_output, contract) when contract == %{} do
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
index 54511abc0027..6d43e493e195 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_view.ex
@@ -1,8 +1,12 @@
defmodule BlockScoutWeb.API.V2.AddressView do
use BlockScoutWeb, :view
+ alias BlockScoutWeb.AddressView
alias BlockScoutWeb.API.V2.{ApiView, Helper, TokenView}
alias BlockScoutWeb.API.V2.Helper
+ alias Explorer.{Chain, Market}
+ alias Explorer.Chain.{Address, SmartContract}
+ alias Explorer.ExchangeRates.Token
def render("message.json", assigns) do
ApiView.render("message.json", assigns)
@@ -29,7 +33,38 @@ defmodule BlockScoutWeb.API.V2.AddressView do
end
def prepare_address(address, conn \\ nil) do
- Helper.address_with_info(conn, address, address.hash)
+ base_info = Helper.address_with_info(conn, address, address.hash)
+ is_proxy = AddressView.smart_contract_is_proxy?(address)
+
+ {implementation_address, implementation_name} =
+ with true <- is_proxy,
+ {address, name} <- SmartContract.get_implementation_address_hash(address.smart_contract),
+ false <- is_nil(address),
+ {:ok, address_hash} <- Chain.string_to_address_hash(address),
+ checksummed_address <- Address.checksum(address_hash) do
+ {checksummed_address, name}
+ else
+ _ ->
+ {nil, nil}
+ end
+
+ balance = address.fetched_coin_balance && address.fetched_coin_balance.value
+ exchange_rate = (Market.get_exchange_rate(Explorer.coin()) || Token.null()).usd_value
+
+ creator_hash = AddressView.from_address_hash(address)
+ creation_tx = creator_hash && AddressView.transaction_hash(address)
+ token = address.token && TokenView.render("token.json", %{token: Market.add_price(address.token)})
+
+ Map.merge(base_info, %{
+ "creator_address_hash" => creator_hash && Address.checksum(creator_hash),
+ "creation_tx_hash" => creation_tx,
+ "token" => token,
+ "coin_balance" => balance,
+ "exchange_rate" => exchange_rate,
+ "implementation_name" => implementation_name,
+ "implementation_address" => implementation_address,
+ "block_number_balance_updated_at" => address.fetched_coin_balance_block_number
+ })
end
def prepare_token_balance({token_balance, token}) do
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 57f914bc0ce1..6dc1808e318b 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
@@ -10,6 +10,10 @@ defmodule BlockScoutWeb.API.V2.Helper do
import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
import BlockScoutWeb.Models.GetAddressTags, only: [get_address_tags: 2, get_tags_on_address: 1]
+ def address_with_info(_, _, nil) do
+ nil
+ end
+
def address_with_info(conn, address, address_hash) do
%{
personal_tags: private_tags,
@@ -27,7 +31,7 @@ defmodule BlockScoutWeb.API.V2.Helper do
def address_with_info(%Address{} = address, _address_hash) do
%{
- "hash" => to_string(address),
+ "hash" => Address.checksum(address),
"is_contract" => is_smart_contract(address),
"name" => address_name(address),
"implementation_name" => implementation_name(address),
@@ -39,8 +43,18 @@ defmodule BlockScoutWeb.API.V2.Helper do
address_with_info(nil, address_hash)
end
+ def address_with_info(nil, nil) do
+ nil
+ end
+
def address_with_info(nil, address_hash) do
- %{"hash" => address_hash, "is_contract" => false, "name" => nil, "implementation_name" => nil, "is_verified" => nil}
+ %{
+ "hash" => Address.checksum(address_hash),
+ "is_contract" => false,
+ "name" => nil,
+ "implementation_name" => nil,
+ "is_verified" => nil
+ }
end
def address_name(%Address{names: [_ | _] = address_names}) do
diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
index e0bdae9c989f..66ae64783ffe 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/token_view.ex
@@ -1,13 +1,41 @@
defmodule BlockScoutWeb.API.V2.TokenView do
+ alias BlockScoutWeb.API.V2.Helper
+ alias Explorer.Chain.Address
+
def render("token.json", %{token: token}) do
%{
- "address" => token.contract_address_hash,
+ "address" => Address.checksum(token.contract_address_hash),
"symbol" => token.symbol,
"name" => token.name,
"decimals" => token.decimals,
"type" => token.type,
- "holders" => to_string(token.holder_count),
- "exchange_rate" => token.usd_value && to_string(token.usd_value)
+ "holders" => token.holder_count && to_string(token.holder_count),
+ "exchange_rate" => exchange_rate(token),
+ "total_supply" => token.total_supply
+ }
+ end
+
+ def render("token_balances.json", %{
+ token_balances: token_balances,
+ next_page_params: next_page_params,
+ conn: conn,
+ token: token
+ }) do
+ %{
+ "items" => Enum.map(token_balances, &prepare_token_balance(&1, conn, token)),
+ "next_page_params" => next_page_params
+ }
+ end
+
+ def exchange_rate(%{usd_value: usd_value}) when not is_nil(usd_value), do: to_string(usd_value)
+ def exchange_rate(_), do: nil
+
+ def prepare_token_balance(token_balance, conn, token) do
+ %{
+ "address" => Helper.address_with_info(conn, token_balance.address, token_balance.address_hash),
+ "value" => token_balance.value,
+ "token_id" => token_balance.token_id,
+ "token" => render("token.json", %{token: token})
}
end
end
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 c2f85b2ba62f..651547ba7f3c 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
@@ -43,8 +43,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
%{"method_id" => method_id, "method_call" => text, "parameters" => prepare_method_mapping(mapping)}
end
- def render("revert_reason.json", %{raw: raw, decoded: decoded}) do
- %{"raw" => raw, "decoded" => decoded}
+ def render("revert_reason.json", %{raw: raw}) do
+ %{"raw" => raw}
end
def render("token_transfers.json", %{token_transfers: token_transfers, next_page_params: next_page_params, conn: conn}) do
@@ -88,7 +88,8 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"to" => Helper.address_with_info(conn, token_transfer.to_address, token_transfer.to_address_hash),
"total" => prepare_token_transfer_total(token_transfer),
"token" => TokenView.render("token.json", %{token: Market.add_price(token_transfer.token)}),
- "type" => Chain.get_token_transfer_type(token_transfer)
+ "type" => Chain.get_token_transfer_type(token_transfer),
+ "timestamp" => block_timestamp(token_transfer.block)
}
end
@@ -204,7 +205,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
"result" => status,
"status" => transaction.status,
"block" => transaction.block_number,
- "timestamp" => transaction.block && transaction.block.timestamp,
+ "timestamp" => block_timestamp(transaction.block),
"from" => Helper.address_with_info(conn, transaction.from_address, transaction.from_address_hash),
"to" => Helper.address_with_info(conn, transaction.to_address, transaction.to_address_hash),
"created_contract" =>
@@ -286,8 +287,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
_ ->
hex = TransactionView.get_pure_transaction_revert_reason(transaction)
- utf8 = TransactionView.decoded_revert_reason(transaction)
- render(__MODULE__, "revert_reason.json", raw: hex, decoded: utf8)
+ render(__MODULE__, "revert_reason.json", raw: hex)
end
end
end
@@ -444,4 +444,7 @@ defmodule BlockScoutWeb.API.V2.TransactionView do
types
end
end
+
+ defp block_timestamp(%Block{} = block), do: block.timestamp
+ defp block_timestamp(_), do: nil
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
index 4a25a937fdc8..1c0c748dc415 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/instance/overview_view.ex
@@ -122,7 +122,7 @@ defmodule BlockScoutWeb.Tokens.Instance.OverviewView do
def smart_contract_with_read_only_functions?(
%Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token
) do
- Enum.any?(token.contract_address.smart_contract.abi, &Helper.queriable_method?(&1))
+ Enum.any?(token.contract_address.smart_contract.abi || [], &Helper.queriable_method?(&1))
end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false
diff --git a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
index e258938f1b95..4fc1c6d28ef4 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/tokens/overview_view.ex
@@ -48,13 +48,13 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
def smart_contract_with_read_only_functions?(
%Token{contract_address: %Address{smart_contract: %SmartContract{}}} = token
) do
- Enum.any?(token.contract_address.smart_contract.abi, &Helper.queriable_method?(&1))
+ Enum.any?(token.contract_address.smart_contract.abi || [], &Helper.queriable_method?(&1))
end
def smart_contract_with_read_only_functions?(%Token{contract_address: %Address{smart_contract: nil}}), do: false
- def smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: %SmartContract{}} = address}) do
- Chain.proxy_contract?(address.hash, address.smart_contract.abi)
+ def smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: %SmartContract{} = smart_contract}}) do
+ SmartContract.proxy_contract?(smart_contract)
end
def smart_contract_is_proxy?(%Token{contract_address: %Address{smart_contract: nil}}), do: false
@@ -63,7 +63,7 @@ defmodule BlockScoutWeb.Tokens.OverviewView do
contract_address: %Address{smart_contract: %SmartContract{}} = address
}) do
Enum.any?(
- address.smart_contract.abi,
+ address.smart_contract.abi || [],
&Writer.write_function?(&1)
)
end
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 000000000000..827deeeecdec
--- /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 88a5fb2f8478..db5dc981e0c7 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
@@ -529,6 +529,8 @@ defmodule BlockScoutWeb.WebRouter do
get("/makerdojo", MakerdojoController, :index)
+ get("/visualize/sol2uml", VisualizeSol2umlController, :index)
+
get("/*path", PageNotFoundController, :index)
end
end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
new file mode 100644
index 000000000000..33fbe54aee77
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/address_controller_test.exs
@@ -0,0 +1,1207 @@
+defmodule BlockScoutWeb.API.V2.AddressControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.{Chain, Repo}
+
+ alias Explorer.Chain.{
+ Address,
+ Address.CoinBalance,
+ Block,
+ InternalTransaction,
+ Log,
+ Token,
+ TokenTransfer,
+ Transaction
+ }
+
+ alias Explorer.Chain.Address.CurrentTokenBalance
+
+ describe "/addresses/{address_hash}" do
+ test "get 404 on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}")
+
+ assert %{"message" => "Not found"} = json_response(request, 404)
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get address & get the same response for checksummed and downcased parameter", %{conn: conn} do
+ address = insert(:address)
+
+ correct_reponse = %{
+ "hash" => Address.checksum(address.hash),
+ "implementation_name" => nil,
+ "is_contract" => false,
+ "is_verified" => false,
+ "name" => nil,
+ "private_tags" => [],
+ "public_tags" => [],
+ "watchlist_names" => [],
+ "creator_address_hash" => nil,
+ "creation_tx_hash" => nil,
+ "token" => nil,
+ "coin_balance" => nil,
+ "exchange_rate" => nil,
+ "implementation_name" => nil,
+ "implementation_address" => nil,
+ "block_number_balance_updated_at" => nil
+ }
+
+ request = get(conn, "/api/v2/addresses/#{Address.checksum(address.hash)}")
+ assert ^correct_reponse = json_response(request, 200)
+
+ request = get(conn, "/api/v2/addresses/#{String.downcase(to_string(address.hash))}")
+ assert ^correct_reponse = json_response(request, 200)
+ end
+ end
+
+ describe "/addresses/{address_hash}/counters" do
+ test "get 404 on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/counters")
+
+ assert %{"message" => "Not found"} = json_response(request, 404)
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/counters")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get counters with 0s", %{conn: conn} do
+ address = insert(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/counters")
+
+ assert %{
+ "transactions_count" => "0",
+ "token_transfers_count" => "0",
+ "gas_usage_count" => "0",
+ "validations_count" => "0"
+ } = json_response(request, 200)
+ end
+
+ test "get counters", %{conn: conn} do
+ address = insert(:address)
+
+ tx_from = insert(:transaction, from_address: address) |> with_block()
+ insert(:transaction, to_address: address) |> with_block()
+ another_tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer,
+ from_address: address,
+ transaction: another_tx,
+ block: another_tx.block,
+ block_number: another_tx.block_number
+ )
+
+ insert(:token_transfer,
+ to_address: address,
+ transaction: another_tx,
+ block: another_tx.block,
+ block_number: another_tx.block_number
+ )
+
+ insert(:block, miner: address)
+
+ Chain.transaction_count(address)
+ Chain.token_transfers_count(address)
+ Chain.gas_usage_count(address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/counters")
+
+ gas_used = to_string(tx_from.gas_used)
+
+ assert %{
+ "transactions_count" => "2",
+ "token_transfers_count" => "2",
+ "gas_usage_count" => ^gas_used,
+ "validations_count" => "1"
+ } = json_response(request, 200)
+ end
+ end
+
+ describe "/addresses/{address_hash}/transactions" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/transactions")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get relevant transaction", %{conn: conn} do
+ address = insert(:address)
+
+ tx = insert(:transaction, from_address: address) |> with_block()
+
+ insert(:transaction) |> with_block()
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(tx, Enum.at(response["items"], 0))
+ end
+
+ test "get pending transaction", %{conn: conn} do
+ address = insert(:address)
+
+ tx = insert(:transaction, from_address: address) |> with_block()
+ pending_tx = insert(:transaction, from_address: address)
+
+ insert(:transaction) |> with_block()
+ insert(:transaction)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 2
+ assert response["next_page_params"] == nil
+ compare_item(pending_tx, Enum.at(response["items"], 0))
+ compare_item(tx, Enum.at(response["items"], 1))
+ end
+
+ test "get only :to transaction", %{conn: conn} do
+ address = insert(:address)
+
+ insert(:transaction, from_address: address) |> with_block()
+ tx = insert(:transaction, to_address: address) |> with_block()
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "to"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(tx, Enum.at(response["items"], 0))
+ end
+
+ test "get only :from transactions", %{conn: conn} do
+ address = insert(:address)
+
+ tx = insert(:transaction, from_address: address) |> with_block()
+ insert(:transaction, to_address: address) |> with_block()
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", %{"filter" => "from"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(tx, Enum.at(response["items"], 0))
+ end
+
+ test "validated txs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ txs = insert_list(51, :transaction, from_address: address) |> with_block()
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, txs)
+ end
+
+ test "pending txs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ txs = insert_list(51, :transaction, from_address: address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, txs)
+ end
+
+ test "pending + validated txs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ txs_pending = insert_list(51, :transaction, from_address: address)
+ txs_validated = insert_list(50, :transaction, to_address: address) |> with_block()
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ assert Enum.count(response["items"]) == 50
+ assert response["next_page_params"] != nil
+ compare_item(Enum.at(txs_pending, 50), Enum.at(response["items"], 0))
+ compare_item(Enum.at(txs_pending, 1), Enum.at(response["items"], 49))
+
+ assert Enum.count(response_2nd_page["items"]) == 50
+ assert response_2nd_page["next_page_params"] != nil
+ compare_item(Enum.at(txs_pending, 0), Enum.at(response_2nd_page["items"], 0))
+ compare_item(Enum.at(txs_validated, 49), Enum.at(response_2nd_page["items"], 1))
+ compare_item(Enum.at(txs_validated, 1), Enum.at(response_2nd_page["items"], 49))
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"])
+ assert response = json_response(request, 200)
+
+ check_paginated_response(response_2nd_page, response, txs_validated ++ [Enum.at(txs_pending, 0)])
+ end
+
+ test ":to txs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ txs = insert_list(51, :transaction, to_address: address) |> with_block()
+ insert_list(51, :transaction, from_address: address) |> with_block()
+
+ filter = %{"filter" => "to"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/transactions", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, txs)
+ end
+
+ test ":from txs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ insert_list(51, :transaction, to_address: address) |> with_block()
+ txs = insert_list(51, :transaction, from_address: address) |> with_block()
+
+ filter = %{"filter" => "from"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/transactions", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, txs)
+ end
+
+ test ":from + :to txs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ txs_from = insert_list(50, :transaction, from_address: address) |> with_block()
+ txs_to = insert_list(51, :transaction, to_address: address) |> with_block()
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ assert Enum.count(response["items"]) == 50
+ assert response["next_page_params"] != nil
+ compare_item(Enum.at(txs_to, 50), Enum.at(response["items"], 0))
+ compare_item(Enum.at(txs_to, 1), Enum.at(response["items"], 49))
+
+ assert Enum.count(response_2nd_page["items"]) == 50
+ assert response_2nd_page["next_page_params"] != nil
+ compare_item(Enum.at(txs_to, 0), Enum.at(response_2nd_page["items"], 0))
+ compare_item(Enum.at(txs_from, 49), Enum.at(response_2nd_page["items"], 1))
+ compare_item(Enum.at(txs_from, 1), Enum.at(response_2nd_page["items"], 49))
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/transactions", response_2nd_page["next_page_params"])
+ assert response = json_response(request, 200)
+
+ check_paginated_response(response_2nd_page, response, txs_from ++ [Enum.at(txs_to, 0)])
+ end
+ end
+
+ describe "/addresses/{address_hash}/token-transfers" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/token-transfers")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get relevant token transfer", %{conn: conn} do
+ address = insert(:address)
+
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
+
+ token_transfer =
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(token_transfer, Enum.at(response["items"], 0))
+ end
+
+ test "get only :to token transfer", %{conn: conn} do
+ address = insert(:address)
+
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
+
+ token_transfer =
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "to"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(token_transfer, Enum.at(response["items"], 0))
+ end
+
+ test "get only :from token transfer", %{conn: conn} do
+ address = insert(:address)
+
+ tx = insert(:transaction) |> with_block()
+
+ token_transfer =
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
+
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", %{"filter" => "from"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(token_transfer, Enum.at(response["items"], 0))
+ end
+
+ test "token transfers can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ token_tranfers =
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
+ end
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, token_tranfers)
+ end
+
+ test ":to token transfers can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
+ end
+
+ token_tranfers =
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
+ end
+
+ filter = %{"filter" => "to"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, token_tranfers)
+ end
+
+ test ":from token transfers can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ token_tranfers =
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
+ end
+
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
+ end
+
+ filter = %{"filter" => "from"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, token_tranfers)
+ end
+
+ test ":from + :to tt can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ tt_from =
+ for _ <- 0..49 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, from_address: address)
+ end
+
+ tt_to =
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+ insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number, to_address: address)
+ end
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ assert Enum.count(response["items"]) == 50
+ assert response["next_page_params"] != nil
+ compare_item(Enum.at(tt_to, 50), Enum.at(response["items"], 0))
+ compare_item(Enum.at(tt_to, 1), Enum.at(response["items"], 49))
+
+ assert Enum.count(response_2nd_page["items"]) == 50
+ assert response_2nd_page["next_page_params"] != nil
+ compare_item(Enum.at(tt_to, 0), Enum.at(response_2nd_page["items"], 0))
+ compare_item(Enum.at(tt_from, 49), Enum.at(response_2nd_page["items"], 1))
+ compare_item(Enum.at(tt_from, 1), Enum.at(response_2nd_page["items"], 49))
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", response_2nd_page["next_page_params"])
+ assert response = json_response(request, 200)
+
+ check_paginated_response(response_2nd_page, response, tt_from ++ [Enum.at(tt_to, 0)])
+ end
+
+ test "check token type filters", %{conn: conn} do
+ address = insert(:address)
+
+ erc_20_token = insert(:token, type: "ERC-20")
+
+ erc_20_tt =
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ from_address: address,
+ token_contract_address: erc_20_token.contract_address
+ )
+ end
+
+ erc_721_token = insert(:token, type: "ERC-721")
+
+ erc_721_tt =
+ for x <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ from_address: address,
+ token_contract_address: erc_721_token.contract_address,
+ token_ids: [x]
+ )
+ end
+
+ erc_1155_token = insert(:token, type: "ERC-1155")
+
+ erc_1155_tt =
+ for x <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ from_address: address,
+ token_contract_address: erc_1155_token.contract_address,
+ token_ids: [x]
+ )
+ end
+
+ # -- ERC-20 --
+ filter = %{"type" => "ERC-20"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_20_tt)
+ # -- ------ --
+
+ # -- ERC-721 --
+ filter = %{"type" => "ERC-721"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_721_tt)
+ # -- ------ --
+
+ # -- ERC-1155 --
+ filter = %{"type" => "ERC-1155"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_1155_tt)
+ # -- ------ --
+
+ # two filters simultaneously
+ filter = %{"type" => "ERC-1155,ERC-20"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ assert Enum.count(response["items"]) == 50
+ assert response["next_page_params"] != nil
+ compare_item(Enum.at(erc_1155_tt, 50), Enum.at(response["items"], 0))
+ compare_item(Enum.at(erc_1155_tt, 1), Enum.at(response["items"], 49))
+
+ assert Enum.count(response_2nd_page["items"]) == 50
+ assert response_2nd_page["next_page_params"] != nil
+ compare_item(Enum.at(erc_1155_tt, 0), Enum.at(response_2nd_page["items"], 0))
+ compare_item(Enum.at(erc_20_tt, 50), Enum.at(response_2nd_page["items"], 1))
+ compare_item(Enum.at(erc_20_tt, 2), Enum.at(response_2nd_page["items"], 49))
+
+ request_3rd_page =
+ get(
+ conn,
+ "/api/v2/addresses/#{address.hash}/token-transfers",
+ Map.merge(response_2nd_page["next_page_params"], filter)
+ )
+
+ assert response_3rd_page = json_response(request_3rd_page, 200)
+ assert Enum.count(response_3rd_page["items"]) == 2
+ assert response_3rd_page["next_page_params"] == nil
+ compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0))
+ compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1))
+ # -- ------ --
+ end
+
+ test "type and direction filters at the same time", %{conn: conn} do
+ address = insert(:address)
+
+ erc_20_token = insert(:token, type: "ERC-20")
+
+ erc_20_tt =
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ from_address: address,
+ token_contract_address: erc_20_token.contract_address
+ )
+ end
+
+ erc_721_token = insert(:token, type: "ERC-721")
+
+ erc_721_tt =
+ for x <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ to_address: address,
+ token_contract_address: erc_721_token.contract_address,
+ token_ids: [x]
+ )
+ end
+
+ filter = %{"type" => "ERC-721", "filter" => "from"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+
+ filter = %{"type" => "ERC-721", "filter" => "to"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_721_tt)
+
+ filter = %{"type" => "ERC-721,ERC-20", "filter" => "to"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_721_tt)
+
+ filter = %{"type" => "ERC-721,ERC-20", "filter" => "from"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/token-transfers", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_20_tt)
+ end
+ end
+
+ describe "/addresses/{address_hash}/internal-transactions" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/internal-transactions")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get internal tx and filter working", %{conn: conn} do
+ address = insert(:address)
+
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ internal_tx_from =
+ insert(:internal_transaction,
+ transaction: tx,
+ index: 1,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: 1,
+ from_address: address
+ )
+
+ internal_tx_to =
+ insert(:internal_transaction,
+ transaction: tx,
+ index: 2,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: 2,
+ to_address: address
+ )
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 2
+ assert response["next_page_params"] == nil
+
+ compare_item(internal_tx_from, Enum.at(response["items"], 1))
+ compare_item(internal_tx_to, Enum.at(response["items"], 0))
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "from"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(internal_tx_from, Enum.at(response["items"], 0))
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", %{"filter" => "to"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(internal_tx_to, Enum.at(response["items"], 0))
+ end
+
+ test "internal txs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ itxs_from =
+ for i <- 1..51 do
+ insert(:internal_transaction,
+ transaction: tx,
+ index: i,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: i,
+ from_address: address
+ )
+ end
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", response["next_page_params"])
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, itxs_from)
+
+ itxs_to =
+ for i <- 52..102 do
+ insert(:internal_transaction,
+ transaction: tx,
+ index: i,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: i,
+ to_address: address
+ )
+ end
+
+ filter = %{"filter" => "to"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(
+ conn,
+ "/api/v2/addresses/#{address.hash}/internal-transactions",
+ Map.merge(response["next_page_params"], filter)
+ )
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, itxs_to)
+
+ filter = %{"filter" => "from"}
+ request = get(conn, "/api/v2/addresses/#{address.hash}/internal-transactions", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(
+ conn,
+ "/api/v2/addresses/#{address.hash}/internal-transactions",
+ Map.merge(response["next_page_params"], filter)
+ )
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, itxs_from)
+ end
+ end
+
+ describe "/addresses/{address_hash}/blocks-validated" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/blocks-validated")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get relevant block validated", %{conn: conn} do
+ address = insert(:address)
+ insert(:block)
+ block = insert(:block, miner: address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ compare_item(block, Enum.at(response["items"], 0))
+ end
+
+ test "blocks validated can be paginated", %{conn: conn} do
+ address = insert(:address)
+ insert(:block)
+ blocks = insert_list(51, :block, miner: address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/blocks-validated", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, blocks)
+ end
+ end
+
+ describe "/addresses/{address_hash}/token-balances" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances")
+
+ assert response = json_response(request, 200)
+ assert response == []
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/token-balances")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get token balance", %{conn: conn} do
+ address = insert(:address)
+
+ ctbs =
+ for _ <- 0..50 do
+ insert(:address_current_token_balance_with_token_id, address: address) |> Repo.preload([:token])
+ end
+ |> Enum.sort_by(fn x -> x.value end, :desc)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/token-balances")
+
+ assert response = json_response(request, 200)
+
+ for i <- 0..50 do
+ compare_item(Enum.at(ctbs, i), Enum.at(response, i))
+ end
+ end
+ end
+
+ describe "/addresses/{address_hash}/coin-balance-history" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/coin-balance-history")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get coin balance history", %{conn: conn} do
+ address = insert(:address)
+
+ insert(:address_coin_balance)
+ acb = insert(:address_coin_balance, address: address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ compare_item(acb, Enum.at(response["items"], 0))
+ end
+
+ test "coin balance history can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ acbs = insert_list(51, :address_coin_balance, address: address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history")
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history", response["next_page_params"])
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, acbs)
+ end
+ end
+
+ describe "/addresses/{address_hash}/coin-balance-history-by-day" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day")
+
+ assert response = json_response(request, 200)
+ assert response == []
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/coin-balance-history-by-day")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get coin balance history by day", %{conn: conn} do
+ address = insert(:address)
+ noon = Timex.now() |> Timex.beginning_of_day() |> Timex.set(hour: 12)
+ block = insert(:block, timestamp: noon, number: 2)
+ block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1), number: 1)
+ insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block.number)
+ insert(:fetched_balance, address_hash: address.hash, value: 2000, block_number: block_one_day_ago.number)
+ insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: noon)
+ insert(:fetched_balance_daily, address_hash: address.hash, value: 2000, day: Timex.shift(noon, days: -1))
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/coin-balance-history-by-day")
+
+ response = json_response(request, 200)
+
+ assert [
+ %{"date" => _, "value" => "2000"},
+ %{"date" => _, "value" => "1000"},
+ %{"date" => _, "value" => "1000"}
+ ] = response
+ end
+ end
+
+ describe "/addresses/{address_hash}/logs" do
+ test "get empty list on non existing address", %{conn: conn} do
+ address = build(:address)
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/logs")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/addresses/0x/logs")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get log", %{conn: conn} do
+ address = insert(:address)
+
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ log =
+ insert(:log,
+ transaction: tx,
+ index: 1,
+ block: tx.block,
+ block_number: tx.block_number,
+ address: address
+ )
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/logs")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(log, Enum.at(response["items"], 0))
+ end
+
+ # for some reasons test does not work if run as single test
+ test "logs can paginate", %{conn: conn} do
+ address = insert(:address)
+
+ logs =
+ for x <- 0..50 do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ insert(:log,
+ transaction: tx,
+ index: x,
+ block: tx.block,
+ block_number: tx.block_number,
+ address: address
+ )
+ end
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/logs")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/addresses/#{address.hash}/logs", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+ check_paginated_response(response, response_2nd_page, logs)
+ end
+
+ test "logs can be filtered by topic", %{conn: conn} do
+ address = insert(:address)
+
+ for x <- 0..20 do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ insert(:log,
+ transaction: tx,
+ index: x,
+ block: tx.block,
+ block_number: tx.block_number,
+ address: address
+ )
+ end
+
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ log =
+ insert(:log,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ address: address,
+ first_topic: "0x123456789123456789"
+ )
+
+ request = get(conn, "/api/v2/addresses/#{address.hash}/logs?topic=0x123456789123456789")
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(log, Enum.at(response["items"], 0))
+ end
+ end
+
+ defp compare_item(%Transaction{} = transaction, json) do
+ assert to_string(transaction.hash) == json["hash"]
+ assert transaction.block_number == json["block"]
+ assert to_string(transaction.value.value) == json["value"]
+ assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"]
+ end
+
+ defp compare_item(%TokenTransfer{} = token_transfer, json) do
+ assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
+ assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
+ end
+
+ defp compare_item(%InternalTransaction{} = internal_tx, json) do
+ assert internal_tx.block_number == json["block"]
+ assert to_string(internal_tx.gas) == json["gas_limit"]
+ assert internal_tx.index == json["index"]
+ assert to_string(internal_tx.transaction_hash) == json["transaction_hash"]
+ assert Address.checksum(internal_tx.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(internal_tx.to_address_hash) == json["to"]["hash"]
+ end
+
+ defp compare_item(%Block{} = block, json) do
+ assert to_string(block.hash) == json["hash"]
+ assert block.number == json["height"]
+ end
+
+ defp compare_item(%CurrentTokenBalance{} = ctb, json) do
+ assert to_string(ctb.value) == json["value"]
+ assert (ctb.token_id && to_string(ctb.token_id)) == json["token_id"]
+ compare_item(ctb.token, json["token"])
+ end
+
+ defp compare_item(%CoinBalance{} = cb, json) do
+ assert to_string(cb.value.value) == json["value"]
+ assert cb.block_number == json["block_number"]
+
+ assert Jason.encode!(Repo.get_by(Block, number: cb.block_number).timestamp) =~
+ String.replace(json["block_timestamp"], "Z", "")
+ end
+
+ defp compare_item(%Token{} = token, json) do
+ assert Address.checksum(token.contract_address_hash) == json["address"]
+ assert to_string(token.symbol) == json["symbol"]
+ assert to_string(token.name) == json["name"]
+ assert to_string(token.type) == json["type"]
+ assert to_string(token.decimals) == json["decimals"]
+ assert (token.holder_count && to_string(token.holder_count)) == json["holders"]
+ assert Map.has_key?(json, "exchange_rate")
+ end
+
+ defp compare_item(%Log{} = log, json) do
+ assert to_string(log.data) == json["data"]
+ assert log.index == json["index"]
+ assert Address.checksum(log.address_hash) == json["address"]["hash"]
+ end
+
+ defp check_paginated_response(first_page_resp, second_page_resp, list) do
+ assert Enum.count(first_page_resp["items"]) == 50
+ assert first_page_resp["next_page_params"] != nil
+ compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0))
+ compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49))
+
+ assert Enum.count(second_page_resp["items"]) == 1
+ assert second_page_resp["next_page_params"] == nil
+ compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0))
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
new file mode 100644
index 000000000000..d16e457441bf
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/block_controller_test.exs
@@ -0,0 +1,329 @@
+defmodule BlockScoutWeb.API.V2.BlockControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.Chain.{Address, Block, Transaction}
+
+ setup do
+ Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
+ Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
+ Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
+ Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Uncles.child_id())
+
+ :ok
+ end
+
+ describe "/blocks" do
+ test "empty lists", %{conn: conn} do
+ request = get(conn, "/api/v2/blocks")
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+
+ request = get(conn, "/api/v2/blocks", %{"type" => "uncle"})
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+
+ request = get(conn, "/api/v2/blocks", %{"type" => "reorg"})
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+
+ request = get(conn, "/api/v2/blocks", %{"type" => "block"})
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get block", %{conn: conn} do
+ block = insert(:block)
+
+ request = get(conn, "/api/v2/blocks")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(block, Enum.at(response["items"], 0))
+ end
+
+ test "type=block returns only consensus blocks", %{conn: conn} do
+ blocks =
+ 4
+ |> insert_list(:block)
+ |> Enum.reverse()
+
+ for index <- 0..3 do
+ uncle = insert(:block, consensus: false)
+ insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
+ end
+
+ 2
+ |> insert_list(:block, consensus: false)
+
+ request = get(conn, "/api/v2/blocks", %{"type" => "block"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 4
+ assert response["next_page_params"] == nil
+
+ for index <- 0..3 do
+ compare_item(Enum.at(blocks, index), Enum.at(response["items"], index))
+ end
+ end
+
+ test "type=block can paginate", %{conn: conn} do
+ blocks =
+ 51
+ |> insert_list(:block)
+
+ filter = %{"type" => "block"}
+
+ request = get(conn, "/api/v2/blocks", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, blocks)
+ end
+
+ test "type=reorg returns only non consensus blocks", %{conn: conn} do
+ blocks =
+ 5
+ |> insert_list(:block)
+
+ for index <- 0..3 do
+ uncle = insert(:block, consensus: false)
+ insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
+ end
+
+ reorgs =
+ 4
+ |> insert_list(:block, consensus: false)
+ |> Enum.reverse()
+
+ request = get(conn, "/api/v2/blocks", %{"type" => "reorg"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 4
+ assert response["next_page_params"] == nil
+
+ for index <- 0..3 do
+ compare_item(Enum.at(reorgs, index), Enum.at(response["items"], index))
+ end
+ end
+
+ test "type=reorg can paginate", %{conn: conn} do
+ reorgs =
+ 51
+ |> insert_list(:block, consensus: false)
+
+ filter = %{"type" => "reorg"}
+ request = get(conn, "/api/v2/blocks", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, reorgs)
+ end
+
+ test "type=uncle returns only uncle blocks", %{conn: conn} do
+ blocks =
+ 4
+ |> insert_list(:block)
+ |> Enum.reverse()
+
+ uncles =
+ for index <- 0..3 do
+ uncle = insert(:block, consensus: false)
+ insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
+ uncle
+ end
+ |> Enum.reverse()
+
+ 4
+ |> insert_list(:block, consensus: false)
+
+ request = get(conn, "/api/v2/blocks", %{"type" => "uncle"})
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 4
+ assert response["next_page_params"] == nil
+
+ for index <- 0..3 do
+ compare_item(Enum.at(uncles, index), Enum.at(response["items"], index))
+ end
+ end
+
+ test "type=uncle can paginate", %{conn: conn} do
+ blocks =
+ 51
+ |> insert_list(:block)
+
+ uncles =
+ for index <- 0..50 do
+ uncle = insert(:block, consensus: false)
+ insert(:block_second_degree_relation, uncle_hash: uncle.hash, nephew: Enum.at(blocks, index))
+ uncle
+ end
+
+ filter = %{"type" => "uncle"}
+ request = get(conn, "/api/v2/blocks", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/blocks", Map.merge(response["next_page_params"], filter))
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, uncles)
+ end
+ end
+
+ describe "/blocks/{block_hash_or_number}" do
+ test "return 422 on invalid parameter", %{conn: conn} do
+ request_1 = get(conn, "/api/v2/blocks/0x123123")
+ assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
+
+ request_2 = get(conn, "/api/v2/blocks/123qwe")
+ assert %{"message" => "Invalid number"} = json_response(request_2, 422)
+ end
+
+ test "return 404 on non existing block", %{conn: conn} do
+ block = build(:block)
+
+ request_1 = get(conn, "/api/v2/blocks/#{block.number}")
+ assert %{"message" => "Not found"} = json_response(request_1, 404)
+
+ request_2 = get(conn, "/api/v2/blocks/#{block.hash}")
+ assert %{"message" => "Not found"} = json_response(request_2, 404)
+ end
+
+ test "get the same blocks by hash and number", %{conn: conn} do
+ block = insert(:block)
+
+ request_1 = get(conn, "/api/v2/blocks/#{block.number}")
+ assert response_1 = json_response(request_1, 200)
+
+ request_2 = get(conn, "/api/v2/blocks/#{block.hash}")
+ assert response_2 = json_response(request_2, 200)
+
+ assert response_2 == response_1
+ compare_item(block, response_2)
+ end
+ end
+
+ describe "/blocks/{block_hash_or_number}/transactions" do
+ test "return 422 on invalid parameter", %{conn: conn} do
+ request_1 = get(conn, "/api/v2/blocks/0x123123/transactions")
+ assert %{"message" => "Invalid hash"} = json_response(request_1, 422)
+
+ request_2 = get(conn, "/api/v2/blocks/123qwe/transactions")
+ assert %{"message" => "Invalid number"} = json_response(request_2, 422)
+ end
+
+ test "return 404 on non existing block", %{conn: conn} do
+ block = build(:block)
+
+ request_1 = get(conn, "/api/v2/blocks/#{block.number}/transactions")
+ assert %{"message" => "Not found"} = json_response(request_1, 404)
+
+ request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
+ assert %{"message" => "Not found"} = json_response(request_2, 404)
+ end
+
+ test "get empty list", %{conn: conn} do
+ block = insert(:block)
+
+ request = get(conn, "/api/v2/blocks/#{block.number}/transactions")
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+
+ request = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "get relevant tx", %{conn: conn} do
+ 10
+ |> insert_list(:transaction)
+ |> with_block()
+
+ block = insert(:block)
+
+ tx =
+ :transaction
+ |> insert()
+ |> with_block(block)
+
+ request = get(conn, "/api/v2/blocks/#{block.number}/transactions")
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(tx, Enum.at(response["items"], 0))
+
+ request = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
+ assert response_1 = json_response(request, 200)
+ assert response_1 == response
+ end
+
+ test "get txs with working next_page_params", %{conn: conn} do
+ 2
+ |> insert_list(:transaction)
+ |> with_block()
+
+ block = insert(:block)
+
+ txs =
+ 51
+ |> insert_list(:transaction)
+ |> with_block(block)
+ |> Enum.reverse()
+
+ request = get(conn, "/api/v2/blocks/#{block.number}/transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/blocks/#{block.number}/transactions", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, txs)
+
+ request_1 = get(conn, "/api/v2/blocks/#{block.hash}/transactions")
+ assert response_1 = json_response(request_1, 200)
+
+ assert response_1 == response
+
+ request_2 = get(conn, "/api/v2/blocks/#{block.hash}/transactions", response_1["next_page_params"])
+ assert response_2 = json_response(request_2, 200)
+ assert response_2 == response_2nd_page
+ end
+ end
+
+ defp compare_item(%Block{} = block, json) do
+ assert to_string(block.hash) == json["hash"]
+ assert block.number == json["height"]
+ end
+
+ defp compare_item(%Transaction{} = transaction, json) do
+ assert to_string(transaction.hash) == json["hash"]
+ assert transaction.block_number == json["block"]
+ assert to_string(transaction.value.value) == json["value"]
+ assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"]
+ end
+
+ defp check_paginated_response(first_page_resp, second_page_resp, list) do
+ assert Enum.count(first_page_resp["items"]) == 50
+ assert first_page_resp["next_page_params"] != nil
+ compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0))
+ compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49))
+
+ assert Enum.count(second_page_resp["items"]) == 1
+ assert second_page_resp["next_page_params"] == nil
+ compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0))
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs
new file mode 100644
index 000000000000..0c00722d58bb
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/config_controller_test.exs
@@ -0,0 +1,22 @@
+defmodule BlockScoutWeb.API.V2.ConfigControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ describe "/config/json-rpc-url" do
+ test "get json rps url if set", %{conn: conn} do
+ url = "http://rps.url:1234/v1"
+ Application.put_env(:block_scout_web, :json_rpc, url)
+
+ request = get(conn, "/api/v2/config/json-rpc-url")
+
+ assert %{"json_rpc_url" => ^url} = json_response(request, 200)
+ end
+
+ test "get empty json rps url if not set", %{conn: conn} do
+ Application.put_env(:block_scout_web, :json_rpc, nil)
+
+ request = get(conn, "/api/v2/config/json-rpc-url")
+
+ assert %{"json_rpc_url" => nil} = json_response(request, 200)
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs
new file mode 100644
index 000000000000..b1e27673df8c
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/main_page_controller_test.exs
@@ -0,0 +1,77 @@
+defmodule BlockScoutWeb.API.V2.MainPageControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.Chain.{Address, Block, Transaction}
+
+ setup do
+ Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
+ Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
+ Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
+ Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
+
+ :ok
+ end
+
+ describe "/main-page/blocks" do
+ test "get empty list when no blocks", %{conn: conn} do
+ request = get(conn, "/api/v2/main-page/blocks")
+ assert [] = json_response(request, 200)
+ end
+
+ test "get last 4 blocks", %{conn: conn} do
+ blocks = insert_list(10, :block) |> Enum.take(-4) |> Enum.reverse()
+
+ request = get(conn, "/api/v2/main-page/blocks")
+ assert response = json_response(request, 200)
+ assert Enum.count(response) == 4
+
+ for i <- 0..3 do
+ compare_item(Enum.at(blocks, i), Enum.at(response, i))
+ end
+ end
+ end
+
+ describe "/main-page/transactions" do
+ test "get empty list when no txs", %{conn: conn} do
+ request = get(conn, "/api/v2/main-page/transactions")
+ assert [] = json_response(request, 200)
+ end
+
+ test "get last 5 txs", %{conn: conn} do
+ txs = insert_list(10, :transaction) |> with_block() |> Enum.take(-6) |> Enum.reverse()
+
+ request = get(conn, "/api/v2/main-page/transactions")
+ assert response = json_response(request, 200)
+ assert Enum.count(response) == 6
+
+ for i <- 0..5 do
+ compare_item(Enum.at(txs, i), Enum.at(response, i))
+ end
+ end
+ end
+
+ describe "/main-page/indexing-status" do
+ test "get indexing status", %{conn: conn} do
+ request = get(conn, "/api/v2/main-page/indexing-status")
+ assert request = json_response(request, 200)
+
+ assert Map.has_key?(request, "finished_indexing_blocks")
+ assert Map.has_key?(request, "finished_indexing")
+ assert Map.has_key?(request, "indexed_blocks_ratio")
+ assert Map.has_key?(request, "indexed_inernal_transactions_ratio")
+ end
+ end
+
+ defp compare_item(%Block{} = block, json) do
+ assert to_string(block.hash) == json["hash"]
+ assert block.number == json["height"]
+ end
+
+ defp compare_item(%Transaction{} = transaction, json) do
+ assert to_string(transaction.hash) == json["hash"]
+ assert transaction.block_number == json["block"]
+ assert to_string(transaction.value.value) == json["value"]
+ assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"]
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs
new file mode 100644
index 000000000000..211c68dc79f0
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/search_controller_test.exs
@@ -0,0 +1,147 @@
+defmodule BlockScoutWeb.API.V2.SearchControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.Chain.Address
+
+ setup do
+ insert(:block)
+ insert(:unique_smart_contract)
+ insert(:unique_token)
+ insert(:transaction)
+ address = insert(:address)
+ insert(:unique_address_name, address: address)
+
+ :ok
+ end
+
+ describe "/search" do
+ test "search block", %{conn: conn} do
+ block = insert(:block)
+
+ request = get(conn, "/api/v2/search?q=#{block.hash}")
+ assert response = json_response(request, 200)
+
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ item = Enum.at(response["items"], 0)
+
+ assert item["type"] == "block"
+ assert item["block_number"] == block.number
+ assert item["block_hash"] == to_string(block.hash)
+ assert item["url"] =~ to_string(block.hash)
+
+ request = get(conn, "/api/v2/search?q=#{block.number}")
+ assert response = json_response(request, 200)
+
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ item = Enum.at(response["items"], 0)
+
+ assert item["type"] == "block"
+ assert item["block_number"] == block.number
+ assert item["block_hash"] == to_string(block.hash)
+ assert item["url"] =~ to_string(block.hash)
+ end
+
+ test "search address", %{conn: conn} do
+ address = insert(:address)
+ name = insert(:unique_address_name, address: address)
+
+ request = get(conn, "/api/v2/search?q=#{address.hash}")
+ assert response = json_response(request, 200)
+
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ item = Enum.at(response["items"], 0)
+
+ assert item["type"] == "address"
+ assert item["name"] == name.name
+ assert item["address"] == Address.checksum(address.hash)
+ assert item["url"] =~ Address.checksum(address.hash)
+ end
+
+ test "search contract", %{conn: conn} do
+ contract = insert(:unique_smart_contract)
+
+ request = get(conn, "/api/v2/search?q=#{contract.name}")
+ assert response = json_response(request, 200)
+
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ item = Enum.at(response["items"], 0)
+
+ assert item["type"] == "contract"
+ assert item["name"] == contract.name
+ assert item["address"] == Address.checksum(contract.address_hash)
+ assert item["url"] =~ Address.checksum(contract.address_hash)
+ end
+
+ test "check pagination", %{conn: conn} do
+ name = "contract"
+ contracts = insert_list(51, :smart_contract, name: name)
+
+ request = get(conn, "/api/v2/search?q=#{name}")
+ assert response = json_response(request, 200)
+
+ assert Enum.count(response["items"]) == 50
+ assert response["next_page_params"] != nil
+
+ item = Enum.at(response["items"], 0)
+
+ assert item["type"] == "contract"
+ assert item["name"] == name
+
+ request_2 = get(conn, "/api/v2/search", response["next_page_params"])
+ assert response_2 = json_response(request_2, 200)
+
+ assert Enum.count(response_2["items"]) == 1
+ assert response_2["next_page_params"] == nil
+
+ item = Enum.at(response_2["items"], 0)
+
+ assert item["type"] == "contract"
+ assert item["name"] == name
+
+ assert item not in response["items"]
+ end
+
+ test "search token", %{conn: conn} do
+ token = insert(:unique_token)
+
+ request = get(conn, "/api/v2/search?q=#{token.name}")
+ assert response = json_response(request, 200)
+
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ item = Enum.at(response["items"], 0)
+
+ assert item["type"] == "token"
+ assert item["name"] == token.name
+ assert item["symbol"] == token.symbol
+ assert item["address"] == Address.checksum(token.contract_address_hash)
+ assert item["token_url"] =~ Address.checksum(token.contract_address_hash)
+ assert item["address_url"] =~ Address.checksum(token.contract_address_hash)
+ end
+
+ test "search transaction", %{conn: conn} do
+ tx = insert(:transaction)
+
+ request = get(conn, "/api/v2/search?q=#{tx.hash}")
+ assert response = json_response(request, 200)
+
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+
+ item = Enum.at(response["items"], 0)
+
+ assert item["type"] == "transaction"
+ assert item["tx_hash"] == to_string(tx.hash)
+ assert item["url"] =~ to_string(tx.hash)
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs
new file mode 100644
index 000000000000..803d5ad51ca3
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/stats_controller_test.exs
@@ -0,0 +1,57 @@
+defmodule BlockScoutWeb.API.V2.StatsControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.Counters.{AddressesCounter, AverageBlockTime}
+
+ describe "/stats" do
+ setup do
+ start_supervised!(AddressesCounter)
+ start_supervised!(AverageBlockTime)
+
+ Application.put_env(:explorer, AverageBlockTime, enabled: true)
+
+ on_exit(fn ->
+ Application.put_env(:explorer, AverageBlockTime, enabled: false)
+ end)
+
+ :ok
+ end
+
+ test "get all fields", %{conn: conn} do
+ request = get(conn, "/api/v2/stats")
+ assert response = json_response(request, 200)
+
+ assert Map.has_key?(response, "total_blocks")
+ assert Map.has_key?(response, "total_addresses")
+ assert Map.has_key?(response, "total_transactions")
+ assert Map.has_key?(response, "average_block_time")
+ assert Map.has_key?(response, "coin_price")
+ assert Map.has_key?(response, "total_gas_used")
+ assert Map.has_key?(response, "transactions_today")
+ assert Map.has_key?(response, "gas_used_today")
+ assert Map.has_key?(response, "gas_prices")
+ assert Map.has_key?(response, "static_gas_price")
+ assert Map.has_key?(response, "market_cap")
+ assert Map.has_key?(response, "network_utilization_percentage")
+ end
+ end
+
+ describe "/stats/charts/market" do
+ test "get empty data", %{conn: conn} do
+ request = get(conn, "/api/v2/stats/charts/market")
+ assert response = json_response(request, 200)
+
+ assert response["chart_data"] == []
+ assert response["available_supply"] == 0
+ end
+ end
+
+ describe "/stats/charts/transactions" do
+ test "get empty data", %{conn: conn} do
+ request = get(conn, "/api/v2/stats/charts/transactions")
+ assert response = json_response(request, 200)
+
+ assert response["chart_data"] == []
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
new file mode 100644
index 000000000000..a0751c6bfee2
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
@@ -0,0 +1,228 @@
+defmodule BlockScoutWeb.API.V2.TokenControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.{Chain, Repo}
+
+ alias Explorer.Chain.{Address, Token, TokenTransfer}
+
+ alias Explorer.Chain.Address.CurrentTokenBalance
+
+ describe "/tokens/{address_hash}" do
+ test "get 404 on non existing address", %{conn: conn} do
+ token = build(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}")
+
+ assert %{"message" => "Not found"} = json_response(request, 404)
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/tokens/0x")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get token", %{conn: conn} do
+ token = insert(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}")
+
+ assert response = json_response(request, 200)
+
+ compare_item(token, response)
+ end
+ end
+
+ describe "/tokens/{address_hash}/counters" do
+ test "get 404 on non existing address", %{conn: conn} do
+ token = build(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters")
+
+ assert %{"message" => "Not found"} = json_response(request, 404)
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/tokens/0x/counters")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get counters", %{conn: conn} do
+ token = insert(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters")
+
+ assert response = json_response(request, 200)
+
+ assert response["transfers_count"] == "0"
+ assert response["token_holders_count"] == "0"
+ end
+
+ test "get not zero counters", %{conn: conn} do
+ contract_token_address = insert(:contract_address)
+ token = insert(:token, contract_address: contract_token_address)
+
+ transaction =
+ :transaction
+ |> insert(to_address: contract_token_address)
+ |> with_block()
+
+ insert_list(
+ 3,
+ :token_transfer,
+ transaction: transaction,
+ token_contract_address: contract_token_address
+ )
+
+ second_page_token_balances =
+ 1..5
+ |> Enum.map(
+ &insert(
+ :address_current_token_balance,
+ token_contract_address_hash: token.contract_address_hash,
+ value: &1 + 1000
+ )
+ )
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/counters")
+
+ assert response = json_response(request, 200)
+
+ assert response["transfers_count"] == "3"
+ assert response["token_holders_count"] == "5"
+ end
+ end
+
+ describe "/tokens/{address_hash}/transfers" do
+ test "get 200 on non existing address", %{conn: conn} do
+ token = build(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
+
+ assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/tokens/0x/transfers")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get empty list", %{conn: conn} do
+ token = insert(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
+
+ assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
+ end
+
+ test "check pagination", %{conn: conn} do
+ token = insert(:token)
+
+ token_tranfers =
+ for _ <- 0..50 do
+ tx = insert(:transaction) |> with_block()
+
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ token_contract_address: token.contract_address
+ )
+ end
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers")
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/tokens/#{token.contract_address.hash}/transfers", response["next_page_params"])
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, token_tranfers)
+ end
+ end
+
+ describe "/tokens/{address_hash}/holders" do
+ test "get 200 on non existing address", %{conn: conn} do
+ token = build(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders")
+
+ assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
+ end
+
+ test "get 422 on invalid address", %{conn: conn} do
+ request = get(conn, "/api/v2/tokens/0x/holders")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "get empty list", %{conn: conn} do
+ token = insert(:token)
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders")
+
+ assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
+ end
+
+ test "check pagination", %{conn: conn} do
+ token = insert(:token)
+
+ token_balances =
+ for i <- 0..50 do
+ insert(
+ :address_current_token_balance,
+ token_contract_address_hash: token.contract_address_hash,
+ value: i + 1000
+ )
+ end
+
+ request = get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders")
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/tokens/#{token.contract_address.hash}/holders", response["next_page_params"])
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, token_balances)
+ end
+ end
+
+ def compare_item(%Token{} = token, json) do
+ assert Address.checksum(token.contract_address.hash) == json["address"]
+ assert token.symbol == json["symbol"]
+ assert token.name == json["name"]
+ assert to_string(token.decimals) == json["decimals"]
+ assert token.type == json["type"]
+ assert token.holder_count == json["holders"]
+ assert to_string(token.total_supply) == json["total_supply"]
+ assert Map.has_key?(json, "exchange_rate")
+ end
+
+ def compare_item(%TokenTransfer{} = token_transfer, json) do
+ assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
+ assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
+ end
+
+ def compare_item(%CurrentTokenBalance{} = ctb, json) do
+ assert Address.checksum(ctb.address_hash) == json["address"]["hash"]
+ assert ctb.token_id == json["token_id"]
+ assert to_string(ctb.value) == json["value"]
+ compare_item(Repo.preload(ctb, [{:token, :contract_address}]).token, json["token"])
+ end
+
+ defp check_paginated_response(first_page_resp, second_page_resp, list) do
+ assert Enum.count(first_page_resp["items"]) == 50
+ assert first_page_resp["next_page_params"] != nil
+ compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0))
+ compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49))
+
+ assert Enum.count(second_page_resp["items"]) == 1
+ assert second_page_resp["next_page_params"] == nil
+ compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0))
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
new file mode 100644
index 000000000000..036b79dfc946
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/transaction_controller_test.exs
@@ -0,0 +1,582 @@
+defmodule BlockScoutWeb.API.V2.TransactionControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias Explorer.Chain.{Address, InternalTransaction, Log, TokenTransfer, Transaction}
+
+ setup do
+ Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
+ Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.TransactionsApiV2.child_id())
+
+ :ok
+ end
+
+ describe "/transactions" do
+ test "empty list", %{conn: conn} do
+ request = get(conn, "/api/v2/transactions")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "non empty list", %{conn: conn} do
+ 1
+ |> insert_list(:transaction)
+ |> with_block()
+
+ request = get(conn, "/api/v2/transactions")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ end
+
+ test "txs with next_page_params", %{conn: conn} do
+ txs =
+ 51
+ |> insert_list(:transaction)
+ |> with_block()
+
+ request = get(conn, "/api/v2/transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/transactions", response["next_page_params"])
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, txs)
+ end
+
+ test "filter=pending", %{conn: conn} do
+ pending_txs =
+ 51
+ |> insert_list(:transaction)
+
+ _mined_txs =
+ 51
+ |> insert_list(:transaction)
+ |> with_block()
+
+ filter = %{"filter" => "pending"}
+
+ request = get(conn, "/api/v2/transactions", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter))
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, pending_txs)
+ end
+
+ test "filter=validated", %{conn: conn} do
+ _pending_txs =
+ 51
+ |> insert_list(:transaction)
+
+ mined_txs =
+ 51
+ |> insert_list(:transaction)
+ |> with_block()
+
+ filter = %{"filter" => "validated"}
+
+ request = get(conn, "/api/v2/transactions", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/transactions", Map.merge(response["next_page_params"], filter))
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, mined_txs)
+ end
+ end
+
+ describe "/transactions/{tx_hash}" do
+ test "return 404 on non existing tx", %{conn: conn} do
+ tx = build(:transaction)
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}")
+
+ assert %{"message" => "Not found"} = json_response(request, 404)
+ end
+
+ test "return 422 on invalid tx hash", %{conn: conn} do
+ request = get(conn, "/api/v2/transactions/0x")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "return existing tx", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ request = get(conn, "/api/v2/transactions/" <> to_string(tx.hash))
+
+ assert response = json_response(request, 200)
+ compare_item(tx, response)
+ end
+ end
+
+ describe "/transactions/{tx_hash}/internal-transactions" do
+ test "return empty list on non existing tx", %{conn: conn} do
+ tx = build(:transaction)
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "return 422 on invalid tx hash", %{conn: conn} do
+ request = get(conn, "/api/v2/transactions/0x/internal-transactions")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "return empty list", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "return relevant internal transaction", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ insert(:internal_transaction,
+ transaction: tx,
+ index: 0,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: 0
+ )
+
+ internal_tx =
+ insert(:internal_transaction,
+ transaction: tx,
+ index: 1,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: 1
+ )
+
+ tx_1 =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ 0..5
+ |> Enum.map(fn index ->
+ insert(:internal_transaction,
+ transaction: tx_1,
+ index: index,
+ block_number: tx_1.block_number,
+ transaction_index: tx_1.index,
+ block_hash: tx_1.block_hash,
+ block_index: index
+ )
+ end)
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(internal_tx, Enum.at(response["items"], 0))
+ end
+
+ test "return list with next_page_params", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ insert(:internal_transaction,
+ transaction: tx,
+ index: 0,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: 0
+ )
+
+ internal_txs =
+ 51..1
+ |> Enum.map(fn index ->
+ insert(:internal_transaction,
+ transaction: tx,
+ index: index,
+ block_number: tx.block_number,
+ transaction_index: tx.index,
+ block_hash: tx.block_hash,
+ block_index: index
+ )
+ end)
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions")
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/internal-transactions", response["next_page_params"])
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, internal_txs)
+ end
+ end
+
+ describe "/transactions/{tx_hash}/logs" do
+ test "return empty list on non existing tx", %{conn: conn} do
+ tx = build(:transaction)
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "return 422 on invalid tx hash", %{conn: conn} do
+ request = get(conn, "/api/v2/transactions/0x/logs")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "return empty list", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "return relevant log", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ log =
+ insert(:log,
+ transaction: tx,
+ index: 1,
+ block: tx.block,
+ block_number: tx.block_number
+ )
+
+ tx_1 =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ 0..5
+ |> Enum.map(fn index ->
+ insert(:log,
+ transaction: tx_1,
+ index: index,
+ block: tx_1.block,
+ block_number: tx_1.block_number
+ )
+ end)
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(log, Enum.at(response["items"], 0))
+ end
+
+ test "return list with next_page_params", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ logs =
+ 50..0
+ |> Enum.map(fn index ->
+ insert(:log,
+ transaction: tx,
+ index: index,
+ block: tx.block,
+ block_number: tx.block_number
+ )
+ end)
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs")
+ assert response = json_response(request, 200)
+
+ request_2nd_page = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/logs", response["next_page_params"])
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, logs)
+ end
+ end
+
+ describe "/transactions/{tx_hash}/token-transfers" do
+ test "return empty list on non existing tx", %{conn: conn} do
+ tx = build(:transaction)
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "return 422 on invalid tx hash", %{conn: conn} do
+ request = get(conn, "/api/v2/transactions/0x/token-transfers")
+
+ assert %{"message" => "Invalid parameter(s)"} = json_response(request, 422)
+ end
+
+ test "return empty list", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
+
+ assert response = json_response(request, 200)
+ assert response["items"] == []
+ assert response["next_page_params"] == nil
+ end
+
+ test "return relevant token transfer", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ token_transfer = insert(:token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
+
+ tx_1 =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ insert_list(6, :token_transfer, transaction: tx_1, block: tx_1.block, block_number: tx_1.block_number)
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
+
+ assert response = json_response(request, 200)
+ assert Enum.count(response["items"]) == 1
+ assert response["next_page_params"] == nil
+ compare_item(token_transfer, Enum.at(response["items"], 0))
+ end
+
+ test "return list with next_page_params", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ token_transfers =
+ insert_list(51, :token_transfer, transaction: tx, block: tx.block, block_number: tx.block_number)
+ |> Enum.reverse()
+
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers")
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", response["next_page_params"])
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, token_transfers)
+ end
+
+ test "check filters", %{conn: conn} do
+ tx =
+ :transaction
+ |> insert()
+ |> with_block()
+
+ erc_1155_token = insert(:token, type: "ERC-1155")
+
+ erc_1155_tt =
+ for x <- 0..50 do
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ token_contract_address: erc_1155_token.contract_address,
+ token_ids: [x]
+ )
+ end
+ |> Enum.reverse()
+
+ erc_721_token = insert(:token, type: "ERC-721")
+
+ erc_721_tt =
+ for x <- 0..50 do
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ token_contract_address: erc_721_token.contract_address,
+ token_ids: [x]
+ )
+ end
+ |> Enum.reverse()
+
+ erc_20_token = insert(:token, type: "ERC-20")
+
+ erc_20_tt =
+ for _ <- 0..50 do
+ insert(:token_transfer,
+ transaction: tx,
+ block: tx.block,
+ block_number: tx.block_number,
+ token_contract_address: erc_20_token.contract_address
+ )
+ end
+ |> Enum.reverse()
+
+ # -- ERC-20 --
+ filter = %{"type" => "ERC-20"}
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(
+ conn,
+ "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
+ Map.merge(response["next_page_params"], filter)
+ )
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_20_tt)
+ # -- ------ --
+
+ # -- ERC-721 --
+ filter = %{"type" => "ERC-721"}
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(
+ conn,
+ "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
+ Map.merge(response["next_page_params"], filter)
+ )
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_721_tt)
+ # -- ------ --
+
+ # -- ERC-1155 --
+ filter = %{"type" => "ERC-1155"}
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(
+ conn,
+ "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
+ Map.merge(response["next_page_params"], filter)
+ )
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ check_paginated_response(response, response_2nd_page, erc_1155_tt)
+ # -- ------ --
+
+ # two filters simultaneously
+ filter = %{"type" => "ERC-1155,ERC-20"}
+ request = get(conn, "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers", filter)
+ assert response = json_response(request, 200)
+
+ request_2nd_page =
+ get(
+ conn,
+ "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
+ Map.merge(response["next_page_params"], filter)
+ )
+
+ assert response_2nd_page = json_response(request_2nd_page, 200)
+
+ assert Enum.count(response["items"]) == 50
+ assert response["next_page_params"] != nil
+ compare_item(Enum.at(erc_1155_tt, 50), Enum.at(response["items"], 0))
+ compare_item(Enum.at(erc_1155_tt, 1), Enum.at(response["items"], 49))
+
+ assert Enum.count(response_2nd_page["items"]) == 50
+ assert response_2nd_page["next_page_params"] != nil
+ compare_item(Enum.at(erc_1155_tt, 0), Enum.at(response_2nd_page["items"], 0))
+ compare_item(Enum.at(erc_20_tt, 50), Enum.at(response_2nd_page["items"], 1))
+ compare_item(Enum.at(erc_20_tt, 2), Enum.at(response_2nd_page["items"], 49))
+
+ request_3rd_page =
+ get(
+ conn,
+ "/api/v2/transactions/#{to_string(tx.hash)}/token-transfers",
+ Map.merge(response_2nd_page["next_page_params"], filter)
+ )
+
+ assert response_3rd_page = json_response(request_3rd_page, 200)
+ assert Enum.count(response_3rd_page["items"]) == 2
+ assert response_3rd_page["next_page_params"] == nil
+ compare_item(Enum.at(erc_20_tt, 1), Enum.at(response_3rd_page["items"], 0))
+ compare_item(Enum.at(erc_20_tt, 0), Enum.at(response_3rd_page["items"], 1))
+ end
+ end
+
+ defp compare_item(%Transaction{} = transaction, json) do
+ assert to_string(transaction.hash) == json["hash"]
+ assert transaction.block_number == json["block"]
+ assert to_string(transaction.value.value) == json["value"]
+ assert Address.checksum(transaction.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(transaction.to_address_hash) == json["to"]["hash"]
+ end
+
+ defp compare_item(%InternalTransaction{} = internal_tx, json) do
+ assert internal_tx.block_number == json["block"]
+ assert to_string(internal_tx.gas) == json["gas_limit"]
+ assert internal_tx.index == json["index"]
+ assert to_string(internal_tx.transaction_hash) == json["transaction_hash"]
+ assert Address.checksum(internal_tx.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(internal_tx.to_address_hash) == json["to"]["hash"]
+ end
+
+ defp compare_item(%Log{} = log, json) do
+ assert to_string(log.data) == json["data"]
+ assert log.index == json["index"]
+ assert Address.checksum(log.address_hash) == json["address"]["hash"]
+ end
+
+ defp compare_item(%TokenTransfer{} = token_transfer, json) do
+ assert Address.checksum(token_transfer.from_address_hash) == json["from"]["hash"]
+ assert Address.checksum(token_transfer.to_address_hash) == json["to"]["hash"]
+ assert to_string(token_transfer.transaction_hash) == json["tx_hash"]
+ end
+
+ defp check_paginated_response(first_page_resp, second_page_resp, txs) do
+ assert Enum.count(first_page_resp["items"]) == 50
+ assert first_page_resp["next_page_params"] != nil
+ compare_item(Enum.at(txs, 50), Enum.at(first_page_resp["items"], 0))
+ compare_item(Enum.at(txs, 1), Enum.at(first_page_resp["items"], 49))
+
+ assert Enum.count(second_page_resp["items"]) == 1
+ assert second_page_resp["next_page_params"] == nil
+ compare_item(Enum.at(txs, 0), Enum.at(second_page_resp["items"], 0))
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
index a4adb8f8d1e6..0090ceaa9ee1 100644
--- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_state_controller_test.exs
@@ -7,7 +7,6 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do
import BlockScoutWeb.WeiHelpers, only: [format_wei_value: 2]
import EthereumJSONRPC, only: [integer_to_quantity: 1]
alias Explorer.Chain.Wei
- alias EthereumJSONRPC.Blocks
describe "GET index/3" do
test "loads existing transaction", %{conn: conn} do
@@ -50,6 +49,35 @@ defmodule BlockScoutWeb.TransactionStateControllerTest do
assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 1)
end
+ test "returns state changes for the transaction with contract creation", %{conn: conn} do
+ block = insert(:block)
+
+ contract_address = insert(:contract_address)
+
+ transaction =
+ :transaction
+ |> insert(to_address: nil)
+ |> with_contract_creation(contract_address)
+ |> with_block(insert(:block))
+
+ insert(:fetched_balance,
+ address_hash: transaction.from_address_hash,
+ value: 1_000_000,
+ block_number: block.number
+ )
+
+ insert(:fetched_balance,
+ address_hash: transaction.block.miner_hash,
+ value: 1_000_000,
+ block_number: block.number
+ )
+
+ conn = get(conn, transaction_state_path(conn, :index, transaction), %{type: "JSON"})
+ {:ok, %{"items" => items}} = conn.resp_body |> Poison.decode()
+
+ assert(items |> Enum.filter(fn item -> item != nil end) |> length() == 2)
+ end
+
test "returns fetched state changes for the transaction with token transfer", %{conn: conn} do
block = insert(:block)
address_a = insert(:address)
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex
index eda00ba3f582..5f01093dae0f 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/http.ex
@@ -3,7 +3,7 @@ defmodule EthereumJSONRPC.HTTP do
JSONRPC over HTTP
"""
- alias EthereumJSONRPC.Transport
+ alias EthereumJSONRPC.{DecodeError, Transport}
require Logger
@@ -125,7 +125,17 @@ defmodule EthereumJSONRPC.HTTP do
{:error, {:bad_gateway, request_url}}
_ ->
- raise EthereumJSONRPC.DecodeError, named_arguments
+ named_arguments
+ |> DecodeError.exception()
+ |> DecodeError.message()
+ |> Logger.error()
+
+ request_url =
+ named_arguments
+ |> Keyword.fetch!(:request)
+ |> Keyword.fetch!(:url)
+
+ {:error, {:bad_response, request_url}}
end
end
end
diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex
index bd491f25fd7f..b63e0971a6d3 100644
--- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex
+++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/request_coordinator.ex
@@ -109,7 +109,7 @@ defmodule EthereumJSONRPC.RequestCoordinator do
defp trace_request(_, fun), do: fun.()
- defp handle_transport_response({:error, {:bad_gateway, _}} = error) do
+ defp handle_transport_response({:error, {error_type, _}} = error) when error_type in [:bad_gateway, :bad_response] do
RollingWindow.inc(table(), @error_key)
inc_throttle_table()
error
diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs
index 683ce9a48455..c2b74440aada 100644
--- a/apps/explorer/config/test.exs
+++ b/apps/explorer/config/test.exs
@@ -44,6 +44,20 @@ config :explorer, Explorer.Tracer, disabled?: false
config :explorer, Explorer.Chain.Cache.MinMissingBlockNumber, enabled: false
+# Configure API database
+config :explorer, Explorer.Repo.Replica1,
+ database: "explorer_test",
+ hostname: "localhost",
+ pool: Ecto.Adapters.SQL.Sandbox,
+ # Default of `5_000` was too low for `BlockFetcher` test
+ ownership_timeout: :timer.minutes(1),
+ timeout: :timer.seconds(60),
+ queue_target: 1000,
+ enable_caching_implementation_data_of_proxy: true,
+ avg_block_time_as_ttl_cached_implementation_data_of_proxy: false,
+ fallback_ttl_cached_implementation_data_of_proxy: :timer.seconds(20),
+ implementation_data_fetching_timeout: :timer.seconds(20)
+
# Configure API database
config :explorer, Explorer.Repo.Account,
database: "explorer_test_account",
diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex
index a01ba2b11e32..da426cf9fec7 100644
--- a/apps/explorer/lib/explorer/application.ex
+++ b/apps/explorer/lib/explorer/application.ex
@@ -20,6 +20,7 @@ defmodule Explorer.Application do
MinMissingBlockNumber,
NetVersion,
Transactions,
+ TransactionsApiV2,
Uncles
}
@@ -55,6 +56,7 @@ defmodule Explorer.Application do
con_cache_child_spec(MarketHistoryCache.cache_name()),
con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
Transactions,
+ TransactionsApiV2,
Accounts,
Uncles,
Supervisor.child_spec({Phoenix.PubSub, name: :chain_pubsub}, id: :chain_pubsub),
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index 8208ef0058fe..cca29b609dc0 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -16,11 +16,11 @@ defmodule Explorer.Chain do
order_by: 2,
order_by: 3,
preload: 2,
+ preload: 3,
select: 2,
select: 3,
subquery: 1,
union: 2,
- update: 2,
where: 2,
where: 3
]
@@ -33,10 +33,9 @@ defmodule Explorer.Chain do
alias Ecto.Adapters.SQL
alias Ecto.{Changeset, Multi, Query}
- alias EthereumJSONRPC.Contract
alias EthereumJSONRPC.Transaction, as: EthereumJSONRPCTransaction
- alias Explorer.Counters.LastFetchedCounter
+ alias Explorer.Counters.{LastFetchedCounter, TokenHoldersCounter, TokenTransfersCounter}
alias Explorer.Chain
@@ -96,12 +95,15 @@ defmodule Explorer.Chain do
alias Explorer.Counters.{
AddressesCounter,
- AddressesWithBalanceCounter
+ AddressesWithBalanceCounter,
+ AddressTokenTransfersCounter,
+ AddressTransactionsCounter,
+ AddressTransactionsGasUsageCounter
}
alias Explorer.Market.MarketHistoryCache
alias Explorer.{PagingOptions, Repo}
- alias Explorer.SmartContract.{Helper, Reader}
+ alias Explorer.SmartContract.Helper
alias Dataloader.Ecto, as: DataloaderEcto
@@ -448,7 +450,7 @@ defmodule Explorer.Chain do
options
|> Keyword.get(:paging_options, @default_paging_options)
- |> fetch_transactions(from_block, to_block)
+ |> fetch_transactions(from_block, to_block, true)
end
defp transactions_block_numbers_at_address(address_hash, options) do
@@ -564,6 +566,20 @@ defmodule Explorer.Chain do
|> Repo.all()
end
+ @spec address_hash_to_token_transfers_new(Hash.Address.t() | String.t(), Keyword.t()) :: [TokenTransfer.t()]
+ def address_hash_to_token_transfers_new(address_hash, options \\ []) do
+ paging_options = Keyword.get(options, :paging_options, @default_paging_options)
+ direction = Keyword.get(options, :direction)
+ filters = Keyword.get(options, :token_type)
+ necessity_by_association = Keyword.get(options, :necessity_by_association)
+
+ direction
+ |> TokenTransfer.token_transfers_by_address_hash(address_hash, filters)
+ |> join_associations(necessity_by_association)
+ |> TokenTransfer.handle_paging_options(paging_options)
+ |> Repo.all()
+ end
+
@doc """
address_hash_to_token_transfers_including_contract/2 function returns token transfers on address (to/from/contract).
It is used by CSV export of token transfers button.
@@ -2180,14 +2196,6 @@ defmodule Explorer.Chain do
def get_token_transfers_per_transaction_preview_count, do: @token_transfers_per_transaction_preview
- defp debug(value, key) do
- require Logger
- Logger.configure(truncate: :infinity)
- Logger.info(key)
- Logger.info(Kernel.inspect(value, limit: :infinity, printable_limit: :infinity))
- value
- end
-
@doc """
Converts list of `t:Explorer.Chain.Transaction.t/0` `hashes` to the list of `t:Explorer.Chain.Transaction.t/0`s for
those `hashes`.
@@ -3502,17 +3510,23 @@ defmodule Explorer.Chain do
the `block_number` and `index` that are passed.
"""
- @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option], [String.t()], [
- :atom
- ]) :: [
+ @spec recent_collated_transactions(true | false, [paging_options | necessity_by_association_option]) :: [
Transaction.t()
]
- def recent_collated_transactions(old_ui?, options \\ [], method_id_filter \\ [], type_filter \\ [])
+ def recent_collated_transactions(old_ui?, options \\ [])
when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
-
- fetch_recent_collated_transactions(old_ui?, paging_options, necessity_by_association, method_id_filter, type_filter)
+ method_id_filter = Keyword.get(options, :method)
+ type_filter = Keyword.get(options, :type)
+
+ fetch_recent_collated_transactions(
+ old_ui?,
+ paging_options,
+ necessity_by_association,
+ method_id_filter,
+ type_filter
+ )
end
# RAP - random access pagination
@@ -3584,7 +3598,6 @@ defmodule Explorer.Chain do
|> apply_filter_by_tx_type_to_transactions(type_filter)
|> join_associations(necessity_by_association)
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
- |> debug("result collated query")
|> Repo.all()
|> (&if(old_ui?,
do: &1,
@@ -3616,13 +3629,15 @@ defmodule Explorer.Chain do
Results will be the transactions older than the `inserted_at` and `hash` that are passed.
"""
- @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false, [String.t()], [
- :atom
- ]) :: [Transaction.t()]
- def recent_pending_transactions(options \\ [], old_ui? \\ true, method_id_filter \\ [], type_filter \\ [])
+ @spec recent_pending_transactions([paging_options | necessity_by_association_option], true | false) :: [
+ Transaction.t()
+ ]
+ def recent_pending_transactions(options \\ [], old_ui? \\ true)
when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
+ method_id_filter = Keyword.get(options, :method)
+ type_filter = Keyword.get(options, :type)
Transaction
|> page_pending_transaction(paging_options)
@@ -3630,10 +3645,9 @@ defmodule Explorer.Chain do
|> pending_transactions_query()
|> apply_filter_by_method_id_to_transactions(method_id_filter)
|> apply_filter_by_tx_type_to_transactions(type_filter)
- |> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash)
+ |> order_by([transaction], desc: transaction.inserted_at, asc: transaction.hash)
|> join_associations(necessity_by_association)
|> (&if(old_ui?, do: preload(&1, [{:token_transfers, [:token, :from_address, :to_address]}]), else: &1)).()
- |> debug("result pendging query")
|> Repo.all()
|> (&if(old_ui?,
do: &1,
@@ -3903,6 +3917,7 @@ defmodule Explorer.Chain do
def transaction_to_token_transfers(transaction_hash, options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
paging_options = options |> Keyword.get(:paging_options, @default_paging_options) |> Map.put(:asc_order, true)
+ token_type = Keyword.get(options, :token_type)
TokenTransfer
|> join(:inner, [token_transfer], transaction in assoc(token_transfer, :transaction))
@@ -3911,6 +3926,9 @@ defmodule Explorer.Chain do
transaction.hash == ^transaction_hash and token_transfer.block_hash == transaction.block_hash and
token_transfer.block_number == transaction.block_number
)
+ |> join(:inner, [tt], token in assoc(tt, :token), as: :token)
+ |> preload([token: token], [{:token, token}])
+ |> TokenTransfer.filter_by_type(token_type)
|> TokenTransfer.page_token_transfer(paging_options)
|> limit(^paging_options.page_size)
|> order_by([token_transfer], asc: token_transfer.log_index)
@@ -4424,6 +4442,30 @@ defmodule Explorer.Chain do
|> repo.insert(on_conflict: :nothing, conflict_target: [:address_hash, :name])
end
+ def get_verified_twin_contract(%Explorer.Chain.Address{} = target_address) do
+ case target_address do
+ %{contract_code: %Chain.Data{bytes: contract_code_bytes}} ->
+ target_address_hash = target_address.hash
+
+ contract_code_md5 = Helper.contract_code_md5(contract_code_bytes)
+
+ verified_contract_twin_query =
+ from(
+ smart_contract in SmartContract,
+ where: smart_contract.contract_code_md5 == ^contract_code_md5,
+ where: smart_contract.address_hash != ^target_address_hash,
+ select: smart_contract,
+ limit: 1
+ )
+
+ verified_contract_twin_query
+ |> Repo.one(timeout: 10_000)
+
+ _ ->
+ nil
+ end
+ end
+
@doc """
Finds metadata for verification of a contract from verified twins: contracts with the same bytecode
which were verified previously, returns a single t:SmartContract.t/0
@@ -4437,24 +4479,8 @@ defmodule Explorer.Chain do
def get_address_verified_twin_contract(%Explorer.Chain.Hash{} = address_hash) do
with target_address <- Repo.get(Address, address_hash),
- false <- is_nil(target_address),
- %{contract_code: %Chain.Data{bytes: contract_code_bytes}} <- target_address do
- target_address_hash = target_address.hash
-
- contract_code_md5 = Helper.contract_code_md5(contract_code_bytes)
-
- verified_contract_twin_query =
- from(
- smart_contract in SmartContract,
- where: smart_contract.contract_code_md5 == ^contract_code_md5,
- where: smart_contract.address_hash != ^target_address_hash,
- select: smart_contract,
- limit: 1
- )
-
- verified_contract_twin =
- verified_contract_twin_query
- |> Repo.one(timeout: 10_000)
+ false <- is_nil(target_address) do
+ verified_contract_twin = get_verified_twin_contract(target_address)
verified_contract_twin_additional_sources = get_contract_additional_sources(verified_contract_twin)
@@ -4547,13 +4573,28 @@ defmodule Explorer.Chain do
Chain.get_address_verified_twin_contract(address_hash).verified_contract
if address_verified_twin_contract do
- Map.put(address_verified_twin_contract, :address_hash, address_hash)
+ address_verified_twin_contract
+ |> Map.put(:address_hash, address_hash)
+ |> Map.put(:implementation_address_hash, current_smart_contract.implementation_address_hash)
+ |> Map.put(:implementation_name, current_smart_contract.implementation_name)
+ |> Map.put(:implementation_fetched_at, current_smart_contract.implementation_fetched_at)
else
current_smart_contract
end
end
end
+ @spec address_hash_to_smart_contract_without_twin(Hash.Address.t()) :: SmartContract.t() | nil
+ def address_hash_to_smart_contract_without_twin(address_hash) do
+ query =
+ from(
+ smart_contract in SmartContract,
+ where: smart_contract.address_hash == ^address_hash
+ )
+
+ Repo.one(query)
+ end
+
def smart_contract_fully_verified?(address_hash_str) when is_binary(address_hash_str) do
case string_to_address_hash(address_hash_str) do
{:ok, address_hash} ->
@@ -4604,13 +4645,28 @@ defmodule Explorer.Chain do
if Repo.one(query), do: true, else: false
end
- defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil) do
+ defp fetch_transactions(paging_options \\ nil, from_block \\ nil, to_block \\ nil, is_address? \\ false) do
Transaction
- |> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
+ |> order_for_transactions(is_address?)
|> where_block_number_in_period(from_block, to_block)
|> handle_paging_options(paging_options)
end
+ defp order_for_transactions(query, true) do
+ query
+ |> order_by([transaction],
+ desc: transaction.block_number,
+ desc: transaction.index,
+ desc: transaction.inserted_at,
+ asc: transaction.hash
+ )
+ end
+
+ defp order_for_transactions(query, _) do
+ query
+ |> order_by([transaction], desc: transaction.block_number, desc: transaction.index)
+ end
+
defp fetch_transactions_in_ascending_order_by_index(paging_options) do
Transaction
|> order_by([transaction], desc: transaction.block_number, asc: transaction.index)
@@ -4821,7 +4877,10 @@ defmodule Explorer.Chain do
where(
query,
[transaction],
- transaction.inserted_at < ^inserted_at or (transaction.inserted_at == ^inserted_at and transaction.hash < ^hash)
+ (is_nil(transaction.block_number) and
+ (transaction.inserted_at < ^inserted_at or
+ (transaction.inserted_at == ^inserted_at and transaction.hash > ^hash))) or
+ not is_nil(transaction.block_number)
)
end
@@ -4854,6 +4913,20 @@ defmodule Explorer.Chain do
defp page_search_results(query, %PagingOptions{key: nil}), do: query
+ defp page_search_results(query, %PagingOptions{
+ key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type}
+ })
+ when holder_count in [nil, ""] do
+ where(
+ query,
+ [item],
+ (item.name > ^name and item.type == ^item_type) or
+ (item.name == ^name and item.inserted_at < ^inserted_at and
+ item.type == ^item_type) or
+ item.type != ^item_type
+ )
+ end
+
# credo:disable-for-next-line
defp page_search_results(query, %PagingOptions{
key: {_address_hash, _tx_hash, _block_hash, holder_count, name, inserted_at, item_type}
@@ -6413,12 +6486,13 @@ defmodule Explorer.Chain do
end
end
- def combine_proxy_implementation_abi(proxy_address_hash, abi) when not is_nil(abi) do
- implementation_abi = get_implementation_abi_from_proxy(proxy_address_hash, abi)
+ def combine_proxy_implementation_abi(%SmartContract{abi: abi} = smart_contract) when not is_nil(abi) do
+ implementation_abi = get_implementation_abi_from_proxy(smart_contract)
+
if Enum.empty?(implementation_abi), do: abi, else: implementation_abi ++ abi
end
- def combine_proxy_implementation_abi(_, abi) when is_nil(abi) do
+ def combine_proxy_implementation_abi(_) do
[]
end
@@ -6450,10 +6524,7 @@ defmodule Explorer.Chain do
def gnosis_safe_contract?(abi) when is_nil(abi), do: false
- defp master_copy_pattern?(method) do
- method
- |> contructor_accepts_named_input("_masterCopy")
- end
+
defp singleton_pattern?(method) do
method
@@ -6474,222 +6545,32 @@ defmodule Explorer.Chain do
end)
end
- defp get_input_by_name(inputs, name) do
- inputs
- |> Enum.find(fn input ->
- Map.get(input, "name") == name
- end)
- end
-
- @spec get_implementation_address_hash(Hash.Address.t(), list()) :: {String.t() | nil, String.t() | nil}
- def get_implementation_address_hash(proxy_address_hash, abi)
- when not is_nil(proxy_address_hash) and not is_nil(abi) do
- implementation_method_abi =
- abi
- |> Enum.find(fn method ->
- Map.get(method, "name") == "implementation" && Map.get(method, "stateMutability") == "view"
- end)
-
- master_copy_method_abi =
- abi
- |> Enum.find(fn method ->
- master_copy_pattern?(method)
- end)
-
- implementation_address =
- cond do
- implementation_method_abi ->
- get_implementation_address_hash_basic(proxy_address_hash, abi)
-
- master_copy_method_abi ->
- get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash)
-
- true ->
- get_implementation_address_hash_eip_1967(proxy_address_hash)
- end
-
- save_implementation_name(implementation_address, proxy_address_hash)
- end
-
- def get_implementation_address_hash(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do
- {nil, nil}
- end
-
- defp get_implementation_address_hash_eip_1967(proxy_address_hash) do
- json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
-
- # https://eips.ethereum.org/EIPS/eip-1967
- storage_slot_logic_contract_address = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
-
- {_status, implementation_address} =
- case Contract.eth_get_storage_at_request(
- proxy_address_hash,
- storage_slot_logic_contract_address,
- nil,
- json_rpc_named_arguments
- ) do
- {:ok, empty_address}
- when empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] ->
- fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments)
-
- {:ok, implementation_logic_address} ->
- {:ok, implementation_logic_address}
-
- {:error, _} ->
- {:ok, "0x"}
- end
-
- abi_decode_address_output(implementation_address)
- end
-
- # changes requested by https://github.com/blockscout/blockscout/issues/4770
- # for support BeaconProxy pattern
- defp fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do
- # https://eips.ethereum.org/EIPS/eip-1967
- storage_slot_beacon_contract_address = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"
-
- implementation_method_abi = [
- %{
- "type" => "function",
- "stateMutability" => "view",
- "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}],
- "name" => "implementation",
- "inputs" => []
- }
- ]
-
- case Contract.eth_get_storage_at_request(
- proxy_address_hash,
- storage_slot_beacon_contract_address,
- nil,
- json_rpc_named_arguments
- ) do
- {:ok, empty_address}
- when empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] ->
- fetch_openzeppelin_proxy_implementation(proxy_address_hash, json_rpc_named_arguments)
-
- {:ok, beacon_contract_address} ->
- case beacon_contract_address
- |> abi_decode_address_output()
- |> get_implementation_address_hash_basic(implementation_method_abi) do
- <> ->
- {:ok, implementation_address}
+ def master_copy_pattern?(method) do
+ Map.get(method, "type") == "constructor" &&
+ method
+ |> Enum.find(fn item ->
+ case item do
+ {"inputs", inputs} ->
+ master_copy_input?(inputs)
_ ->
- {:ok, beacon_contract_address}
+ false
end
-
- {:error, _} ->
- {:ok, "0x"}
- end
- end
-
- # changes requested by https://github.com/blockscout/blockscout/issues/5292
- defp fetch_openzeppelin_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do
- # This is the keccak-256 hash of "org.zeppelinos.proxy.implementation"
- storage_slot_logic_contract_address = "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3"
-
- case Contract.eth_get_storage_at_request(
- proxy_address_hash,
- storage_slot_logic_contract_address,
- nil,
- json_rpc_named_arguments
- ) do
- {:ok, empty_address}
- when empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] ->
- {:ok, "0x"}
-
- {:ok, logic_contract_address} ->
- {:ok, logic_contract_address}
-
- {:error, _} ->
- {:ok, "0x"}
- end
- end
-
- defp get_implementation_address_hash_basic(proxy_address_hash, abi) do
- # 5c60da1b = keccak256(implementation())
- implementation_address =
- case Reader.query_contract(
- proxy_address_hash,
- abi,
- %{
- "5c60da1b" => []
- },
- false
- ) do
- %{"5c60da1b" => {:ok, [result]}} -> result
- _ -> nil
- end
-
- address_to_hex(implementation_address)
- end
-
- defp get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) do
- json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
-
- master_copy_storage_pointer = "0x0"
-
- {:ok, implementation_address} =
- Contract.eth_get_storage_at_request(
- proxy_address_hash,
- master_copy_storage_pointer,
- nil,
- json_rpc_named_arguments
- )
-
- abi_decode_address_output(implementation_address)
- end
-
- defp save_implementation_name(empty_address_hash_string, _)
- when empty_address_hash_string in [
- "0x",
- "0x0",
- "0x0000000000000000000000000000000000000000000000000000000000000000",
- @burn_address_hash_str
- ],
- do: {empty_address_hash_string, nil}
-
- defp save_implementation_name(implementation_address_hash_string, proxy_address_hash)
- when is_binary(implementation_address_hash_string) do
- with {:ok, address_hash} <- string_to_address_hash(implementation_address_hash_string),
- %SmartContract{name: name} <- address_hash_to_smart_contract(address_hash) do
- SmartContract
- |> where([sc], sc.address_hash == ^proxy_address_hash)
- |> update(set: [implementation_name: ^name])
- |> Repo.update_all([])
-
- {implementation_address_hash_string, name}
- else
- _ ->
- {implementation_address_hash_string, nil}
- end
+ end)
end
- defp save_implementation_name(other, _), do: {other, nil}
-
- defp abi_decode_address_output(nil), do: nil
-
- defp abi_decode_address_output("0x"), do: @burn_address_hash_str
-
- defp abi_decode_address_output(address) when is_binary(address) do
- if String.length(address) > 42 do
- "0x" <> String.slice(address, -40, 40)
- else
- address
- end
+ defp get_input_by_name(inputs, name) do
+ inputs
+ |> Enum.find(fn input ->
+ Map.get(input, "name") == name
+ end)
end
- defp abi_decode_address_output(_), do: nil
-
- defp address_to_hex(address) do
- if address do
- if String.starts_with?(address, "0x") do
- address
- else
- "0x" <> Base.encode16(address, case: :lower)
- end
- end
+ defp master_copy_input?(inputs) do
+ inputs
+ |> Enum.find(fn input ->
+ Map.get(input, "name") == "_masterCopy"
+ end)
end
def get_implementation_abi(implementation_address_hash_string) when not is_nil(implementation_address_hash_string) do
@@ -6715,15 +6596,13 @@ defmodule Explorer.Chain do
[]
end
- def get_implementation_abi_from_proxy(proxy_address_hash, abi)
+ def get_implementation_abi_from_proxy(%SmartContract{address_hash: proxy_address_hash, abi: abi} = smart_contract)
when not is_nil(proxy_address_hash) and not is_nil(abi) do
- {implementation_address_hash_string, _name} = get_implementation_address_hash(proxy_address_hash, abi)
+ {implementation_address_hash_string, _name} = SmartContract.get_implementation_address_hash(smart_contract)
get_implementation_abi(implementation_address_hash_string)
end
- def get_implementation_abi_from_proxy(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do
- []
- end
+ def get_implementation_abi_from_proxy(_), do: []
defp format_tx_first_trace(first_trace, block_hash, json_rpc_named_arguments) do
{:ok, to_address_hash} =
@@ -7076,10 +6955,16 @@ defmodule Explorer.Chain do
recent_pending_transactions(options, false, method_id_filter, type_filter_options)
end
- def recent_transactions(options, _, method_id_filter, type_filter_options) do
- recent_collated_transactions(false, options, method_id_filter, type_filter_options)
+ def recent_transactions(options, [:pending | _]) do
+ recent_pending_transactions(options, false)
+ end
+
+ def recent_transactions(options, _) do
+ recent_collated_transactions(false, options)
end
+ def apply_filter_by_method_id_to_transactions(query, nil), do: query
+
def apply_filter_by_method_id_to_transactions(query, filter) when is_list(filter) do
method_ids = Enum.flat_map(filter, &map_name_or_method_id_to_method_id/1)
@@ -7290,4 +7175,83 @@ defmodule Explorer.Chain do
def count_new_contracts_from_cache do
NewContractsCounter.fetch()
end
+
+ def address_counters(address) do
+ validation_count_task =
+ Task.async(fn ->
+ address_to_validation_count(address.hash)
+ end)
+
+ Task.start_link(fn ->
+ transaction_count(address)
+ end)
+
+ Task.start_link(fn ->
+ token_transfers_count(address)
+ end)
+
+ Task.start_link(fn ->
+ gas_usage_count(address)
+ end)
+
+ [
+ validation_count_task
+ ]
+ |> Task.yield_many(:infinity)
+ |> Enum.map(fn {_task, res} ->
+ case res do
+ {:ok, result} ->
+ result
+
+ {:exit, reason} ->
+ raise "Query fetching address counters terminated: #{inspect(reason)}"
+
+ nil ->
+ raise "Query fetching address counters timed out."
+ end
+ end)
+ |> List.to_tuple()
+ end
+
+ def transaction_count(address) do
+ AddressTransactionsCounter.fetch(address)
+ end
+
+ def token_transfers_count(address) do
+ AddressTokenTransfersCounter.fetch(address)
+ end
+
+ def gas_usage_count(address) do
+ AddressTransactionsGasUsageCounter.fetch(address)
+ end
+
+ def fetch_token_counters(address_hash, timeout) do
+ total_token_transfers_task =
+ Task.async(fn ->
+ TokenTransfersCounter.fetch(address_hash)
+ end)
+
+ total_token_holders_task =
+ Task.async(fn ->
+ TokenHoldersCounter.fetch(address_hash)
+ end)
+
+ [total_token_transfers_task, total_token_holders_task]
+ |> Task.yield_many(timeout)
+ |> Enum.map(fn {_task, res} ->
+ case res do
+ {:ok, result} ->
+ result
+
+ {:exit, reason} ->
+ Logger.warn("Query fetching token counters terminated: #{inspect(reason)}")
+ 0
+
+ nil ->
+ Logger.warn("Query fetching token counters timed out.")
+ 0
+ end
+ end)
+ |> List.to_tuple()
+ end
end
diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex
index 4cf3fbd813d0..93174cb70c5f 100644
--- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex
+++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex
@@ -23,7 +23,7 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do
* `token_contract_address_hash` - The contract address hash foreign key.
* `block_number` - The block's number that the transfer took place.
* `value` - The value that's represents the balance.
- * `token_id` - The token_id of the transferred token (applicable for ERC-1155 and ERC-721 tokens)
+ * `token_id` - The token_id of the transferred token (applicable for ERC-1155)
* `token_type` - The type of the token
"""
@type t :: %__MODULE__{
diff --git a/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex b/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex
new file mode 100644
index 000000000000..0a5cf715c1bc
--- /dev/null
+++ b/apps/explorer/lib/explorer/chain/cache/transactions_api_v2.ex
@@ -0,0 +1,27 @@
+defmodule Explorer.Chain.Cache.TransactionsApiV2 do
+ @moduledoc """
+ Caches the latest imported transactions
+ """
+
+ alias Explorer.Chain.Transaction
+
+ use Explorer.Chain.OrderedCache,
+ name: :transactions_api_v2,
+ max_size: 51,
+ preloads: [
+ :block,
+ created_contract_address: :names,
+ from_address: :names,
+ to_address: :names
+ ],
+ ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval],
+ global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl]
+
+ @type element :: Transaction.t()
+
+ @type id :: {non_neg_integer(), non_neg_integer()}
+
+ def element_to_id(%Transaction{block_number: block_number, index: index}) do
+ {block_number, index}
+ end
+end
diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
index c33d7cf24d37..e0f0caa99801 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
@@ -220,51 +220,15 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
- %{
- changes_list_no_token_id: changes_list_no_token_id,
- changes_list_with_token_id: changes_list_with_token_id
- } =
+ ordered_changes_list =
changes_list
- |> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc ->
- updated_change =
- if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do
- change
- else
- Map.put(change, :token_id, nil)
- end
-
- if updated_change.token_id do
- changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id]
-
- %{
- changes_list_no_token_id: acc.changes_list_no_token_id,
- changes_list_with_token_id: changes_list_with_token_id
- }
+ |> Enum.map(fn change ->
+ if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do
+ change
else
- changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id]
-
- %{
- changes_list_no_token_id: changes_list_no_token_id,
- changes_list_with_token_id: acc.changes_list_with_token_id
- }
+ Map.put(change, :token_id, nil)
end
end)
-
- ordered_changes_list_no_token_id =
- changes_list_no_token_id
- |> Enum.group_by(fn %{
- address_hash: address_hash,
- token_contract_address_hash: token_contract_address_hash
- } ->
- {address_hash, token_contract_address_hash}
- end)
- |> Enum.map(fn {_, grouped_address_token_balances} ->
- Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end)
- end)
- |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash})
-
- ordered_changes_list_with_token_id =
- changes_list_with_token_id
|> Enum.group_by(fn %{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
@@ -273,33 +237,18 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
{address_hash, token_contract_address_hash, token_id}
end)
|> Enum.map(fn {_, grouped_address_token_balances} ->
- Enum.max_by(grouped_address_token_balances, fn %{block_number: block_number} -> block_number end)
+ Enum.max_by(grouped_address_token_balances, fn balance ->
+ {Map.get(balance, :block_number), Map.get(balance, :value_fetched_at)}
+ end)
end)
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash})
- {:ok, inserted_changes_list_no_token_id} =
- if Enum.count(ordered_changes_list_no_token_id) > 0 do
- Import.insert_changes_list(
- repo,
- ordered_changes_list_no_token_id,
- conflict_target: {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash) WHERE token_id IS NULL>},
- on_conflict: on_conflict,
- for: CurrentTokenBalance,
- returning: true,
- timeout: timeout,
- timestamps: timestamps
- )
- else
- {:ok, []}
- end
-
- {:ok, inserted_changes_list_with_token_id} =
- if Enum.count(ordered_changes_list_with_token_id) > 0 do
+ {:ok, inserted_changes_list} =
+ if Enum.count(ordered_changes_list) > 0 do
Import.insert_changes_list(
repo,
- ordered_changes_list_with_token_id,
- conflict_target:
- {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, token_id) WHERE token_id IS NOT NULL>},
+ ordered_changes_list,
+ conflict_target: {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, COALESCE(token_id, -1))>},
on_conflict: on_conflict,
for: CurrentTokenBalance,
returning: true,
@@ -310,7 +259,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
{:ok, []}
end
- inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id
+ inserted_changes_list
end
defp default_on_conflict do
@@ -329,9 +278,10 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
],
where:
fragment("? < EXCLUDED.block_number", current_token_balance.block_number) or
- (fragment("EXCLUDED.value IS NOT NULL") and
- is_nil(current_token_balance.value_fetched_at) and
- fragment("? = EXCLUDED.block_number", current_token_balance.block_number))
+ (fragment("? = EXCLUDED.block_number", current_token_balance.block_number) and
+ fragment("EXCLUDED.value IS NOT NULL") and
+ (is_nil(current_token_balance.value_fetched_at) or
+ fragment("? < EXCLUDED.value_fetched_at", current_token_balance.value_fetched_at)))
)
end
diff --git a/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex b/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex
index d80f561bbe7a..e991427de4d8 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex
@@ -60,58 +60,15 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce TokenBalance ShareLocks order (see docs: sharelocks.md)
- %{
- changes_list_no_token_id: changes_list_no_token_id,
- changes_list_with_token_id: changes_list_with_token_id
- } =
+ ordered_changes_list =
changes_list
- |> Enum.reduce(%{changes_list_no_token_id: [], changes_list_with_token_id: []}, fn change, acc ->
- updated_change =
- if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do
- change
- else
- Map.put(change, :token_id, nil)
- end
-
- if updated_change.token_id do
- changes_list_with_token_id = [updated_change | acc.changes_list_with_token_id]
-
- %{
- changes_list_no_token_id: acc.changes_list_no_token_id,
- changes_list_with_token_id: changes_list_with_token_id
- }
+ |> Enum.map(fn change ->
+ if Map.has_key?(change, :token_id) and Map.get(change, :token_type) == "ERC-1155" do
+ change
else
- changes_list_no_token_id = [updated_change | acc.changes_list_no_token_id]
-
- %{
- changes_list_no_token_id: changes_list_no_token_id,
- changes_list_with_token_id: acc.changes_list_with_token_id
- }
+ Map.put(change, :token_id, nil)
end
end)
-
- ordered_changes_list_no_token_id =
- changes_list_no_token_id
- |> Enum.group_by(fn %{
- address_hash: address_hash,
- token_contract_address_hash: token_contract_address_hash,
- block_number: block_number
- } ->
- {token_contract_address_hash, address_hash, block_number}
- end)
- |> Enum.map(fn {_, grouped_address_token_balances} ->
- dedup = Enum.dedup(grouped_address_token_balances)
-
- if Enum.count(dedup) > 1 do
- Enum.max_by(dedup, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end)
- else
- Enum.at(dedup, 0)
- end
- end)
- |> Enum.sort_by(&{&1.token_contract_address_hash, &1.address_hash, &1.block_number})
-
- ordered_changes_list_with_token_id =
- changes_list_with_token_id
|> Enum.group_by(fn %{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
@@ -122,20 +79,20 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
end)
|> Enum.map(fn {_, grouped_address_token_balances} ->
if Enum.count(grouped_address_token_balances) > 1 do
- Enum.max_by(grouped_address_token_balances, fn %{value_fetched_at: value_fetched_at} -> value_fetched_at end)
+ Enum.max_by(grouped_address_token_balances, fn balance -> Map.get(balance, :value_fetched_at) end)
else
Enum.at(grouped_address_token_balances, 0)
end
end)
|> Enum.sort_by(&{&1.token_contract_address_hash, &1.token_id, &1.address_hash, &1.block_number})
- {:ok, inserted_changes_list_no_token_id} =
- if Enum.count(ordered_changes_list_no_token_id) > 0 do
+ {:ok, inserted_changes_list} =
+ if Enum.count(ordered_changes_list) > 0 do
Import.insert_changes_list(
repo,
- ordered_changes_list_no_token_id,
+ ordered_changes_list,
conflict_target:
- {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, block_number) WHERE token_id IS NULL>},
+ {:unsafe_fragment, ~s<(address_hash, token_contract_address_hash, COALESCE(token_id, -1), block_number)>},
on_conflict: on_conflict,
for: TokenBalance,
returning: true,
@@ -146,26 +103,6 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
{:ok, []}
end
- {:ok, inserted_changes_list_with_token_id} =
- if Enum.count(ordered_changes_list_with_token_id) > 0 do
- Import.insert_changes_list(
- repo,
- ordered_changes_list_with_token_id,
- conflict_target:
- {:unsafe_fragment,
- ~s<(address_hash, token_contract_address_hash, token_id, block_number) WHERE token_id IS NOT NULL>},
- on_conflict: on_conflict,
- for: TokenBalance,
- returning: true,
- timeout: timeout,
- timestamps: timestamps
- )
- else
- {:ok, []}
- end
-
- inserted_changes_list = inserted_changes_list_no_token_id ++ inserted_changes_list_with_token_id
-
{:ok, inserted_changes_list}
end
@@ -174,7 +111,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
token_balance in TokenBalance,
update: [
set: [
- value: fragment("EXCLUDED.value"),
+ value: fragment("COALESCE(EXCLUDED.value, ?)", token_balance.value),
value_fetched_at: fragment("EXCLUDED.value_fetched_at"),
token_type: fragment("EXCLUDED.token_type"),
inserted_at: fragment("LEAST(EXCLUDED.inserted_at, ?)", token_balance.inserted_at),
@@ -182,9 +119,8 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
]
],
where:
- fragment("EXCLUDED.value IS NOT NULL") and
- (is_nil(token_balance.value_fetched_at) or
- fragment("? < EXCLUDED.value_fetched_at", token_balance.value_fetched_at))
+ is_nil(token_balance.value_fetched_at) or fragment("EXCLUDED.value_fetched_at IS NULL") or
+ fragment("? < EXCLUDED.value_fetched_at", token_balance.value_fetched_at)
)
end
end
diff --git a/apps/explorer/lib/explorer/chain/log.ex b/apps/explorer/lib/explorer/chain/log.ex
index 013549865be0..d8529a7a260a 100644
--- a/apps/explorer/lib/explorer/chain/log.ex
+++ b/apps/explorer/lib/explorer/chain/log.ex
@@ -131,8 +131,8 @@ defmodule Explorer.Chain.Log do
]
case Chain.find_contract_address(log.address_hash, address_options, true) do
- {:ok, %{smart_contract: %{abi: abi}}} ->
- full_abi = Chain.combine_proxy_implementation_abi(log.address_hash, abi)
+ {:ok, %{smart_contract: smart_contract}} ->
+ full_abi = Chain.combine_proxy_implementation_abi(smart_contract)
with {:ok, selector, mapping} <- find_and_decode(full_abi, log, transaction),
identifier <- Base.encode16(selector.method_id, case: :lower),
diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex
index 5fe6f3668383..56724590e797 100644
--- a/apps/explorer/lib/explorer/chain/smart_contract.ex
+++ b/apps/explorer/lib/explorer/chain/smart_contract.ex
@@ -13,9 +13,15 @@ defmodule Explorer.Chain.SmartContract do
use Explorer.Schema
alias Ecto.Changeset
+ alias EthereumJSONRPC.Contract
+ alias Explorer.Counters.AverageBlockTime
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Address, ContractMethod, DecompiledSmartContract, Hash}
alias Explorer.Chain.SmartContract.ExternalLibrary
+ alias Explorer.SmartContract.Reader
+ alias Timex.Duration
+
+ @burn_address_hash_str "0x0000000000000000000000000000000000000000"
@typedoc """
The name of a parameter to a function or event.
@@ -197,11 +203,15 @@ defmodule Explorer.Chain.SmartContract do
* `partially_verified` - whether contract verified using partial matched source code or not.
* `is_vyper_contract` - boolean flag, determines if contract is Vyper or not
* `file_path` - show the filename or path to the file of the contract source file
- * `is_changed_bytecode` - boolean flag, determines if contract's bytecode was modified
+ * `is_changed_bytecode` - boolean flag, determines if contract's bytecode was modified
* `bytecode_checked_at` - timestamp of the last check of contract's bytecode matching (DB and BlockChain)
* `contract_code_md5` - md5(`t:Explorer.Chain.Address.t/0` `contract_code`)
* `implementation_name` - name of the proxy implementation
+ * `compiler_settings` - raw compilation parameters
+ * `implementation_fetched_at` - timestamp of the last fetching contract's implementation info
+ * `implementation_address_hash` - address hash of the proxy's implementation if any
* `autodetect_constructor_args` - field was added for storing user's choice
+ * `is_yul` - field was added for storing user's choice
"""
@type t :: %Explorer.Chain.SmartContract{
@@ -221,7 +231,11 @@ defmodule Explorer.Chain.SmartContract do
bytecode_checked_at: DateTime.t(),
contract_code_md5: String.t(),
implementation_name: String.t() | nil,
- autodetect_constructor_args: boolean | nil
+ compiler_settings: map() | nil,
+ implementation_fetched_at: DateTime.t(),
+ implementation_address_hash: Hash.Address.t(),
+ autodetect_constructor_args: boolean | nil,
+ is_yul: boolean | nil
}
schema "smart_contracts" do
@@ -242,7 +256,11 @@ defmodule Explorer.Chain.SmartContract do
field(:bytecode_checked_at, :utc_datetime_usec, default: DateTime.add(DateTime.utc_now(), -86400, :second))
field(:contract_code_md5, :string)
field(:implementation_name, :string)
+ field(:compiler_settings, :map)
+ field(:implementation_fetched_at, :utc_datetime_usec, default: nil)
+ field(:implementation_address_hash, Hash.Address, default: nil)
field(:autodetect_constructor_args, :boolean, virtual: true)
+ field(:is_yul, :boolean, virtual: true)
has_many(
:decompiled_smart_contracts,
@@ -284,14 +302,16 @@ defmodule Explorer.Chain.SmartContract do
:is_changed_bytecode,
:bytecode_checked_at,
:contract_code_md5,
- :implementation_name
+ :implementation_name,
+ :compiler_settings,
+ :implementation_address_hash,
+ :implementation_fetched_at
])
|> validate_required([
:name,
:compiler_version,
:optimization,
:contract_source_code,
- :abi,
:address_hash,
:contract_code_md5
])
@@ -431,6 +451,7 @@ defmodule Explorer.Chain.SmartContract do
%__MODULE__{}
|> changeset(Map.from_struct(twin_contract))
|> Changeset.put_change(:autodetect_constructor_args, true)
+ |> Changeset.put_change(:is_yul, false)
|> Changeset.force_change(:address_hash, Changeset.get_field(changeset, :address_hash))
end
@@ -443,6 +464,7 @@ defmodule Explorer.Chain.SmartContract do
|> Changeset.put_change(:compiler_version, "latest")
|> Changeset.put_change(:contract_source_code, "")
|> Changeset.put_change(:autodetect_constructor_args, true)
+ |> Changeset.put_change(:is_yul, false)
end
def merge_twin_vyper_contract_with_changeset(
@@ -485,4 +507,370 @@ defmodule Explorer.Chain.SmartContract do
end
defp to_address_hash(address_hash), do: address_hash
+
+ def proxy_contract?(%__MODULE__{abi: abi} = smart_contract) when not is_nil(abi) do
+ implementation_method_abi =
+ abi
+ |> Enum.find(fn method ->
+ Map.get(method, "name") == "implementation" ||
+ Chain.master_copy_pattern?(method)
+ end)
+
+ if implementation_method_abi ||
+ not is_nil(
+ smart_contract
+ |> get_implementation_address_hash()
+ |> Tuple.to_list()
+ |> List.first()
+ ),
+ do: true,
+ else: false
+ end
+
+ def proxy_contract?(_), do: false
+
+ def get_implementation_address_hash(%__MODULE__{abi: nil}), do: false
+
+ def get_implementation_address_hash(
+ %__MODULE__{
+ address_hash: address_hash,
+ implementation_fetched_at: implementation_fetched_at
+ } = smart_contract
+ ) do
+ updated_smart_contract =
+ if Application.get_env(:explorer, :enable_caching_implementation_data_of_proxy) &&
+ check_implementation_refetch_neccessity(implementation_fetched_at) do
+ Chain.address_hash_to_smart_contract(address_hash)
+ else
+ smart_contract
+ end
+
+ get_implementation_address_hash({:updated, updated_smart_contract})
+ end
+
+ def get_implementation_address_hash(
+ {:updated,
+ %__MODULE__{
+ address_hash: address_hash,
+ abi: abi,
+ implementation_address_hash: implementation_address_hash_from_db,
+ implementation_name: implementation_name_from_db,
+ implementation_fetched_at: implementation_fetched_at
+ }}
+ ) do
+ if check_implementation_refetch_neccessity(implementation_fetched_at) do
+ get_implementation_address_hash_task = Task.async(fn -> get_implementation_address_hash(address_hash, abi) end)
+
+ timeout = Application.get_env(:explorer, :implementation_data_fetching_timeout)
+
+ case Task.yield(get_implementation_address_hash_task, timeout) ||
+ Task.ignore(get_implementation_address_hash_task) do
+ {:ok, {:empty, :empty}} ->
+ {nil, nil}
+
+ {:ok, {address_hash, _name} = result} when not is_nil(address_hash) ->
+ result
+
+ _ ->
+ {db_implementation_data_converter(implementation_address_hash_from_db),
+ db_implementation_data_converter(implementation_name_from_db)}
+ end
+ else
+ {db_implementation_data_converter(implementation_address_hash_from_db),
+ db_implementation_data_converter(implementation_name_from_db)}
+ end
+ end
+
+ def get_implementation_address_hash(_), do: {nil, nil}
+
+ defp db_implementation_data_converter(nil), do: nil
+ defp db_implementation_data_converter(string) when is_binary(string), do: string
+ defp db_implementation_data_converter(other), do: to_string(other)
+
+ defp check_implementation_refetch_neccessity(nil), do: true
+
+ defp check_implementation_refetch_neccessity(timestamp) do
+ if Application.get_env(:explorer, :enable_caching_implementation_data_of_proxy) do
+ now = DateTime.utc_now()
+
+ average_block_time =
+ if Application.get_env(:explorer, :avg_block_time_as_ttl_cached_implementation_data_of_proxy) do
+ case AverageBlockTime.average_block_time() do
+ {:error, :disabled} ->
+ 0
+
+ duration ->
+ duration
+ |> Duration.to_milliseconds()
+ end
+ else
+ 0
+ end
+
+ fresh_time_distance =
+ case average_block_time do
+ 0 ->
+ Application.get_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy)
+
+ time ->
+ round(time)
+ end
+
+ timestamp
+ |> DateTime.add(fresh_time_distance, :millisecond)
+ |> DateTime.compare(now) != :gt
+ else
+ true
+ end
+ end
+
+ @spec get_implementation_address_hash(Hash.Address.t(), list()) :: {String.t() | nil, String.t() | nil}
+ defp get_implementation_address_hash(proxy_address_hash, abi)
+ when not is_nil(proxy_address_hash) and not is_nil(abi) do
+ implementation_method_abi =
+ abi
+ |> Enum.find(fn method ->
+ Map.get(method, "name") == "implementation" && Map.get(method, "stateMutability") == "view"
+ end)
+
+ master_copy_method_abi =
+ abi
+ |> Enum.find(fn method ->
+ Chain.master_copy_pattern?(method)
+ end)
+
+ implementation_address =
+ cond do
+ implementation_method_abi ->
+ get_implementation_address_hash_basic(proxy_address_hash, abi)
+
+ master_copy_method_abi ->
+ get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash)
+
+ true ->
+ get_implementation_address_hash_eip_1967(proxy_address_hash)
+ end
+
+ save_implementation_data(implementation_address, proxy_address_hash)
+ end
+
+ defp get_implementation_address_hash(proxy_address_hash, abi) when is_nil(proxy_address_hash) or is_nil(abi) do
+ {nil, nil}
+ end
+
+ defp get_implementation_address_hash_eip_1967(proxy_address_hash) do
+ json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
+
+ # https://eips.ethereum.org/EIPS/eip-1967
+ storage_slot_logic_contract_address = "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc"
+
+ {_status, implementation_address} =
+ case Contract.eth_get_storage_at_request(
+ proxy_address_hash,
+ storage_slot_logic_contract_address,
+ nil,
+ json_rpc_named_arguments
+ ) do
+ {:ok, empty_address}
+ when empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000", nil] ->
+ fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments)
+
+ {:ok, implementation_logic_address} ->
+ {:ok, implementation_logic_address}
+
+ _ ->
+ {:ok, nil}
+ end
+
+ abi_decode_address_output(implementation_address)
+ end
+
+ # changes requested by https://github.com/blockscout/blockscout/issues/4770
+ # for support BeaconProxy pattern
+ defp fetch_beacon_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do
+ # https://eips.ethereum.org/EIPS/eip-1967
+ storage_slot_beacon_contract_address = "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50"
+
+ implementation_method_abi = [
+ %{
+ "type" => "function",
+ "stateMutability" => "view",
+ "outputs" => [%{"type" => "address", "name" => "", "internalType" => "address"}],
+ "name" => "implementation",
+ "inputs" => []
+ }
+ ]
+
+ case Contract.eth_get_storage_at_request(
+ proxy_address_hash,
+ storage_slot_beacon_contract_address,
+ nil,
+ json_rpc_named_arguments
+ ) do
+ {:ok, empty_address}
+ when empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000", nil] ->
+ fetch_openzeppelin_proxy_implementation(proxy_address_hash, json_rpc_named_arguments)
+
+ {:ok, beacon_contract_address} ->
+ case beacon_contract_address
+ |> abi_decode_address_output()
+ |> get_implementation_address_hash_basic(implementation_method_abi) do
+ <> ->
+ {:ok, implementation_address}
+
+ _ ->
+ {:ok, beacon_contract_address}
+ end
+
+ _ ->
+ {:ok, nil}
+ end
+ end
+
+ # changes requested by https://github.com/blockscout/blockscout/issues/5292
+ defp fetch_openzeppelin_proxy_implementation(proxy_address_hash, json_rpc_named_arguments) do
+ # This is the keccak-256 hash of "org.zeppelinos.proxy.implementation"
+ storage_slot_logic_contract_address = "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3"
+
+ case Contract.eth_get_storage_at_request(
+ proxy_address_hash,
+ storage_slot_logic_contract_address,
+ nil,
+ json_rpc_named_arguments
+ ) do
+ {:ok, empty_address}
+ when empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] ->
+ {:ok, "0x"}
+
+ {:ok, logic_contract_address} ->
+ {:ok, logic_contract_address}
+
+ _ ->
+ {:ok, nil}
+ end
+ end
+
+ defp get_implementation_address_hash_basic(proxy_address_hash, abi) do
+ # 5c60da1b = keccak256(implementation())
+ implementation_address =
+ case Reader.query_contract(
+ proxy_address_hash,
+ abi,
+ %{
+ "5c60da1b" => []
+ },
+ false
+ ) do
+ %{"5c60da1b" => {:ok, [result]}} -> result
+ _ -> nil
+ end
+
+ address_to_hex(implementation_address)
+ end
+
+ defp get_implementation_address_hash_from_master_copy_pattern(proxy_address_hash) do
+ json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
+
+ master_copy_storage_pointer = "0x0"
+
+ {:ok, implementation_address} =
+ case Contract.eth_get_storage_at_request(
+ proxy_address_hash,
+ master_copy_storage_pointer,
+ nil,
+ json_rpc_named_arguments
+ ) do
+ {:ok, empty_address}
+ when empty_address in ["0x", "0x0", "0x0000000000000000000000000000000000000000000000000000000000000000"] ->
+ {:ok, "0x"}
+
+ {:ok, logic_contract_address} ->
+ {:ok, logic_contract_address}
+
+ _ ->
+ {:ok, nil}
+ end
+
+ abi_decode_address_output(implementation_address)
+ end
+
+ defp save_implementation_data(nil, _), do: {nil, nil}
+
+ defp save_implementation_data(empty_address_hash_string, proxy_address_hash)
+ when empty_address_hash_string in [
+ "0x",
+ "0x0",
+ "0x0000000000000000000000000000000000000000000000000000000000000000",
+ @burn_address_hash_str
+ ] do
+ proxy_address_hash
+ |> Chain.address_hash_to_smart_contract_without_twin()
+ |> changeset(%{
+ implementation_name: nil,
+ implementation_address_hash: nil,
+ implementation_fetched_at: DateTime.utc_now()
+ })
+ |> Repo.update()
+
+ {:empty, :empty}
+ end
+
+ defp save_implementation_data(implementation_address_hash_string, proxy_address_hash)
+ when is_binary(implementation_address_hash_string) do
+ with {:ok, address_hash} <- Chain.string_to_address_hash(implementation_address_hash_string),
+ proxy_contract <- Chain.address_hash_to_smart_contract_without_twin(proxy_address_hash),
+ false <- is_nil(proxy_contract),
+ %{implementation: %__MODULE__{name: name}, proxy: proxy_contract} <- %{
+ implementation: Chain.address_hash_to_smart_contract(address_hash),
+ proxy: proxy_contract
+ } do
+ proxy_contract
+ |> changeset(%{
+ implementation_name: name,
+ implementation_address_hash: implementation_address_hash_string,
+ implementation_fetched_at: DateTime.utc_now()
+ })
+ |> Repo.update()
+
+ {implementation_address_hash_string, name}
+ else
+ %{implementation: _, proxy: proxy_contract} ->
+ proxy_contract
+ |> changeset(%{
+ implementation_name: nil,
+ implementation_address_hash: implementation_address_hash_string,
+ implementation_fetched_at: DateTime.utc_now()
+ })
+ |> Repo.update()
+
+ {implementation_address_hash_string, nil}
+
+ _ ->
+ {implementation_address_hash_string, nil}
+ end
+ end
+
+ defp address_to_hex(address) do
+ if address do
+ if String.starts_with?(address, "0x") do
+ address
+ else
+ "0x" <> Base.encode16(address, case: :lower)
+ end
+ end
+ end
+
+ defp abi_decode_address_output(nil), do: nil
+
+ defp abi_decode_address_output("0x"), do: @burn_address_hash_str
+
+ defp abi_decode_address_output(address) when is_binary(address) do
+ if String.length(address) > 42 do
+ "0x" <> String.slice(address, -40, 40)
+ else
+ address
+ end
+ end
+
+ defp abi_decode_address_output(_), do: nil
end
diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex
index e203f7ada899..dc5cf7eb9ca1 100644
--- a/apps/explorer/lib/explorer/chain/token_transfer.ex
+++ b/apps/explorer/lib/explorer/chain/token_transfer.ex
@@ -25,7 +25,7 @@ defmodule Explorer.Chain.TokenTransfer do
use Explorer.Schema
import Ecto.Changeset
- import Ecto.Query, only: [from: 2, limit: 2, where: 3]
+ import Ecto.Query, only: [from: 2, limit: 2, where: 3, join: 5, order_by: 3, preload: 3]
alias Explorer.Chain.{Address, Block, Hash, TokenTransfer, Transaction}
alias Explorer.Chain.Token.Instance
@@ -247,6 +247,16 @@ defmodule Explorer.Chain.TokenTransfer do
)
end
+ def handle_paging_options(query, nil), do: query
+
+ def handle_paging_options(query, %PagingOptions{key: nil, page_size: nil}), do: query
+
+ def handle_paging_options(query, paging_options) do
+ query
+ |> page_token_transfer(paging_options)
+ |> limit(^paging_options.page_size)
+ end
+
@doc """
Fetches the transaction hashes from token transfers according
to the address hash.
@@ -310,4 +320,36 @@ defmodule Explorer.Chain.TokenTransfer do
tt.block_number < ^block_number
)
end
+
+ def token_transfers_by_address_hash(direction, address_hash, token_types) do
+ TokenTransfer
+ |> filter_by_direction(direction, address_hash)
+ |> order_by([tt], desc: tt.block_number, desc: tt.log_index)
+ |> join(:inner, [tt], token in assoc(tt, :token), as: :token)
+ |> preload([token: token], [{:token, token}])
+ |> filter_by_type(token_types)
+ end
+
+ def filter_by_direction(query, :to, address_hash) do
+ query
+ |> where([tt], tt.to_address_hash == ^address_hash)
+ end
+
+ def filter_by_direction(query, :from, address_hash) do
+ query
+ |> where([tt], tt.from_address_hash == ^address_hash)
+ end
+
+ def filter_by_direction(query, _, address_hash) do
+ query
+ |> where([tt], tt.from_address_hash == ^address_hash or tt.to_address_hash == ^address_hash)
+ end
+
+ def filter_by_type(query, []), do: query
+
+ def filter_by_type(query, token_types) when is_list(token_types) do
+ where(query, [token: token], token.type in ^token_types)
+ end
+
+ def filter_by_type(query, _), do: query
end
diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex
index eff752277a86..c170bb9475e1 100644
--- a/apps/explorer/lib/explorer/chain/transaction.ex
+++ b/apps/explorer/lib/explorer/chain/transaction.ex
@@ -23,6 +23,7 @@ defmodule Explorer.Chain.Transaction do
Hash,
InternalTransaction,
Log,
+ SmartContract,
Token,
TokenTransfer,
Transaction,
@@ -521,7 +522,7 @@ defmodule Explorer.Chain.Transaction do
candidates_query
|> Repo.all()
|> Enum.flat_map(fn candidate ->
- case do_decoded_input_data(data, [candidate.abi], nil, hash) do
+ case do_decoded_input_data(data, %SmartContract{abi: [candidate.abi], address_hash: nil}, hash) do
{:ok, _, _, _} = decoded -> [decoded]
_ -> []
end
@@ -536,11 +537,11 @@ defmodule Explorer.Chain.Transaction do
def decoded_input_data(%__MODULE__{
input: %{bytes: data},
- to_address: %{smart_contract: %{abi: abi, address_hash: address_hash}},
+ to_address: %{smart_contract: smart_contract},
hash: hash
}) do
- case do_decoded_input_data(data, abi, address_hash, hash) do
- # In some cases transactions use methods of some unpredictable contracts, so we can try to look up for method in a whole DB
+ case do_decoded_input_data(data, smart_contract, hash) do
+ # In some cases transactions use methods of some unpredictadle contracts, so we can try to look up for method in a whole DB
{:error, :could_not_decode} ->
case decoded_input_data(%__MODULE__{
to_address: %{smart_contract: nil},
@@ -562,8 +563,8 @@ defmodule Explorer.Chain.Transaction do
end
end
- defp do_decoded_input_data(data, abi, address_hash, hash) do
- full_abi = Chain.combine_proxy_implementation_abi(address_hash, abi)
+ defp do_decoded_input_data(data, smart_contract, hash) do
+ full_abi = Chain.combine_proxy_implementation_abi(smart_contract)
with {:ok, {selector, values}} <- find_and_decode(full_abi, data, hash),
{:ok, mapping} <- selector_mapping(selector, values, hash),
diff --git a/apps/explorer/lib/explorer/etherscan/contracts.ex b/apps/explorer/lib/explorer/etherscan/contracts.ex
index 089ef7538f83..253d4bde9118 100644
--- a/apps/explorer/lib/explorer/etherscan/contracts.ex
+++ b/apps/explorer/lib/explorer/etherscan/contracts.ex
@@ -74,13 +74,13 @@ defmodule Explorer.Etherscan.Contracts do
def append_proxy_info(%Address{smart_contract: smart_contract} = address) when not is_nil(smart_contract) do
updated_smart_contract =
- if Chain.proxy_contract?(address.hash, smart_contract.abi) do
+ if SmartContract.proxy_contract?(smart_contract) do
smart_contract
|> Map.put(:is_proxy, true)
|> Map.put(
:implementation_address_hash_string,
- address.hash
- |> Chain.get_implementation_address_hash(smart_contract.abi)
+ smart_contract
+ |> SmartContract.get_implementation_address_hash()
|> Tuple.to_list()
|> List.first()
)
diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex
index 3ef71da72e4f..68cff935e03e 100644
--- a/apps/explorer/lib/explorer/smart_contract/reader.ex
+++ b/apps/explorer/lib/explorer/smart_contract/reader.ex
@@ -565,15 +565,12 @@ defmodule Explorer.SmartContract.Reader do
end
defp get_abi(contract_address_hash, type) do
- abi =
- contract_address_hash
- |> Chain.address_hash_to_smart_contract()
- |> Map.get(:abi)
+ contract = Chain.address_hash_to_smart_contract(contract_address_hash)
if type == :proxy do
- Chain.get_implementation_abi_from_proxy(contract_address_hash, abi)
+ Chain.get_implementation_abi_from_proxy(contract)
else
- abi
+ contract.abi
end
end
diff --git a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex
index e54fcddd7b62..34a2eab02dcf 100644
--- a/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex
+++ b/apps/explorer/lib/explorer/smart_contract/rust_verifier_interface.ex
@@ -2,6 +2,7 @@ defmodule Explorer.SmartContract.RustVerifierInterface do
@moduledoc """
Adapter for contracts verification with https://github.com/blockscout/blockscout-rs/blob/main/smart-contract-verifier
"""
+ alias Explorer.Utility.RustService
alias HTTPoison.Response
require Logger
@@ -47,7 +48,7 @@ defmodule Explorer.SmartContract.RustVerifierInterface do
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
+ case HTTPoison.post(url, Jason.encode!(normalize_creation_bytecode(body)), headers, recv_timeout: @post_timeout) do
{:ok, %Response{body: body, status_code: 200}} ->
proccess_verifier_response(body)
@@ -124,6 +125,10 @@ defmodule Explorer.SmartContract.RustVerifierInterface do
def proccess_verifier_response(other), do: {:error, other}
+ def normalize_creation_bytecode(%{"creation_bytecode" => ""} = map), do: Map.replace(map, "creation_bytecode", nil)
+
+ def normalize_creation_bytecode(map), do: map
+
def multiple_files_verification_url, do: "#{base_api_url()}" <> "/solidity/verify/multiple-files"
def vyper_multiple_files_verification_url, do: "#{base_api_url()}" <> "/vyper/verify/multiple-files"
@@ -137,14 +142,7 @@ defmodule Explorer.SmartContract.RustVerifierInterface do
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
+ RustService.base_url(__MODULE__)
end
def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled]
diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex
index c7ea1dd25e4e..0cb5cb46407b 100644
--- a/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex
+++ b/apps/explorer/lib/explorer/smart_contract/solidity/publisher.ex
@@ -52,8 +52,9 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
|> Map.put("contract_source_code", contract_source_code)
|> Map.put("external_libraries", contract_libraries)
|> Map.put("name", contract_name)
+ |> cast_compiler_settings(false)
- publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string))
+ publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string || "null"))
{:ok, %{abi: abi, constructor_arguments: constructor_arguments}} ->
params_with_constructor_arguments =
@@ -90,7 +91,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
"optimization_runs" => _,
"sources" => _
} = result_params} ->
- proccess_rust_verifier_response(result_params, address_hash)
+ proccess_rust_verifier_response(result_params, address_hash, true)
{:ok, %{abi: abi, constructor_arguments: constructor_arguments}, additional_params} ->
params_with_constructor_arguments =
@@ -155,7 +156,8 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
"optimization_runs" => _,
"sources" => sources
} = result_params,
- address_hash
+ address_hash,
+ is_standard_json? \\ false
) do
secondary_sources =
for {file, source} <- sources,
@@ -171,10 +173,23 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
|> Map.put("name", contract_name)
|> Map.put("file_path", file_name)
|> Map.put("secondary_sources", secondary_sources)
+ |> cast_compiler_settings(is_standard_json?)
publish_smart_contract(address_hash, prepared_params, Jason.decode!(abi_string))
end
+ def cast_compiler_settings(params, false), do: Map.put(params, "compiler_settings", nil)
+
+ def cast_compiler_settings(params, true) do
+ case Jason.decode(params["compiler_settings"]) do
+ {:ok, map} ->
+ Map.put(params, "compiler_settings", map)
+
+ _ ->
+ Map.put(params, "compiler_settings", nil)
+ end
+ end
+
def publish_smart_contract(address_hash, params, abi) do
attrs = address_hash |> attributes(params, abi)
@@ -232,6 +247,7 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
defp attributes(address_hash, params, abi \\ %{}) do
constructor_arguments = params["constructor_arguments"]
+ compiler_settings = params["compiler_settings"]
clean_constructor_arguments =
if constructor_arguments != nil && constructor_arguments != "" do
@@ -240,6 +256,13 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
nil
end
+ clean_compiler_settings =
+ if compiler_settings in ["", nil, %{}] do
+ nil
+ else
+ compiler_settings
+ end
+
prepared_external_libraries = prepare_external_libraies(params["external_libraries"])
compiler_version = CompilerVersion.get_strict_compiler_version(:solc, params["compiler_version"])
@@ -261,7 +284,9 @@ defmodule Explorer.SmartContract.Solidity.Publisher do
verified_via_sourcify: params["verified_via_sourcify"],
partially_verified: params["partially_verified"],
is_vyper_contract: false,
- autodetect_constructor_args: params["autodetect_constructor_args"]
+ autodetect_constructor_args: params["autodetect_constructor_args"],
+ is_yul: params["is_yul"] || false,
+ compiler_settings: clean_compiler_settings
}
end
diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex
index bf8e9e6780f6..eb849469ac58 100644
--- a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex
+++ b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex
@@ -51,7 +51,10 @@ defmodule Explorer.SmartContract.Solidity.Verifier do
params
|> Map.put("creation_bytecode", creation_tx_input)
|> Map.put("deployed_bytecode", deployed_bytecode)
- |> Map.put("sources", %{"#{params["name"]}.sol" => params["contract_source_code"]})
+ |> Map.put("sources", %{
+ "#{params["name"]}.#{smart_contract_source_file_extension(parse_boolean(params["is_yul"]))}" =>
+ params["contract_source_code"]
+ })
|> Map.put("contract_libraries", params["external_libraries"])
|> Map.put("optimization_runs", prepare_optimization_runs(params["optimization"], params["optimization_runs"]))
|> RustVerifierInterface.verify_multi_part()
@@ -81,6 +84,9 @@ defmodule Explorer.SmartContract.Solidity.Verifier do
end)
end
+ defp smart_contract_source_file_extension(true), do: "yul"
+ defp smart_contract_source_file_extension(_), do: "sol"
+
defp prepare_optimization_runs(false_, _) when false_ in [false, "false"], do: nil
defp prepare_optimization_runs(true_, runs) when true_ in [true, "true"] do
@@ -199,6 +205,7 @@ defmodule Explorer.SmartContract.Solidity.Verifier do
|> Map.put("file_path", file_path)
|> Map.put("name", contract_name)
|> Map.put("secondary_sources", secondary_sources)
+ |> Map.put("compiler_settings", map_json_input["settings"])
{:halt, {:ok, verified_data, additional_params}}
diff --git a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex
index b33870f3be50..9e1c7f40c211 100644
--- a/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex
+++ b/apps/explorer/lib/explorer/third_party_integrations/sourcify.ex
@@ -367,6 +367,7 @@ defmodule Explorer.ThirdPartyIntegrations.Sourcify do
|> Map.put("optimization_runs", Map.get(optimizer, "runs"))
|> Map.put("external_libraries", Map.get(settings, "libraries"))
|> Map.put("verified_via_sourcify", true)
+ |> Map.put("compiler_settings", settings)
%{
"params_to_publish" => params,
diff --git a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
index 753b13b08b8b..07da5222d35d 100644
--- a/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
+++ b/apps/explorer/lib/explorer/token/instance_metadata_retriever.ex
@@ -172,7 +172,7 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
def fetch_json(%{@uri => {:ok, ["data:application/json," <> json]}}, hex_token_id) do
decoded_json = URI.decode(json)
- fetch_json(%{@token_uri => {:ok, [decoded_json]}}, hex_token_id)
+ fetch_json(%{@uri => {:ok, [decoded_json]}}, hex_token_id)
rescue
e ->
Logger.debug(["Unknown metadata format #{inspect(json)}. error #{inspect(e)}"],
@@ -182,6 +182,40 @@ defmodule Explorer.Token.InstanceMetadataRetriever do
{:error, json}
end
+ def fetch_json(%{@token_uri => {:ok, ["data:application/json;base64," <> base64_encoded_json]}}, hex_token_id) do
+ case Base.url_decode64(base64_encoded_json) do
+ {:ok, base64_decoded} ->
+ fetch_json(%{@token_uri => {:ok, [base64_decoded]}}, hex_token_id)
+
+ _ ->
+ {:error, base64_encoded_json}
+ end
+ rescue
+ e ->
+ Logger.debug(["Unknown metadata format base64 #{inspect(base64_encoded_json)}. error #{inspect(e)}"],
+ fetcher: :token_instances
+ )
+
+ {:error, base64_encoded_json}
+ end
+
+ def fetch_json(%{@uri => {:ok, ["data:application/json;base64," <> base64_encoded_json]}}, hex_token_id) do
+ case Base.url_decode64(base64_encoded_json) do
+ {:ok, base64_decoded} ->
+ fetch_json(%{@uri => {:ok, [base64_decoded]}}, hex_token_id)
+
+ _ ->
+ {:error, base64_encoded_json}
+ end
+ rescue
+ e ->
+ Logger.debug(["Unknown metadata format base64 #{inspect(base64_encoded_json)}. error #{inspect(e)}"],
+ fetcher: :token_instances
+ )
+
+ {:error, base64_encoded_json}
+ end
+
def fetch_json(%{@token_uri => {:ok, ["ipfs://ipfs/" <> ipfs_uid]}}, hex_token_id) do
ipfs_url = "https://ipfs.io/ipfs/" <> ipfs_uid
fetch_metadata_inner(ipfs_url, hex_token_id)
diff --git a/apps/explorer/lib/explorer/token/metadata_retriever.ex b/apps/explorer/lib/explorer/token/metadata_retriever.ex
index 304b9330b68d..29b744d708a0 100644
--- a/apps/explorer/lib/explorer/token/metadata_retriever.ex
+++ b/apps/explorer/lib/explorer/token/metadata_retriever.ex
@@ -342,7 +342,10 @@ defmodule Explorer.Token.MetadataRetriever do
defp handle_large_string(nil), do: nil
defp handle_large_string(string), do: handle_large_string(string, byte_size(string))
- defp handle_large_string(string, size) when size > 255, do: binary_part(string, 0, 255)
+
+ defp handle_large_string(string, size) when size > 255,
+ do: string |> binary_part(0, 255) |> String.chunk(:valid) |> List.first()
+
defp handle_large_string(string, _size), do: string
defp remove_null_bytes(string) do
diff --git a/apps/explorer/lib/explorer/utility/rust_service.ex b/apps/explorer/lib/explorer/utility/rust_service.ex
new file mode 100644
index 000000000000..63f949961252
--- /dev/null
+++ b/apps/explorer/lib/explorer/utility/rust_service.ex
@@ -0,0 +1,15 @@
+defmodule Explorer.Utility.RustService do
+ @moduledoc """
+ Module is responsible for common utils related to rust microservices.
+ """
+ def base_url(module) 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
+end
diff --git a/apps/explorer/lib/explorer/visualize/sol2uml.ex b/apps/explorer/lib/explorer/visualize/sol2uml.ex
new file mode 100644
index 000000000000..b507c63a2e10
--- /dev/null
+++ b/apps/explorer/lib/explorer/visualize/sol2uml.ex
@@ -0,0 +1,68 @@
+defmodule Explorer.Visualize.Sol2uml do
+ @moduledoc """
+ Adapter for sol2uml visualizer with https://github.com/blockscout/blockscout-rs/blob/main/visualizer
+ """
+ alias Explorer.Utility.RustService
+ alias HTTPoison.Response
+ require Logger
+
+ @post_timeout 60_000
+ @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: status_code}} ->
+ Logger.error(fn -> ["Invalid status code from visualizer: #{status_code}. body: ", inspect(body)] end)
+ {:error, "failed to visualize contract"}
+
+ {: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:visualize-contracts"
+
+ def base_api_url, do: "#{base_url()}" <> "/api/v1"
+
+ def base_url do
+ RustService.base_url(__MODULE__)
+ end
+
+ def enabled?, do: Application.get_env(:explorer, __MODULE__)[:enabled]
+end
diff --git a/apps/explorer/priv/repo/migrations/20220527131249_add_implementation_fields.exs b/apps/explorer/priv/repo/migrations/20220527131249_add_implementation_fields.exs
new file mode 100644
index 000000000000..5f3839dd4a4c
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20220527131249_add_implementation_fields.exs
@@ -0,0 +1,10 @@
+defmodule Explorer.Repo.Migrations.AddImplementationFields do
+ use Ecto.Migration
+
+ def change do
+ alter table(:smart_contracts) do
+ add(:implementation_address_hash, :bytea, null: true)
+ add(:implementation_fetched_at, :"timestamp without time zone", null: true)
+ end
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs b/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs
index 24244fd249a7..95ad38588c15 100644
--- a/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs
+++ b/apps/explorer/priv/repo/migrations/20220919105140_add_method_id_index.exs
@@ -5,11 +5,11 @@ defmodule Explorer.Repo.Migrations.AddMethodIdIndex do
def up do
execute("""
- CREATE INDEX CONCURRENTLY method_id ON public.transactions USING btree (substring(input for 4));
+ CREATE INDEX CONCURRENTLY IF NOT EXISTS method_id ON public.transactions USING btree (substring(input for 4));
""")
end
def down do
- execute("DROP INDEX method_id")
+ execute("DROP INDEX IF EXISTS method_id")
end
end
diff --git a/apps/explorer/priv/repo/migrations/20221114113853_remove_not_null_constraint_from_abi.exs b/apps/explorer/priv/repo/migrations/20221114113853_remove_not_null_constraint_from_abi.exs
new file mode 100644
index 000000000000..81903e79e30c
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20221114113853_remove_not_null_constraint_from_abi.exs
@@ -0,0 +1,7 @@
+defmodule Explorer.Repo.Migrations.RemoveNotNullConstraintFromAbi do
+ use Ecto.Migration
+
+ def change do
+ execute("ALTER TABLE smart_contracts ALTER COLUMN abi DROP NOT NULL;")
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20221117075456_modify_address_token_balances_indexes.exs b/apps/explorer/priv/repo/migrations/20221117075456_modify_address_token_balances_indexes.exs
new file mode 100644
index 000000000000..9c21c4b40a89
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20221117075456_modify_address_token_balances_indexes.exs
@@ -0,0 +1,58 @@
+defmodule Explorer.Repo.Migrations.ModifyAddressTokenBalancesIndexes do
+ use Ecto.Migration
+
+ def change do
+ drop_if_exists(
+ unique_index(
+ :address_token_balances,
+ ~w(address_hash token_contract_address_hash block_number)a,
+ name: :fetched_token_balances,
+ where: "token_id IS NULL"
+ )
+ )
+
+ drop_if_exists(
+ unique_index(
+ :address_token_balances,
+ ~w(address_hash token_contract_address_hash token_id block_number)a,
+ name: :fetched_token_balances_with_token_id,
+ where: "token_id IS NOT NULL"
+ )
+ )
+
+ create_if_not_exists(
+ unique_index(
+ :address_token_balances,
+ [:address_hash, :token_contract_address_hash, "COALESCE(token_id, -1)", :block_number],
+ name: :fetched_token_balances
+ )
+ )
+
+ drop_if_exists(
+ unique_index(
+ :address_token_balances,
+ ~w(address_hash token_contract_address_hash block_number)a,
+ name: :unfetched_token_balances,
+ where: "value_fetched_at IS NULL and token_id IS NULL"
+ )
+ )
+
+ drop_if_exists(
+ unique_index(
+ :address_token_balances,
+ ~w(address_hash token_contract_address_hash token_id block_number)a,
+ name: :unfetched_token_balances_with_token_id,
+ where: "value_fetched_at IS NULL and token_id IS NOT NULL"
+ )
+ )
+
+ create_if_not_exists(
+ unique_index(
+ :address_token_balances,
+ [:address_hash, :token_contract_address_hash, "COALESCE(token_id, -1)", :block_number],
+ name: :unfetched_token_balances,
+ where: "value_fetched_at IS NULL"
+ )
+ )
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20221117080657_modify_address_current_token_balances_indexes.exs b/apps/explorer/priv/repo/migrations/20221117080657_modify_address_current_token_balances_indexes.exs
new file mode 100644
index 000000000000..295a7a5c3f15
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20221117080657_modify_address_current_token_balances_indexes.exs
@@ -0,0 +1,31 @@
+defmodule Explorer.Repo.Migrations.ModifyAddressCurrentTokenBalancesIndexes do
+ use Ecto.Migration
+
+ def change do
+ drop_if_exists(
+ unique_index(
+ :address_current_token_balances,
+ ~w(address_hash token_contract_address_hash)a,
+ name: :fetched_current_token_balances,
+ where: "token_id IS NULL"
+ )
+ )
+
+ drop_if_exists(
+ unique_index(
+ :address_current_token_balances,
+ ~w(address_hash token_contract_address_hash token_id)a,
+ name: :fetched_current_token_balances_with_token_id,
+ where: "token_id IS NOT NULL"
+ )
+ )
+
+ create_if_not_exists(
+ unique_index(
+ :address_current_token_balances,
+ [:address_hash, :token_contract_address_hash, "COALESCE(token_id, -1)"],
+ name: :fetched_current_token_balances
+ )
+ )
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20221120184715_add_json_compiler_settings.exs b/apps/explorer/priv/repo/migrations/20221120184715_add_json_compiler_settings.exs
new file mode 100644
index 000000000000..e6f90d78648a
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20221120184715_add_json_compiler_settings.exs
@@ -0,0 +1,9 @@
+defmodule Explorer.Repo.Migrations.AddJsonCompilerSettings do
+ use Ecto.Migration
+
+ def change do
+ alter table(:smart_contracts) do
+ add(:compiler_settings, :jsonb, null: true)
+ end
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20221126103223_add_txs_indexes.exs b/apps/explorer/priv/repo/migrations/20221126103223_add_txs_indexes.exs
new file mode 100644
index 000000000000..241ed059688c
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20221126103223_add_txs_indexes.exs
@@ -0,0 +1,73 @@
+defmodule Explorer.Repo.Migrations.AddTxsIndexes do
+ use Ecto.Migration
+ @disable_ddl_transaction true
+ @disable_migration_lock true
+
+ def change do
+ drop_if_exists(
+ index(
+ :transactions,
+ [:from_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash],
+ name: "transactions_from_address_hash_recent_collated_index",
+ concurrently: true
+ )
+ )
+
+ drop_if_exists(
+ index(
+ :transactions,
+ [:to_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash],
+ name: "transactions_to_address_hash_recent_collated_index",
+ concurrently: true
+ )
+ )
+
+ drop_if_exists(
+ index(
+ :transactions,
+ [:created_contract_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", :hash],
+ name: "transactions_created_contract_address_hash_recent_collated_index",
+ concurrently: true
+ )
+ )
+
+ create_if_not_exists(
+ index(
+ :transactions,
+ [
+ :from_address_hash,
+ "block_number DESC NULLS FIRST",
+ "index DESC NULLS FIRST",
+ "inserted_at DESC",
+ "hash ASC"
+ ],
+ name: "transactions_from_address_hash_with_pending_index",
+ concurrently: true
+ )
+ )
+
+ create_if_not_exists(
+ index(
+ :transactions,
+ [:to_address_hash, "block_number DESC NULLS FIRST", "index DESC NULLS FIRST", "inserted_at DESC", "hash ASC"],
+ name: "transactions_to_address_hash_with_pending_index",
+ concurrently: true
+ )
+ )
+
+ create_if_not_exists(
+ index(
+ :transactions,
+ [
+ :created_contract_address_hash,
+ "block_number DESC NULLS FIRST",
+ "index DESC NULLS FIRST",
+ "inserted_at DESC",
+ "hash ASC"
+ ],
+ name: "transactions_created_contract_address_hash_with_pending_index",
+ concurrently: true
+ )
+ )
+ end
+end
diff --git a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs
index 7c34c72719cd..31eba0553de0 100644
--- a/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs
+++ b/apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs
@@ -97,30 +97,30 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
- token_contract_address_hash: ^token_erc_20_contract_address_hash,
- value: ^value_3,
- token_id: ^token_id_3
+ token_contract_address_hash: ^token_contract_address_hash,
+ value: ^value_1,
+ token_id: ^token_id_1
},
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
- token_contract_address_hash: ^token_erc_721_contract_address_hash,
- value: ^value_5,
- token_id: nil
+ token_contract_address_hash: ^token_contract_address_hash,
+ value: ^value_2,
+ token_id: ^token_id_2
},
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
- token_contract_address_hash: ^token_contract_address_hash,
- value: ^value_1,
- token_id: ^token_id_1
+ token_contract_address_hash: ^token_erc_20_contract_address_hash,
+ value: ^value_3,
+ token_id: ^token_id_3
},
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
- token_contract_address_hash: ^token_contract_address_hash,
- value: ^value_2,
- token_id: ^token_id_2
+ token_contract_address_hash: ^token_erc_721_contract_address_hash,
+ value: ^value_5,
+ token_id: nil
}
],
address_current_token_balances_update_token_holder_counts: [
@@ -172,7 +172,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
block_number: block_number,
token_contract_address_hash: token_erc_721.contract_address_hash,
value: value_4,
- value_fetched_at: DateTime.utc_now(),
+ value_fetched_at: DateTime.add(DateTime.utc_now(), -1),
token_id: token_id_4,
token_type: "ERC-721"
},
diff --git a/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs b/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs
index f44b6bca792f..15725869bd85 100644
--- a/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs
+++ b/apps/explorer/test/explorer/chain/import/runner/address/token_balances_test.exs
@@ -41,6 +41,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
+ token_id: nil,
value: ^value,
value_fetched_at: ^value_fetched_at
}
@@ -83,6 +84,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do
address_hash: address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
+ token_id: nil,
value: nil,
value_fetched_at: ^value_fetched_at
}
@@ -153,6 +155,70 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalancesTest do
run_changes(new_changes, options)
end
+ test "set value_fetched_at to null for existing record if incoming data has this field empty" do
+ address = insert(:address)
+ token = insert(:token)
+
+ options = %{
+ timeout: :infinity,
+ timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}
+ }
+
+ block_number = 1
+
+ value = Decimal.new(100)
+ value_fetched_at = DateTime.utc_now()
+
+ token_contract_address_hash = token.contract_address_hash
+ address_hash = address.hash
+
+ first_changes = %{
+ address_hash: address_hash,
+ block_number: block_number,
+ token_contract_address_hash: token_contract_address_hash,
+ token_id: 11,
+ token_type: "ERC-721",
+ value: value,
+ value_fetched_at: value_fetched_at
+ }
+
+ assert {:ok,
+ %{
+ address_token_balances: [
+ %TokenBalance{
+ address_hash: address_hash,
+ block_number: ^block_number,
+ token_contract_address_hash: ^token_contract_address_hash,
+ token_id: nil,
+ value: ^value,
+ value_fetched_at: ^value_fetched_at
+ }
+ ]
+ }} = run_changes(first_changes, options)
+
+ second_changes = %{
+ address_hash: address_hash,
+ block_number: block_number,
+ token_contract_address_hash: token_contract_address_hash,
+ token_id: 12,
+ token_type: "ERC-721"
+ }
+
+ assert {:ok,
+ %{
+ address_token_balances: [
+ %TokenBalance{
+ address_hash: address_hash,
+ block_number: ^block_number,
+ token_contract_address_hash: ^token_contract_address_hash,
+ token_id: nil,
+ value: ^value,
+ value_fetched_at: nil
+ }
+ ]
+ }} = run_changes(second_changes, options)
+ end
+
defp run_changes(changes, options) when is_map(changes) do
run_changes_list([changes], options)
end
diff --git a/apps/explorer/test/explorer/chain/smart_contract_test.exs b/apps/explorer/test/explorer/chain/smart_contract_test.exs
new file mode 100644
index 000000000000..38ff09e3b1e3
--- /dev/null
+++ b/apps/explorer/test/explorer/chain/smart_contract_test.exs
@@ -0,0 +1,237 @@
+defmodule Explorer.Chain.SmartContractTest do
+ use Explorer.DataCase, async: false
+
+ import Mox
+ alias Explorer.Chain
+ alias Explorer.Chain.SmartContract
+
+ doctest Explorer.Chain.SmartContract
+
+ setup :verify_on_exit!
+ setup :set_mox_global
+
+ describe "test fetching implementation" do
+ test "check proxy_contract/1 function" do
+ smart_contract = insert(:smart_contract)
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20))
+ Application.put_env(:explorer, :implementation_data_fetching_timeout, :timer.seconds(20))
+
+ refute smart_contract.implementation_fetched_at
+
+ # fetch nil implementation and save it to db
+ get_eip1967_implementation_zero_addresses()
+ refute SmartContract.proxy_contract?(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+ assert_empty_implementation(smart_contract.address_hash)
+ # extract proxy info from db
+ refute SmartContract.proxy_contract?(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+ assert_empty_implementation(smart_contract.address_hash)
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0)
+
+ get_eip1967_implementation_error_response()
+ refute SmartContract.proxy_contract?(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+
+ get_eip1967_implementation_non_zero_address()
+ assert SmartContract.proxy_contract?(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+ assert_implementation_address(smart_contract.address_hash)
+
+ get_eip1967_implementation_non_zero_address()
+ assert SmartContract.proxy_contract?(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+ assert_implementation_address(smart_contract.address_hash)
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20))
+ assert SmartContract.proxy_contract?(smart_contract)
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0)
+ get_eip1967_implementation_non_zero_address()
+ assert SmartContract.proxy_contract?(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+
+ get_eip1967_implementation_error_response()
+ assert SmartContract.proxy_contract?(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+ end
+
+ test "test get_implementation_adddress_hash/1" do
+ smart_contract = insert(:smart_contract)
+ implementation_smart_contract = insert(:smart_contract, name: "proxy")
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20))
+ Application.put_env(:explorer, :implementation_data_fetching_timeout, :timer.seconds(20))
+
+ refute smart_contract.implementation_fetched_at
+
+ # fetch nil implementation and save it to db
+ get_eip1967_implementation_zero_addresses()
+ assert {nil, nil} = SmartContract.get_implementation_address_hash(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+ assert_empty_implementation(smart_contract.address_hash)
+
+ # extract proxy info from db
+ assert {nil, nil} = SmartContract.get_implementation_address_hash(smart_contract)
+ assert_empty_implementation(smart_contract.address_hash)
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0)
+
+ string_implementation_address_hash = to_string(implementation_smart_contract.address_hash)
+
+ expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
+ id: 0,
+ method: "eth_getStorageAt",
+ params: [
+ _,
+ "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
+ "latest"
+ ]
+ },
+ _options ->
+ {:ok, string_implementation_address_hash}
+ end)
+
+ assert {^string_implementation_address_hash, "proxy"} =
+ SmartContract.get_implementation_address_hash(smart_contract)
+
+ verify!(EthereumJSONRPC.Mox)
+
+ assert_exact_name_and_address(
+ smart_contract.address_hash,
+ implementation_smart_contract.address_hash,
+ implementation_smart_contract.name
+ )
+
+ get_eip1967_implementation_error_response()
+
+ assert {^string_implementation_address_hash, "proxy"} =
+ SmartContract.get_implementation_address_hash(smart_contract)
+
+ verify!(EthereumJSONRPC.Mox)
+
+ assert_exact_name_and_address(
+ smart_contract.address_hash,
+ implementation_smart_contract.address_hash,
+ implementation_smart_contract.name
+ )
+
+ contract_1 = Chain.address_hash_to_smart_contract(smart_contract.address_hash)
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, :timer.seconds(20))
+
+ assert {^string_implementation_address_hash, "proxy"} =
+ SmartContract.get_implementation_address_hash(smart_contract)
+
+ contract_2 = Chain.address_hash_to_smart_contract(smart_contract.address_hash)
+
+ assert contract_1.implementation_fetched_at == contract_2.implementation_fetched_at &&
+ contract_1.updated_at == contract_2.updated_at
+
+ Application.put_env(:explorer, :fallback_ttl_cached_implementation_data_of_proxy, 0)
+ get_eip1967_implementation_zero_addresses()
+ assert {nil, nil} = SmartContract.get_implementation_address_hash(smart_contract)
+ verify!(EthereumJSONRPC.Mox)
+ assert_empty_implementation(smart_contract.address_hash)
+ end
+ end
+
+ def get_eip1967_implementation_zero_addresses do
+ EthereumJSONRPC.Mox
+ |> expect(:json_rpc, fn %{
+ id: 0,
+ method: "eth_getStorageAt",
+ params: [
+ _,
+ "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
+ "latest"
+ ]
+ },
+ _options ->
+ {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"}
+ end)
+ |> expect(:json_rpc, fn %{
+ id: 0,
+ method: "eth_getStorageAt",
+ params: [
+ _,
+ "0xa3f0ad74e5423aebfd80d3ef4346578335a9a72aeaee59ff6cb3582b35133d50",
+ "latest"
+ ]
+ },
+ _options ->
+ {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"}
+ end)
+ |> expect(:json_rpc, fn %{
+ id: 0,
+ method: "eth_getStorageAt",
+ params: [
+ _,
+ "0x7050c9e0f4ca769c69bd3a8ef740bc37934f8e2c036e5a723fd8ee048ed3f8c3",
+ "latest"
+ ]
+ },
+ _options ->
+ {:ok, "0x0000000000000000000000000000000000000000000000000000000000000000"}
+ end)
+ end
+
+ def get_eip1967_implementation_non_zero_address do
+ expect(EthereumJSONRPC.Mox, :json_rpc, fn %{
+ id: 0,
+ method: "eth_getStorageAt",
+ params: [
+ _,
+ "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
+ "latest"
+ ]
+ },
+ _options ->
+ {:ok, "0x0000000000000000000000000000000000000000000000000000000000000001"}
+ end)
+ end
+
+ def get_eip1967_implementation_error_response do
+ EthereumJSONRPC.Mox
+ |> expect(:json_rpc, fn %{
+ id: 0,
+ method: "eth_getStorageAt",
+ params: [
+ _,
+ "0x360894a13ba1a3210667c828492db98dca3e2076cc3735a920a3ca505d382bbc",
+ "latest"
+ ]
+ },
+ _options ->
+ {:error, "error"}
+ end)
+ end
+
+ def assert_empty_implementation(address_hash) do
+ contract = Chain.address_hash_to_smart_contract(address_hash)
+ assert contract.implementation_fetched_at
+ refute contract.implementation_name
+ refute contract.implementation_address_hash
+ end
+
+ def assert_implementation_address(address_hash) do
+ contract = Chain.address_hash_to_smart_contract(address_hash)
+ assert contract.implementation_fetched_at
+ assert contract.implementation_address_hash
+ end
+
+ def assert_implementation_name(address_hash) do
+ contract = Chain.address_hash_to_smart_contract(address_hash)
+ assert contract.implementation_fetched_at
+ assert contract.implementation_name
+ end
+
+ def assert_exact_name_and_address(address_hash, implementation_address_hash, implementation_name) do
+ contract = Chain.address_hash_to_smart_contract(address_hash)
+ assert contract.implementation_fetched_at
+ assert contract.implementation_name == implementation_name
+ assert to_string(contract.implementation_address_hash) == to_string(implementation_address_hash)
+ end
+end
diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs
index 9808527a8da4..e191c081b843 100644
--- a/apps/explorer/test/explorer/chain_test.exs
+++ b/apps/explorer/test/explorer/chain_test.exs
@@ -3546,7 +3546,7 @@ defmodule Explorer.ChainTest do
assert [
%TokenTransfer{
- token: %Ecto.Association.NotLoaded{},
+ token: %Token{},
transaction: %Ecto.Association.NotLoaded{}
}
] = Chain.transaction_to_token_transfers(transaction.hash)
@@ -6140,26 +6140,36 @@ defmodule Explorer.ChainTest do
test "combine_proxy_implementation_abi/2 returns empty [] abi if proxy abi is null" do
proxy_contract_address = insert(:contract_address)
- assert Chain.combine_proxy_implementation_abi(proxy_contract_address, nil) == []
+
+ assert Chain.combine_proxy_implementation_abi(%SmartContract{address_hash: proxy_contract_address.hash, abi: nil}) ==
+ []
end
test "combine_proxy_implementation_abi/2 returns [] abi for unverified proxy" do
proxy_contract_address = insert(:contract_address)
+ smart_contract =
+ insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123")
+
get_eip1967_implementation()
- assert Chain.combine_proxy_implementation_abi(proxy_contract_address, []) == []
+ assert Chain.combine_proxy_implementation_abi(smart_contract) == []
end
test "combine_proxy_implementation_abi/2 returns proxy abi if implementation is not verified" do
proxy_contract_address = insert(:contract_address)
- insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
- assert Chain.combine_proxy_implementation_abi(proxy_contract_address, @proxy_abi) == @proxy_abi
+
+ smart_contract =
+ insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
+
+ assert Chain.combine_proxy_implementation_abi(smart_contract) == @proxy_abi
end
test "combine_proxy_implementation_abi/2 returns proxy + implementation abi if implementation is verified" do
proxy_contract_address = insert(:contract_address)
- insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
+
+ smart_contract =
+ insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
implementation_contract_address = insert(:contract_address)
@@ -6187,7 +6197,7 @@ defmodule Explorer.ChainTest do
end
)
- combined_abi = Chain.combine_proxy_implementation_abi(proxy_contract_address.hash, @proxy_abi)
+ combined_abi = Chain.combine_proxy_implementation_abi(smart_contract)
assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 0) end) == false
assert Enum.any?(@proxy_abi, fn el -> el == Enum.at(@implementation_abi, 1) end) == false
@@ -6197,26 +6207,36 @@ defmodule Explorer.ChainTest do
test "get_implementation_abi_from_proxy/2 returns empty [] abi if proxy abi is null" do
proxy_contract_address = insert(:contract_address)
- assert Chain.get_implementation_abi_from_proxy(proxy_contract_address, nil) == []
+
+ assert Chain.get_implementation_abi_from_proxy(%SmartContract{address_hash: proxy_contract_address.hash, abi: nil}) ==
+ []
end
test "get_implementation_abi_from_proxy/2 returns [] abi for unverified proxy" do
proxy_contract_address = insert(:contract_address)
+ smart_contract =
+ insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123")
+
get_eip1967_implementation()
- assert Chain.combine_proxy_implementation_abi(proxy_contract_address, []) == []
+ assert Chain.combine_proxy_implementation_abi(smart_contract) == []
end
test "get_implementation_abi_from_proxy/2 returns [] if implementation is not verified" do
proxy_contract_address = insert(:contract_address)
- insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
- assert Chain.get_implementation_abi_from_proxy(proxy_contract_address, @proxy_abi) == []
+
+ smart_contract =
+ insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
+
+ assert Chain.get_implementation_abi_from_proxy(smart_contract) == []
end
test "get_implementation_abi_from_proxy/2 returns implementation abi if implementation is verified" do
proxy_contract_address = insert(:contract_address)
- insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
+
+ smart_contract =
+ insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: @proxy_abi, contract_code_md5: "123")
implementation_contract_address = insert(:contract_address)
@@ -6244,14 +6264,16 @@ defmodule Explorer.ChainTest do
end
)
- implementation_abi = Chain.get_implementation_abi_from_proxy(proxy_contract_address.hash, @proxy_abi)
+ implementation_abi = Chain.get_implementation_abi_from_proxy(smart_contract)
assert implementation_abi == @implementation_abi
end
test "get_implementation_abi_from_proxy/2 returns implementation abi in case of EIP-1967 proxy pattern" do
proxy_contract_address = insert(:contract_address)
- insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123")
+
+ smart_contract =
+ insert(:smart_contract, address_hash: proxy_contract_address.hash, abi: [], contract_code_md5: "123")
implementation_contract_address = insert(:contract_address)
@@ -6281,7 +6303,7 @@ defmodule Explorer.ChainTest do
end
)
- implementation_abi = Chain.get_implementation_abi_from_proxy(proxy_contract_address.hash, [])
+ implementation_abi = Chain.get_implementation_abi_from_proxy(smart_contract)
assert implementation_abi == @implementation_abi
end
diff --git a/apps/explorer/test/explorer/token/metadata_retriever_test.exs b/apps/explorer/test/explorer/token/metadata_retriever_test.exs
index 9f8ead7468d8..404ae713bb9f 100644
--- a/apps/explorer/test/explorer/token/metadata_retriever_test.exs
+++ b/apps/explorer/test/explorer/token/metadata_retriever_test.exs
@@ -340,6 +340,58 @@ defmodule Explorer.Token.MetadataRetrieverTest do
assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected
end
+ test "shortens strings larger than 255 characters with unicode graphemes" do
+ long_token_name_shortened =
+ "文章の論旨や要点を短くまとめて表現する要約文。学生の頃、レポート作成などで書いた経験があるものの、それ以降はまったく書いていないという人は多いことでしょう。 しかし、文章"
+
+ token = insert(:token, contract_address: build(:contract_address))
+
+ expect(
+ EthereumJSONRPC.Mox,
+ :json_rpc,
+ 1,
+ fn requests, _opts ->
+ {:ok,
+ Enum.map(requests, fn
+ %{id: id, method: "eth_call", params: [%{data: "0x313ce567", to: _}, "latest"]} ->
+ %{
+ id: id,
+ result: "0x0000000000000000000000000000000000000000000000000000000000000012"
+ }
+
+ %{id: id, method: "eth_call", params: [%{data: "0x06fdde03", to: _}, "latest"]} ->
+ %{
+ id: id,
+ result:
+ "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000128e69687e7aba0e381aee8ab96e697a8e38284e8a681e782b9e38292e79fade3818fe381bee381a8e38281e381a6e8a1a8e78fbee38199e3828be8a681e7b484e69687e38082e5ada6e7949fe381aee9a083e38081e383ace3839de383bce38388e4bd9ce68890e381aae381a9e381a7e69bb8e38184e3819fe7b58ce9a893e3818ce38182e3828be38282e381aee381aee38081e3819de3828ce4bba5e9998de381afe381bee381a3e3819fe3818fe69bb8e38184e381a6e38184e381aae38184e381a8e38184e38186e4babae381afe5a49ae38184e38193e381a8e381a7e38197e38287e38186e380822020e38197e3818be38197e38081e69687e7aba0e4bd9ce68890e3818ce88ba6e6898be381aae4babae38284e38081e69687e7aba0e3818ce3828fe3818b000000000000000000000000000000000000000000000000"
+ }
+
+ %{id: id, method: "eth_call", params: [%{data: "0x95d89b41", to: _}, "latest"]} ->
+ %{
+ id: id,
+ result:
+ "0x00000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000003424e540000000000000000000000000000000000000000000000000000000000"
+ }
+
+ %{id: id, method: "eth_call", params: [%{data: "0x18160ddd", to: _}, "latest"]} ->
+ %{
+ id: id,
+ result: "0x0000000000000000000000000000000000000000000000000de0b6b3a7640000"
+ }
+ end)}
+ end
+ )
+
+ expected = %{
+ name: long_token_name_shortened,
+ decimals: 18,
+ total_supply: 1_000_000_000_000_000_000,
+ symbol: "BNT"
+ }
+
+ assert MetadataRetriever.get_functions_of(token.contract_address_hash) == expected
+ end
+
test "retries when some function gave error" do
token = insert(:token, contract_address: build(:contract_address))
diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex
index e83166edd714..80a10f5811cb 100644
--- a/apps/explorer/test/support/factory.ex
+++ b/apps/explorer/test/support/factory.ex
@@ -180,6 +180,13 @@ defmodule Explorer.Factory do
}
end
+ def unique_address_name_factory do
+ %Address.Name{
+ address: build(:address),
+ name: sequence("FooContract")
+ }
+ end
+
def unfetched_balance_factory do
%CoinBalance{
address_hash: address_hash(),
@@ -667,6 +674,10 @@ defmodule Explorer.Factory do
}
end
+ def unique_token_factory do
+ Map.replace(token_factory(), :name, sequence("Infinite Token"))
+ end
+
def token_transfer_log_factory do
token_contract_address = build(:address)
to_address = build(:address)
@@ -835,6 +846,10 @@ defmodule Explorer.Factory do
}
end
+ def unique_smart_contract_factory do
+ Map.replace(smart_contract_factory(), :name, sequence("SimpleStorage"))
+ end
+
def decompiled_smart_contract_factory do
contract_code_info = contract_code_info()
@@ -865,6 +880,15 @@ defmodule Explorer.Factory do
}
end
+ def address_coin_balance_factory do
+ %CoinBalance{
+ address: insert(:address),
+ block_number: insert(:block).number,
+ value: Enum.random(1..100_000_000),
+ value_fetched_at: DateTime.utc_now()
+ }
+ end
+
def address_current_token_balance_factory do
%CurrentTokenBalance{
address: build(:address),
@@ -876,6 +900,17 @@ defmodule Explorer.Factory do
}
end
+ def address_current_token_balance_with_token_id_factory do
+ %CurrentTokenBalance{
+ address: build(:address),
+ token_contract_address_hash: insert(:token).contract_address_hash,
+ block_number: block_number(),
+ value: Enum.random(1..100_000),
+ value_fetched_at: DateTime.utc_now(),
+ token_id: Enum.random([nil, Enum.random(1..100_000)])
+ }
+ end
+
defp block_hash_to_next_transaction_index(block_hash) do
import Kernel, except: [+: 2]
diff --git a/apps/indexer/lib/indexer/fetcher/coin_balance.ex b/apps/indexer/lib/indexer/fetcher/coin_balance.ex
index 1fd0624e6720..3321b9016336 100644
--- a/apps/indexer/lib/indexer/fetcher/coin_balance.ex
+++ b/apps/indexer/lib/indexer/fetcher/coin_balance.ex
@@ -20,13 +20,8 @@ defmodule Indexer.Fetcher.CoinBalance do
use BufferedTask
- @defaults [
- flush_interval: :timer.seconds(3),
- max_batch_size: 500,
- max_concurrency: 4,
- task_supervisor: Indexer.Fetcher.CoinBalance.TaskSupervisor,
- metadata: [fetcher: :coin_balance]
- ]
+ @default_max_batch_size 500
+ @default_max_concurrency 4
@doc """
Asynchronously fetches balances for each address `hash` at the `block_number`.
@@ -56,7 +51,7 @@ defmodule Indexer.Fetcher.CoinBalance do
end
merged_init_options =
- @defaults
+ defaults()
|> Keyword.merge(mergeable_init_options)
|> Keyword.put(:state, state)
@@ -264,4 +259,14 @@ defmodule Indexer.Fetcher.CoinBalance do
end
end)
end
+
+ defp defaults do
+ [
+ flush_interval: :timer.seconds(3),
+ max_batch_size: Application.get_env(:indexer, __MODULE__)[:batch_size] || @default_max_batch_size,
+ max_concurrency: Application.get_env(:indexer, __MODULE__)[:concurrency] || @default_max_concurrency,
+ task_supervisor: Indexer.Fetcher.CoinBalance.TaskSupervisor,
+ metadata: [fetcher: :coin_balance]
+ ]
+ end
end
diff --git a/apps/indexer/lib/indexer/fetcher/token_instance.ex b/apps/indexer/lib/indexer/fetcher/token_instance.ex
index 1dbf68d87045..278506b91b13 100644
--- a/apps/indexer/lib/indexer/fetcher/token_instance.ex
+++ b/apps/indexer/lib/indexer/fetcher/token_instance.ex
@@ -54,7 +54,6 @@ defmodule Indexer.Fetcher.TokenInstance do
@impl BufferedTask
def run([%{contract_address_hash: hash, token_id: token_id}], _json_rpc_named_arguments) do
fetch_instance(hash, token_id)
- update_current_token_balances(hash, token_id)
:ok
end
@@ -99,58 +98,6 @@ defmodule Indexer.Fetcher.TokenInstance do
end
end
- defp update_current_token_balances(token_contract_address_hash, token_id) do
- token_id
- |> instance_owner_request(token_contract_address_hash)
- |> List.wrap()
- |> InstanceOwnerReader.get_owner_of()
- |> Enum.map(¤t_token_balances_import_params/1)
- |> all_import_params()
- |> Chain.import()
- end
-
- defp instance_owner_request(token_id, token_contract_address_hash) do
- %{
- token_contract_address_hash: to_string(token_contract_address_hash),
- token_id: Decimal.to_integer(token_id)
- }
- end
-
- defp current_token_balances_import_params(%{token_contract_address_hash: hash, token_id: token_id, owner: owner}) do
- %{
- value: Decimal.new(1),
- block_number: BlockNumber.get_max(),
- value_fetched_at: DateTime.utc_now(),
- token_id: token_id,
- token_type: Repo.get_by(Token, contract_address_hash: hash).type,
- address_hash: owner,
- token_contract_address_hash: hash
- }
- end
-
- defp all_import_params(balances_import_params) do
- addresses_import_params =
- balances_import_params
- |> Enum.reduce([], fn %{address_hash: address_hash}, acc ->
- case Repo.get_by(Address, hash: address_hash) do
- nil -> [%{hash: address_hash} | acc]
- _address -> acc
- end
- end)
- |> case do
- [] -> %{}
- params -> %{addresses: %{params: params}}
- end
-
- current_token_balances_import_params = %{
- address_current_token_balances: %{
- params: balances_import_params
- }
- }
-
- Map.merge(current_token_balances_import_params, addresses_import_params)
- end
-
@doc """
Fetches token instance data asynchronously.
"""
diff --git a/apps/indexer/test/indexer/fetcher/token_instance_test.exs b/apps/indexer/test/indexer/fetcher/token_instance_test.exs
deleted file mode 100644
index ade5894ac5a2..000000000000
--- a/apps/indexer/test/indexer/fetcher/token_instance_test.exs
+++ /dev/null
@@ -1,87 +0,0 @@
-defmodule Indexer.Fetcher.TokenInstanceTest do
- use EthereumJSONRPC.Case, async: false
- use Explorer.DataCase
-
- import Mox
-
- alias Explorer.Chain
- alias Explorer.Chain.Address
- alias Explorer.Chain.Address.CurrentTokenBalance
- alias Explorer.Repo
- alias Indexer.Fetcher.TokenInstance
-
- describe "run/2" do
- test "updates current token balance" do
- token = insert(:token, type: "ERC-1155")
- token_contract_address_hash = token.contract_address_hash
- instance = insert(:token_instance, token_contract_address_hash: token_contract_address_hash)
- token_id = instance.token_id
- address = insert(:address, hash: "0x57e93bb58268de818b42e3795c97bad58afcd3fe")
- address_hash = address.hash
-
- EthereumJSONRPC.Mox
- |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0xc87b56dd" <> _}, _]}], _ ->
- {:ok,
- [
- %{
- id: 0,
- result:
- "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000027b7d000000000000000000000000000000000000000000000000000000000000"
- }
- ]}
- end)
- |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0x6352211e" <> _}, _]}], _ ->
- {:ok, [%{id: 0, result: "0x00000000000000000000000057e93bb58268de818b42e3795c97bad58afcd3fe"}]}
- end)
-
- TokenInstance.run(
- [%{contract_address_hash: token_contract_address_hash, token_id: token_id}],
- nil
- )
-
- assert %{
- token_id: ^token_id,
- token_type: "ERC-1155",
- token_contract_address_hash: ^token_contract_address_hash,
- address_hash: ^address_hash
- } = Repo.one(CurrentTokenBalance)
- end
-
- test "updates current token balance with missing address" do
- token = insert(:token, type: "ERC-1155")
- token_contract_address_hash = token.contract_address_hash
- instance = insert(:token_instance, token_contract_address_hash: token_contract_address_hash)
- token_id = instance.token_id
- {:ok, address_hash} = Chain.string_to_address_hash("0x57e93bb58268de818b42e3795c97bad58afcd3fe")
-
- EthereumJSONRPC.Mox
- |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0xc87b56dd" <> _}, _]}], _ ->
- {:ok,
- [
- %{
- id: 0,
- result:
- "0x000000000000000000000000000000000000000000000000000000000000002000000000000000000000000000000000000000000000000000000000000000027b7d000000000000000000000000000000000000000000000000000000000000"
- }
- ]}
- end)
- |> expect(:json_rpc, fn [%{id: 0, method: "eth_call", params: [%{data: "0x6352211e" <> _}, _]}], _ ->
- {:ok, [%{id: 0, result: "0x00000000000000000000000057e93bb58268de818b42e3795c97bad58afcd3fe"}]}
- end)
-
- TokenInstance.run(
- [%{contract_address_hash: token_contract_address_hash, token_id: token_id}],
- nil
- )
-
- assert %{
- token_id: ^token_id,
- token_type: "ERC-1155",
- token_contract_address_hash: ^token_contract_address_hash,
- address_hash: ^address_hash
- } = Repo.one(CurrentTokenBalance)
-
- assert %Address{} = Repo.get_by(Address, hash: address_hash)
- end
- end
-end
diff --git a/config/runtime.exs b/config/runtime.exs
index 3db22bf1d93c..9e54ebd200f8 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -9,12 +9,12 @@ indexer_memory_limit =
|> System.get_env(to_string(indexer_memory_limit_default))
|> Integer.parse()
|> case do
- {integer, ""} -> integer
- _ -> indexer_memory_limit_default
- end
+ {integer, ""} -> integer
+ _ -> indexer_memory_limit_default
+ end
config :indexer,
- memory_limit: indexer_memory_limit <<< 32
+ memory_limit: indexer_memory_limit <<< 32
indexer_empty_blocks_sanitizer_batch_size_default = 100
@@ -23,20 +23,20 @@ indexer_empty_blocks_sanitizer_batch_size =
|> System.get_env(to_string(indexer_empty_blocks_sanitizer_batch_size_default))
|> Integer.parse()
|> case do
- {integer, ""} -> integer
- _ -> indexer_empty_blocks_sanitizer_batch_size_default
- end
+ {integer, ""} -> integer
+ _ -> indexer_empty_blocks_sanitizer_batch_size_default
+ end
config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer.Supervisor,
- disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true"
+ disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true"
config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer, batch_size: indexer_empty_blocks_sanitizer_batch_size
config :block_scout_web, :footer,
- chat_link: System.get_env("FOOTER_CHAT_LINK", "http://discord.gg/celo"),
- forum_link: System.get_env("FOOTER_FORUM_LINK", "https://forum.celo.org/"),
- github_link: System.get_env("FOOTER_GITHUB_LINK", "https://github.com/celo-org/blockscout"),
- enable_forum_link: System.get_env("FOOTER_ENABLE_FORUM_LINK", "false") == "true"
+ chat_link: System.get_env("FOOTER_CHAT_LINK", "http://discord.gg/celo"),
+ forum_link: System.get_env("FOOTER_FORUM_LINK", "https://forum.celo.org/"),
+ github_link: System.get_env("FOOTER_GITHUB_LINK", "https://github.com/celo-org/blockscout"),
+ enable_forum_link: System.get_env("FOOTER_ENABLE_FORUM_LINK", "false") == "true"
######################
### BlockScout Web ###
@@ -44,34 +44,34 @@ config :block_scout_web, :footer,
# Configures Ueberauth's Auth0 auth provider
config :ueberauth, Ueberauth.Strategy.Auth0.OAuth,
- domain: System.get_env("ACCOUNT_AUTH0_DOMAIN"),
- client_id: System.get_env("ACCOUNT_AUTH0_CLIENT_ID"),
- client_secret: System.get_env("ACCOUNT_AUTH0_CLIENT_SECRET")
+ domain: System.get_env("ACCOUNT_AUTH0_DOMAIN"),
+ client_id: System.get_env("ACCOUNT_AUTH0_CLIENT_ID"),
+ client_secret: System.get_env("ACCOUNT_AUTH0_CLIENT_SECRET")
# Configures Ueberauth local settings
config :ueberauth, Ueberauth,
- logout_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_URL"),
- logout_return_to_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_RETURN_URL")
+ logout_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_URL"),
+ logout_return_to_url: System.get_env("ACCOUNT_AUTH0_LOGOUT_RETURN_URL")
config :block_scout_web,
- version: System.get_env("BLOCKSCOUT_VERSION"),
- segment_key: System.get_env("SEGMENT_KEY"),
- release_link: System.get_env("RELEASE_LINK"),
- decompiled_smart_contract_token: System.get_env("DECOMPILED_SMART_CONTRACT_TOKEN"),
- show_percentage: if(System.get_env("SHOW_ADDRESS_MARKETCAP_PERCENTAGE", "true") == "false", do: false, else: true),
- checksum_address_hashes: if(System.get_env("CHECKSUM_ADDRESS_HASHES", "true") == "false", do: false, else: true)
+ version: System.get_env("BLOCKSCOUT_VERSION"),
+ segment_key: System.get_env("SEGMENT_KEY"),
+ release_link: System.get_env("RELEASE_LINK"),
+ decompiled_smart_contract_token: System.get_env("DECOMPILED_SMART_CONTRACT_TOKEN"),
+ show_percentage: if(System.get_env("SHOW_ADDRESS_MARKETCAP_PERCENTAGE", "true") == "false", do: false, else: true),
+ checksum_address_hashes: if(System.get_env("CHECKSUM_ADDRESS_HASHES", "true") == "false", do: false, else: true)
config :block_scout_web, BlockScoutWeb.Chain,
- network: System.get_env("NETWORK"),
- subnetwork: System.get_env("SUBNETWORK"),
- network_icon: System.get_env("NETWORK_ICON"),
- logo: System.get_env("LOGO", "/images/celo_logo.svg"),
- logo_footer: System.get_env("LOGO_FOOTER", "/images/celo_logo.svg"),
- logo_text: System.get_env("LOGO_TEXT"),
- has_emission_funds: false,
- show_maintenance_alert: System.get_env("SHOW_MAINTENANCE_ALERT", "false") == "true",
- enable_testnet_label: System.get_env("SHOW_TESTNET_LABEL", "false") == "true",
- testnet_label_text: System.get_env("TESTNET_LABEL_TEXT", "Testnet")
+ network: System.get_env("NETWORK"),
+ subnetwork: System.get_env("SUBNETWORK"),
+ network_icon: System.get_env("NETWORK_ICON"),
+ logo: System.get_env("LOGO", "/images/celo_logo.svg"),
+ logo_footer: System.get_env("LOGO_FOOTER", "/images/celo_logo.svg"),
+ logo_text: System.get_env("LOGO_TEXT"),
+ has_emission_funds: false,
+ show_maintenance_alert: System.get_env("SHOW_MAINTENANCE_ALERT", "false") == "true",
+ enable_testnet_label: System.get_env("SHOW_TESTNET_LABEL", "false") == "true",
+ testnet_label_text: System.get_env("TESTNET_LABEL_TEXT", "Testnet")
verification_max_libraries_default = 10
@@ -80,38 +80,38 @@ verification_max_libraries =
|> System.get_env(to_string(verification_max_libraries_default))
|> Integer.parse()
|> case do
- {integer, ""} -> integer
- _ -> verification_max_libraries_default
- end
+ {integer, ""} -> integer
+ _ -> verification_max_libraries_default
+ end
config :block_scout_web,
- link_to_other_explorers: System.get_env("LINK_TO_OTHER_EXPLORERS") == "true",
- other_explorers: System.get_env("OTHER_EXPLORERS"),
- webapp_url: System.get_env("WEBAPP_URL"),
- api_url: System.get_env("API_URL"),
- apps_menu: if(System.get_env("APPS_MENU", "false") == "true", do: true, else: false),
- stats_enabled: System.get_env("DISABLE_STATS") != "true",
- stats_report_url: System.get_env("STATS_REPORT_URL", ""),
- makerdojo_url: System.get_env("MAKERDOJO_URL", ""),
- apps: System.get_env("APPS") || System.get_env("EXTERNAL_APPS"),
- gas_price: System.get_env("GAS_PRICE", nil),
- restricted_list: System.get_env("RESTRICTED_LIST", nil),
- restricted_list_key: System.get_env("RESTRICTED_LIST_KEY", nil),
- dark_forest_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST"),
- dark_forest_addresses_v_0_5: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST_V_0_5"),
- circles_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_CIRCLES"),
- test_tokens_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_TEST_TOKEN"),
- max_size_to_show_array_as_is: Integer.parse(System.get_env("MAX_SIZE_UNLESS_HIDE_ARRAY", "50")),
- max_length_to_show_string_without_trimming: System.get_env("MAX_STRING_LENGTH_WITHOUT_TRIMMING", "2040"),
- re_captcha_site_key: System.get_env("RE_CAPTCHA_SITE_KEY", nil),
- re_captcha_api_key: System.get_env("RE_CAPTCHA_API_KEY", nil),
- re_captcha_secret_key: System.get_env("RE_CAPTCHA_SECRET_KEY", nil),
- re_captcha_project_id: System.get_env("RE_CAPTCHA_PROJECT_ID", nil),
- re_captcha_client_key: System.get_env("RE_CAPTCHA_CLIENT_KEY", nil),
- new_tags: System.get_env("NEW_TAGS"),
- chain_id: System.get_env("CHAIN_ID"),
- json_rpc: System.get_env("JSON_RPC"),
- verification_max_libraries: verification_max_libraries
+ link_to_other_explorers: System.get_env("LINK_TO_OTHER_EXPLORERS") == "true",
+ other_explorers: System.get_env("OTHER_EXPLORERS"),
+ webapp_url: System.get_env("WEBAPP_URL"),
+ api_url: System.get_env("API_URL"),
+ apps_menu: if(System.get_env("APPS_MENU", "false") == "true", do: true, else: false),
+ stats_enabled: System.get_env("DISABLE_STATS") != "true",
+ stats_report_url: System.get_env("STATS_REPORT_URL", ""),
+ makerdojo_url: System.get_env("MAKERDOJO_URL", ""),
+ apps: System.get_env("APPS") || System.get_env("EXTERNAL_APPS"),
+ gas_price: System.get_env("GAS_PRICE", nil),
+ restricted_list: System.get_env("RESTRICTED_LIST", nil),
+ restricted_list_key: System.get_env("RESTRICTED_LIST_KEY", nil),
+ dark_forest_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST"),
+ dark_forest_addresses_v_0_5: System.get_env("CUSTOM_CONTRACT_ADDRESSES_DARK_FOREST_V_0_5"),
+ circles_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_CIRCLES"),
+ test_tokens_addresses: System.get_env("CUSTOM_CONTRACT_ADDRESSES_TEST_TOKEN"),
+ max_size_to_show_array_as_is: Integer.parse(System.get_env("MAX_SIZE_UNLESS_HIDE_ARRAY", "50")),
+ max_length_to_show_string_without_trimming: System.get_env("MAX_STRING_LENGTH_WITHOUT_TRIMMING", "2040"),
+ re_captcha_site_key: System.get_env("RE_CAPTCHA_SITE_KEY", nil),
+ re_captcha_api_key: System.get_env("RE_CAPTCHA_API_KEY", nil),
+ re_captcha_secret_key: System.get_env("RE_CAPTCHA_SECRET_KEY", nil),
+ re_captcha_project_id: System.get_env("RE_CAPTCHA_PROJECT_ID", nil),
+ re_captcha_client_key: System.get_env("RE_CAPTCHA_CLIENT_KEY", nil),
+ new_tags: System.get_env("NEW_TAGS"),
+ chain_id: System.get_env("CHAIN_ID"),
+ json_rpc: System.get_env("JSON_RPC"),
+ verification_max_libraries: verification_max_libraries
default_api_rate_limit = 50
default_api_rate_limit_str = Integer.to_string(default_api_rate_limit)
@@ -121,41 +121,41 @@ global_api_rate_limit_value =
|> System.get_env(default_api_rate_limit_str)
|> Integer.parse()
|> case do
- {integer, ""} -> integer
- _ -> default_api_rate_limit
- end
+ {integer, ""} -> integer
+ _ -> default_api_rate_limit
+ end
api_rate_limit_by_key_value =
"API_RATE_LIMIT_BY_KEY"
|> System.get_env(default_api_rate_limit_str)
|> Integer.parse()
|> case do
- {integer, ""} -> integer
- _ -> default_api_rate_limit
- end
+ {integer, ""} -> integer
+ _ -> default_api_rate_limit
+ end
api_rate_limit_by_ip_value =
"API_RATE_LIMIT_BY_IP"
|> System.get_env(default_api_rate_limit_str)
|> Integer.parse()
|> case do
- {integer, ""} -> integer
- _ -> default_api_rate_limit
- end
+ {integer, ""} -> integer
+ _ -> default_api_rate_limit
+ end
config :block_scout_web, :api_rate_limit,
- global_limit: global_api_rate_limit_value,
- limit_by_key: api_rate_limit_by_key_value,
- limit_by_ip: api_rate_limit_by_ip_value,
- static_api_key: System.get_env("API_RATE_LIMIT_STATIC_API_KEY", nil),
- whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS", nil)
+ global_limit: global_api_rate_limit_value,
+ limit_by_key: api_rate_limit_by_key_value,
+ limit_by_ip: api_rate_limit_by_ip_value,
+ static_api_key: System.get_env("API_RATE_LIMIT_STATIC_API_KEY", nil),
+ whitelisted_ips: System.get_env("API_RATE_LIMIT_WHITELISTED_IPS", nil)
config :block_scout_web, BlockScoutWeb.Endpoint,
- server: true,
- url: [
- scheme: System.get_env("BLOCKSCOUT_PROTOCOL") || "http",
- host: System.get_env("BLOCKSCOUT_HOST") || "localhost"
- ]
+ server: true,
+ url: [
+ scheme: System.get_env("BLOCKSCOUT_PROTOCOL") || "http",
+ host: System.get_env("BLOCKSCOUT_HOST") || "localhost"
+ ]
# Configures History
price_chart_config =
@@ -173,11 +173,11 @@ tx_chart_config =
end
config :block_scout_web,
- chart_config: Map.merge(price_chart_config, tx_chart_config)
+ chart_config: Map.merge(price_chart_config, tx_chart_config)
config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance,
- # days
- coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10")
+ # days
+ coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10")
config :block_scout_web, BlockScoutWeb.API.V2, enabled: System.get_env("API_V2_ENABLED") == "true"
@@ -186,15 +186,15 @@ config :block_scout_web, BlockScoutWeb.API.V2, enabled: System.get_env("API_V2_E
########################
config :ethereum_jsonrpc,
- rpc_transport: if(System.get_env("ETHEREUM_JSONRPC_TRANSPORT", "http") == "http", do: :http, else: :ipc),
- ipc_path: System.get_env("IPC_PATH"),
- disable_archive_balances?: System.get_env("ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES", "false") == "true"
+ rpc_transport: if(System.get_env("ETHEREUM_JSONRPC_TRANSPORT", "http") == "http", do: :http, else: :ipc),
+ ipc_path: System.get_env("IPC_PATH"),
+ disable_archive_balances?: System.get_env("ETHEREUM_JSONRPC_DISABLE_ARCHIVE_BALANCES", "false") == "true"
debug_trace_transaction_timeout = System.get_env("ETHEREUM_JSONRPC_DEBUG_TRACE_TRANSACTION_TIMEOUT", "900s")
config :ethereum_jsonrpc, :internal_transaction_timeout, debug_trace_transaction_timeout
config :ethereum_jsonrpc, EthereumJSONRPC.PendingTransaction,
- type: System.get_env("ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE", "default")
+ type: System.get_env("ETHEREUM_JSONRPC_PENDING_TRANSACTIONS_TYPE", "default")
################
### Explorer ###
@@ -210,49 +210,57 @@ healthy_blocks_period =
end
config :explorer,
- coin: System.get_env("COIN", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO",
- coin_name: System.get_env("COIN_NAME", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO",
- allowed_evm_versions:
- System.get_env("ALLOWED_EVM_VERSIONS") ||
- "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,default",
- include_uncles_in_average_block_time:
- if(System.get_env("UNCLES_IN_AVERAGE_BLOCK_TIME") == "true", do: true, else: false),
- healthy_blocks_period: healthy_blocks_period,
- realtime_events_sender:
- if(disable_webapp != "true",
- do: Explorer.Chain.Events.SimpleSender,
- else: Explorer.Chain.Events.PubSubSender
- )
+ coin: System.get_env("COIN", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO",
+ coin_name: System.get_env("COIN_NAME", nil) || System.get_env("EXCHANGE_RATES_COIN") || "CELO",
+ allowed_evm_versions:
+ System.get_env("ALLOWED_EVM_VERSIONS") ||
+ "homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,istanbul,berlin,london,default",
+ include_uncles_in_average_block_time:
+ if(System.get_env("UNCLES_IN_AVERAGE_BLOCK_TIME") == "true", do: true, else: false),
+ healthy_blocks_period: healthy_blocks_period,
+ realtime_events_sender:
+ if(disable_webapp != "true",
+ do: Explorer.Chain.Events.SimpleSender,
+ else: Explorer.Chain.Events.PubSubSender
+ ),
+ enable_caching_implementation_data_of_proxy: true,
+ avg_block_time_as_ttl_cached_implementation_data_of_proxy: true,
+ fallback_ttl_cached_implementation_data_of_proxy: :timer.seconds(4),
+ implementation_data_fetching_timeout: :timer.seconds(2)
+
+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",
- do: false,
- else: true
- ),
- event_source: Explorer.Chain.Events.PubSubSource
+ enabled:
+ if(disable_webapp == "true" && disable_indexer == "true",
+ do: false,
+ else: true
+ ),
+ event_source: Explorer.Chain.Events.PubSubSource
config :explorer, Explorer.ChainSpec.GenesisData,
- chain_spec_path:
- System.get_env(
- "CHAIN_SPEC_PATH",
- "https://www.googleapis.com/storage/v1/b/genesis_blocks/o/#{String.downcase(System.get_env("SUBNETWORK", "Baklava"))}?alt=media"
- ),
- emission_format: System.get_env("EMISSION_FORMAT", "DEFAULT"),
- rewards_contract_address: System.get_env("REWARDS_CONTRACT", "0xeca443e8e1ab29971a45a9c57a6a9875701698a5")
+ chain_spec_path:
+ System.get_env(
+ "CHAIN_SPEC_PATH",
+ "https://www.googleapis.com/storage/v1/b/genesis_blocks/o/#{String.downcase(System.get_env("SUBNETWORK", "Baklava"))}?alt=media"
+ ),
+ emission_format: System.get_env("EMISSION_FORMAT", "DEFAULT"),
+ rewards_contract_address: System.get_env("REWARDS_CONTRACT", "0xeca443e8e1ab29971a45a9c57a6a9875701698a5")
config :explorer, Explorer.Chain.Cache.BlockNumber,
- ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
- global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
+ ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
+ global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
address_sum_global_ttl =
"CACHE_ADDRESS_SUM_PERIOD"
|> System.get_env("")
|> Integer.parse()
|> case do
- {integer, ""} -> integer
- _ -> 3600
- end
+ {integer, ""} -> integer
+ _ -> 3600
+ end
|> :timer.seconds()
config :explorer, Explorer.Chain.Cache.AddressSum, global_ttl: address_sum_global_ttl
@@ -260,12 +268,12 @@ config :explorer, Explorer.Chain.Cache.AddressSum, global_ttl: address_sum_globa
config :explorer, Explorer.Chain.Cache.AddressSumMinusBurnt, global_ttl: address_sum_global_ttl
config :explorer, Explorer.ExchangeRates,
- store: :ets,
- enabled: System.get_env("DISABLE_EXCHANGE_RATES") != "true",
- coingecko_coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"),
- coingecko_api_key: System.get_env("EXCHANGE_RATES_COINGECKO_API_KEY"),
- coinmarketcap_api_key: System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY"),
- fetch_btc_value: System.get_env("EXCHANGE_RATES_FETCH_BTC_VALUE") == "true"
+ store: :ets,
+ enabled: System.get_env("DISABLE_EXCHANGE_RATES") != "true",
+ coingecko_coin_id: System.get_env("EXCHANGE_RATES_COINGECKO_COIN_ID"),
+ coingecko_api_key: System.get_env("EXCHANGE_RATES_COINGECKO_API_KEY"),
+ coinmarketcap_api_key: System.get_env("EXCHANGE_RATES_COINMARKETCAP_API_KEY"),
+ fetch_btc_value: System.get_env("EXCHANGE_RATES_FETCH_BTC_VALUE") == "true"
exchange_rates_source =
cond do
@@ -292,9 +300,9 @@ txs_stats_days_to_compile_at_init =
|> elem(0)
config :explorer, Explorer.Chain.Transaction.History.Historian,
- enabled: System.get_env("ENABLE_TXS_STATS", "true") != "false",
- init_lag: txs_stats_init_lag,
- days_to_compile_at_init: txs_stats_days_to_compile_at_init
+ enabled: System.get_env("ENABLE_TXS_STATS", "true") != "false",
+ init_lag: txs_stats_init_lag,
+ days_to_compile_at_init: txs_stats_days_to_compile_at_init
history_fetch_interval =
case Integer.parse(System.get_env("HISTORY_FETCH_INTERVAL", "")) do
@@ -307,8 +315,8 @@ config :explorer, Explorer.History.Process, history_fetch_interval: history_fetc
if System.get_env("METADATA_CONTRACT") && System.get_env("VALIDATORS_CONTRACT") do
config :explorer, Explorer.Validator.MetadataRetriever,
- metadata_contract_address: System.get_env("METADATA_CONTRACT"),
- validators_contract_address: System.get_env("VALIDATORS_CONTRACT")
+ metadata_contract_address: System.get_env("METADATA_CONTRACT"),
+ validators_contract_address: System.get_env("VALIDATORS_CONTRACT")
config :explorer, Explorer.Validator.MetadataProcessor, enabled: disable_indexer != "true"
else
@@ -316,8 +324,8 @@ else
end
config :explorer, Explorer.Chain.Block.Reward,
- validators_contract_address: System.get_env("VALIDATORS_CONTRACT"),
- keys_manager_contract_address: System.get_env("KEYS_MANAGER_CONTRACT")
+ validators_contract_address: System.get_env("VALIDATORS_CONTRACT"),
+ keys_manager_contract_address: System.get_env("KEYS_MANAGER_CONTRACT")
case System.get_env("SUPPLY_MODULE") do
"rsk" ->
@@ -336,57 +344,61 @@ case System.get_env("MARKET_CAP_ENABLED", "false") do
end
config :explorer,
- checksum_function: System.get_env("CHECKSUM_FUNCTION") && String.to_atom(System.get_env("CHECKSUM_FUNCTION"))
+ checksum_function: System.get_env("CHECKSUM_FUNCTION") && String.to_atom(System.get_env("CHECKSUM_FUNCTION"))
config :explorer, Explorer.Chain.Cache.Blocks,
- ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
- global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
+ ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
+ global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.Transactions,
- ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
- global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
+ ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
+ global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
+
+config :explorer, Explorer.Chain.Cache.TransactionsApiV2,
+ ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
+ global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.Accounts,
- ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
- global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
+ ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
+ global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.Uncles,
- ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
- global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
+ ttl_check_interval: if(disable_indexer == "true", do: :timer.seconds(1), else: false),
+ global_ttl: if(disable_indexer == "true", do: :timer.seconds(5))
config :explorer, Explorer.ThirdPartyIntegrations.Sourcify,
- server_url: System.get_env("SOURCIFY_SERVER_URL") || "https://sourcify.dev/server",
- enabled: System.get_env("ENABLE_SOURCIFY_INTEGRATION") == "true",
- chain_id: System.get_env("CHAIN_ID"),
- repo_url: System.get_env("SOURCIFY_REPO_URL") || "https://repo.sourcify.dev/contracts"
+ server_url: System.get_env("SOURCIFY_SERVER_URL") || "https://sourcify.dev/server",
+ enabled: System.get_env("ENABLE_SOURCIFY_INTEGRATION") == "true",
+ chain_id: System.get_env("CHAIN_ID"),
+ repo_url: System.get_env("SOURCIFY_REPO_URL") || "https://repo.sourcify.dev/contracts"
config :explorer, Explorer.SmartContract.RustVerifierInterface,
- service_url: System.get_env("RUST_VERIFICATION_SERVICE_URL"),
- enabled: System.get_env("ENABLE_RUST_VERIFICATION_SERVICE") == "true"
+ service_url: System.get_env("RUST_VERIFICATION_SERVICE_URL"),
+ enabled: System.get_env("ENABLE_RUST_VERIFICATION_SERVICE") == "true"
config :explorer, Explorer.ThirdPartyIntegrations.AirTable,
- table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"),
- api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY")
+ table_url: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL"),
+ api_key: System.get_env("ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY")
config :explorer, Explorer.Mailer,
- adapter: Bamboo.SendGridAdapter,
- api_key: System.get_env("ACCOUNT_SENDGRID_API_KEY")
+ adapter: Bamboo.SendGridAdapter,
+ api_key: System.get_env("ACCOUNT_SENDGRID_API_KEY")
config :explorer, Explorer.Account,
- enabled: System.get_env("ACCOUNT_ENABLED") == "true",
- sendgrid: [
- sender: System.get_env("ACCOUNT_SENDGRID_SENDER"),
- template: System.get_env("ACCOUNT_SENDGRID_TEMPLATE")
- ]
+ enabled: System.get_env("ACCOUNT_ENABLED") == "true",
+ sendgrid: [
+ sender: System.get_env("ACCOUNT_SENDGRID_SENDER"),
+ template: System.get_env("ACCOUNT_SENDGRID_TEMPLATE")
+ ]
{token_id_migration_first_block, _} = Integer.parse(System.get_env("TOKEN_ID_MIGRATION_FIRST_BLOCK", "0"))
{token_id_migration_concurrency, _} = Integer.parse(System.get_env("TOKEN_ID_MIGRATION_CONCURRENCY", "1"))
{token_id_migration_batch_size, _} = Integer.parse(System.get_env("TOKEN_ID_MIGRATION_BATCH_SIZE", "500"))
config :explorer, :token_id_migration,
- first_block: token_id_migration_first_block,
- concurrency: token_id_migration_concurrency,
- batch_size: token_id_migration_batch_size
+ first_block: token_id_migration_first_block,
+ concurrency: token_id_migration_concurrency,
+ batch_size: token_id_migration_batch_size
###############
### Indexer ###
@@ -425,23 +437,30 @@ block_transformer =
end
config :indexer,
- block_transformer: block_transformer,
- ecto_repos: [Explorer.Repo.Local],
- metadata_updater_seconds_interval:
- String.to_integer(System.get_env("TOKEN_METADATA_UPDATE_INTERVAL") || "#{2 * 24 * 60 * 60}"),
- health_check_port: port || 4001,
- block_ranges: System.get_env("BLOCK_RANGES") || "",
- first_block: System.get_env("FIRST_BLOCK") || "",
- last_block: System.get_env("LAST_BLOCK") || "",
- metrics_enabled: System.get_env("METRICS_ENABLED") || false,
- trace_first_block: System.get_env("TRACE_FIRST_BLOCK") || "",
- trace_last_block: System.get_env("TRACE_LAST_BLOCK") || "",
- fetch_rewards_way: System.get_env("FETCH_REWARDS_WAY", "trace_block")
+ block_transformer: block_transformer,
+ ecto_repos: [Explorer.Repo.Local],
+ metadata_updater_seconds_interval:
+ String.to_integer(System.get_env("TOKEN_METADATA_UPDATE_INTERVAL") || "#{2 * 24 * 60 * 60}"),
+ health_check_port: port || 4001,
+ block_ranges: System.get_env("BLOCK_RANGES") || "",
+ first_block: System.get_env("FIRST_BLOCK") || "",
+ last_block: System.get_env("LAST_BLOCK") || "",
+ metrics_enabled: System.get_env("METRICS_ENABLED") || false,
+ trace_first_block: System.get_env("TRACE_FIRST_BLOCK") || "",
+ trace_last_block: System.get_env("TRACE_LAST_BLOCK") || "",
+ fetch_rewards_way: System.get_env("FETCH_REWARDS_WAY", "trace_block")
+
+{receipts_batch_size, _} = Integer.parse(System.get_env("INDEXER_RECEIPTS_BATCH_SIZE", "250"))
+{receipts_concurrency, _} = Integer.parse(System.get_env("INDEXER_RECEIPTS_CONCURRENCY", "10"))
+
+config :indexer,
+ receipts_batch_size: receipts_batch_size,
+ receipts_concurrency: receipts_concurrency
config :indexer, Indexer.Fetcher.PendingTransaction.Supervisor,
- disabled?:
- System.get_env("ETHEREUM_JSONRPC_VARIANT") == "besu" ||
- System.get_env("INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER", "false") == "true"
+ disabled?:
+ System.get_env("ETHEREUM_JSONRPC_VARIANT") == "besu" ||
+ System.get_env("INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER", "false") == "true"
token_balance_on_demand_fetcher_threshold_minutes = System.get_env("TOKEN_BALANCE_ON_DEMAND_FETCHER_THRESHOLD_MINUTES")
@@ -466,26 +485,26 @@ coin_balance_on_demand_fetcher_threshold =
config :indexer, Indexer.Fetcher.CoinBalanceOnDemand, threshold: coin_balance_on_demand_fetcher_threshold
config :indexer, Indexer.Fetcher.BlockReward.Supervisor,
- disabled?: System.get_env("INDEXER_DISABLE_BLOCK_REWARD_FETCHER", "false") == "true"
+ disabled?: System.get_env("INDEXER_DISABLE_BLOCK_REWARD_FETCHER", "false") == "true"
config :indexer, Indexer.Fetcher.InternalTransaction.Supervisor,
- disabled?: System.get_env("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER", "false") == "true"
+ disabled?: System.get_env("INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER", "false") == "true"
config :indexer, Indexer.Fetcher.CoinBalance.Supervisor,
- disabled?: System.get_env("INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER", "false") == "true"
+ disabled?: System.get_env("INDEXER_DISABLE_ADDRESS_COIN_BALANCE_FETCHER", "false") == "true"
config :indexer, Indexer.Fetcher.TokenUpdater.Supervisor,
- disabled?: System.get_env("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER", "false") == "true"
+ disabled?: System.get_env("INDEXER_DISABLE_CATALOGED_TOKEN_UPDATER_FETCHER", "false") == "true"
config :indexer, Indexer.Fetcher.EmptyBlocksSanitizer.Supervisor,
- disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true"
+ disabled?: System.get_env("INDEXER_DISABLE_EMPTY_BLOCK_SANITIZER", "false") == "true"
config :indexer, Indexer.Supervisor, enabled: System.get_env("DISABLE_INDEXER") != "true"
config :indexer, Indexer.Block.Realtime.Supervisor, enabled: System.get_env("DISABLE_REALTIME_INDEXER") != "true"
config :indexer, Indexer.Fetcher.TokenInstance.Supervisor,
- disabled?: System.get_env("DISABLE_TOKEN_INSTANCE_FETCHER", "false") == "true"
+ disabled?: System.get_env("DISABLE_TOKEN_INSTANCE_FETCHER", "false") == "true"
blocks_catchup_fetcher_batch_size_default_str = "10"
blocks_catchup_fetcher_concurrency_default_str = "10"
@@ -497,12 +516,9 @@ blocks_catchup_fetcher_concurrency_default_str = "10"
Integer.parse(System.get_env("INDEXER_CATCHUP_BLOCKS_CONCURRENCY", blocks_catchup_fetcher_concurrency_default_str))
config :indexer, Indexer.Block.Catchup.Fetcher,
- batch_size: blocks_catchup_fetcher_batch_size,
- concurrency: blocks_catchup_fetcher_concurrency
+ batch_size: blocks_catchup_fetcher_batch_size,
+ concurrency: blocks_catchup_fetcher_concurrency
-if File.exists?("#{Path.absname(__DIR__)}/runtime/#{config_env()}.exs") do
- Code.require_file("#{config_env()}.exs", "#{Path.absname(__DIR__)}/runtime")
-end
{internal_transaction_fetcher_batch_size, _} =
Integer.parse(System.get_env("INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE", "8"))
@@ -511,9 +527,20 @@ end
Integer.parse(System.get_env("INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY", "8"))
config :indexer, Indexer.Fetcher.InternalTransaction,
- batch_size: internal_transaction_fetcher_batch_size,
- concurrency: internal_transaction_fetcher_concurrency
+ batch_size: internal_transaction_fetcher_batch_size,
+ concurrency: internal_transaction_fetcher_concurrency
+{coin_balance_fetcher_batch_size, _} = Integer.parse(System.get_env("INDEXER_COIN_BALANCES_BATCH_SIZE", "500"))
+
+{coin_balance_fetcher_concurrency, _} = Integer.parse(System.get_env("INDEXER_COIN_BALANCES_CONCURRENCY", "4"))
+
+config :indexer, Indexer.Fetcher.CoinBalance,
+ batch_size: coin_balance_fetcher_batch_size,
+ concurrency: coin_balance_fetcher_concurrency
+
+if File.exists?("#{Path.absname(__DIR__)}/runtime/#{config_env()}.exs") do
+ Code.require_file("#{config_env()}.exs", "#{Path.absname(__DIR__)}/runtime")
+end
for config <- "../apps/*/config/runtime/#{config_env()}.exs" |> Path.expand(__DIR__) |> Path.wildcard() do
if File.exists?(config) do
diff --git a/config/runtime/test.exs b/config/runtime/test.exs
index f9bc04c0b75c..d020cf8fc807 100644
--- a/config/runtime/test.exs
+++ b/config/runtime/test.exs
@@ -7,6 +7,7 @@ alias EthereumJSONRPC.Variant
######################
config :block_scout_web, BlockScoutWeb.CsvExportController, itx_export_enabled: true
+config :block_scout_web, BlockScoutWeb.API.V2, enabled: true
########################
### Ethereum JSONRPC ###
diff --git a/docker-compose/docker-compose-no-build-erigon.yml b/docker-compose/docker-compose-no-build-erigon.yml
index 5f3eb2a0122e..b1640627a815 100644
--- a/docker-compose/docker-compose-no-build-erigon.yml
+++ b/docker-compose/docker-compose-no-build-erigon.yml
@@ -58,3 +58,13 @@ services:
- ./envs/common-smart-contract-verifier.env
ports:
- 8043:8043
+
+ visualizer:
+ image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest}
+ restart: always
+ container_name: 'visualizer'
+ env_file:
+ - ./envs/common-visualizer.env
+ ports:
+ - 8050:8050
+
diff --git a/docker-compose/docker-compose-no-build-ganache.yml b/docker-compose/docker-compose-no-build-ganache.yml
index 22442c15a98c..177d402e47bf 100644
--- a/docker-compose/docker-compose-no-build-ganache.yml
+++ b/docker-compose/docker-compose-no-build-ganache.yml
@@ -60,3 +60,12 @@ services:
ports:
- 8043:8043
+ visualizer:
+ image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest}
+ restart: always
+ container_name: 'visualizer'
+ env_file:
+ - ./envs/common-visualizer.env
+ ports:
+ - 8050:8050
+
diff --git a/docker-compose/docker-compose-no-build-geth.yml b/docker-compose/docker-compose-no-build-geth.yml
index 0fd7caa74db8..116ee4afe79a 100644
--- a/docker-compose/docker-compose-no-build-geth.yml
+++ b/docker-compose/docker-compose-no-build-geth.yml
@@ -58,3 +58,13 @@ services:
- ./envs/common-smart-contract-verifier.env
ports:
- 8043:8043
+
+ visualizer:
+ image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest}
+ restart: always
+ container_name: 'visualizer'
+ env_file:
+ - ./envs/common-visualizer.env
+ ports:
+ - 8050:8050
+
diff --git a/docker-compose/docker-compose-no-build-hardhat-network.yml b/docker-compose/docker-compose-no-build-hardhat-network.yml
index 9fabf5f3e185..3748696d8353 100644
--- a/docker-compose/docker-compose-no-build-hardhat-network.yml
+++ b/docker-compose/docker-compose-no-build-hardhat-network.yml
@@ -57,3 +57,13 @@ services:
- ./envs/common-smart-contract-verifier.env
ports:
- 8043:8043
+
+ visualizer:
+ image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest}
+ restart: always
+ container_name: 'visualizer'
+ env_file:
+ - ./envs/common-visualizer.env
+ ports:
+ - 8050:8050
+
diff --git a/docker-compose/docker-compose-no-build-nethermind.yml b/docker-compose/docker-compose-no-build-nethermind.yml
index e5d607428012..300d63c8278d 100644
--- a/docker-compose/docker-compose-no-build-nethermind.yml
+++ b/docker-compose/docker-compose-no-build-nethermind.yml
@@ -58,3 +58,13 @@ services:
- ./envs/common-smart-contract-verifier.env
ports:
- 8043:8043
+
+ visualizer:
+ image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest}
+ restart: always
+ container_name: 'visualizer'
+ env_file:
+ - ./envs/common-visualizer.env
+ ports:
+ - 8050:8050
+
diff --git a/docker-compose/docker-compose-no-build-no-db-container.yml b/docker-compose/docker-compose-no-build-no-db-container.yml
index 24295de00e3a..c73468c81cd0 100644
--- a/docker-compose/docker-compose-no-build-no-db-container.yml
+++ b/docker-compose/docker-compose-no-build-no-db-container.yml
@@ -41,3 +41,13 @@ services:
- ./envs/common-smart-contract-verifier.env
ports:
- 8043:8043
+
+ visualizer:
+ image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest}
+ restart: always
+ container_name: 'visualizer'
+ env_file:
+ - ./envs/common-visualizer.env
+ ports:
+ - 8050:8050
+
diff --git a/docker-compose/docker-compose-no-rust-verification.yml b/docker-compose/docker-compose-no-rust-services.yml
similarity index 97%
rename from docker-compose/docker-compose-no-rust-verification.yml
rename to docker-compose/docker-compose-no-rust-services.yml
index 287912e609e0..f0ede3f16325 100644
--- a/docker-compose/docker-compose-no-rust-verification.yml
+++ b/docker-compose/docker-compose-no-rust-services.yml
@@ -52,6 +52,7 @@ services:
- ./envs/common-blockscout.env
environment:
ENABLE_RUST_VERIFICATION_SERVICE: 'false'
+ VISUALIZE_SOL2UML_ENABLED: 'false'
ports:
- 4000:4000
volumes:
diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml
index 24b67343ac5c..63223524638b 100644
--- a/docker-compose/docker-compose.yml
+++ b/docker-compose/docker-compose.yml
@@ -64,3 +64,12 @@ services:
- ./envs/common-smart-contract-verifier.env
ports:
- 8043:8043
+
+ visualizer:
+ image: ghcr.io/blockscout/visualizer:${VISUALIZER_DOCKER_TAG:-latest}
+ restart: always
+ container_name: 'visualizer'
+ env_file:
+ - ./envs/common-visualizer.env
+ ports:
+ - 8050:8050
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index cffb1aac85e9..73c936dbfa47 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -86,6 +86,10 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false
# INDEXER_CATCHUP_BLOCKS_CONCURRENCY=
# INDEXER_INTERNAL_TRANSACTIONS_BATCH_SIZE=
# INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY=
+# INDEXER_COIN_BALANCES_BATCH_SIZE=
+# INDEXER_COIN_BALANCES_CONCURRENCY=
+# INDEXER_RECEIPTS_BATCH_SIZE=
+# INDEXER_RECEIPTS_CONCURRENCY=
# TOKEN_ID_MIGRATION_FIRST_BLOCK=
# TOKEN_ID_MIGRATION_CONCURRENCY=
# TOKEN_ID_MIGRATION_BATCH_SIZE=
@@ -133,6 +137,8 @@ API_RATE_LIMIT_STATIC_API_KEY=
FETCH_REWARDS_WAY=trace_block
ENABLE_RUST_VERIFICATION_SERVICE=true
RUST_VERIFICATION_SERVICE_URL=http://host.docker.internal:8043/
+VISUALIZE_SOL2UML_ENABLED=true
+VISUALIZE_SOL2UML_SERVICE_URL=http://host.docker.internal:8050/
# DATABASE_READ_ONLY_API_URL=
# ACCOUNT_DATABASE_URL=
# ACCOUNT_POOL_SIZE=
diff --git a/docker-compose/envs/common-visualizer.env b/docker-compose/envs/common-visualizer.env
new file mode 100644
index 000000000000..b4fd470849cb
--- /dev/null
+++ b/docker-compose/envs/common-visualizer.env
@@ -0,0 +1 @@
+VISUALIZER__SERVER__GRPC__ENABLED=false
diff --git a/docker/Makefile b/docker/Makefile
index 29f285b08ad5..6423dd549b2e 100644
--- a/docker/Makefile
+++ b/docker/Makefile
@@ -467,6 +467,18 @@ endif
ifdef INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY
BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY=$(INDEXER_INTERNAL_TRANSACTIONS_CONCURRENCY)'
endif
+ifdef INDEXER_COIN_BALANCES_BATCH_SIZE
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_BATCH_SIZE=$(INDEXER_RECEIPTS_BATCH_SIZE)'
+endif
+ifdef INDEXER_COIN_BALANCES_CONCURRENCY
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_CONCURRENCY=$(INDEXER_RECEIPTS_CONCURRENCY)'
+endif
+ifdef INDEXER_RECEIPTS_BATCH_SIZE
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_BATCH_SIZE=$(INDEXER_RECEIPTS_BATCH_SIZE)'
+endif
+ifdef INDEXER_RECEIPTS_CONCURRENCY
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_RECEIPTS_CONCURRENCY=$(INDEXER_RECEIPTS_CONCURRENCY)'
+endif
ifdef INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE
BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE=$(INDEXER_EMPTY_BLOCKS_SANITIZER_BATCH_SIZE)'
endif
@@ -545,6 +557,13 @@ endif
ifdef ACCOUNT_CLOAK_KEY
BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_CLOAK_KEY=$(ACCOUNT_CLOAK_KEY)'
endif
+ifdef VISUALIZE_SOL2UML_ENABLED
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'VISUALIZE_SOL2UML_ENABLED=$(VISUALIZE_SOL2UML_ENABLED)'
+endif
+ifdef VISUALIZE_SOL2UML_SERVICE_URL
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'VISUALIZE_SOL2UML_SERVICE_URL=$(VISUALIZE_SOL2UML_SERVICE_URL)'
+endif
+
HAS_BLOCKSCOUT_IMAGE := $(shell docker images | grep -sw "${BS_CONTAINER_IMAGE} ")
build:
diff --git a/mix.lock b/mix.lock
index d6db8c60311d..e556d348228c 100644
--- a/mix.lock
+++ b/mix.lock
@@ -36,8 +36,8 @@
"dialyxir": {:hex, :dialyxir, "1.2.0", "58344b3e87c2e7095304c81a9ae65cb68b613e28340690dfe1a5597fd08dec37", [:mix], [{:erlex, ">= 0.2.6", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "61072136427a851674cab81762be4dbeae7679f85b1272b6d25c3a839aff8463"},
"digital_token": {:hex, :digital_token, "0.4.0", "2ad6894d4a40be8b2890aad286ecd5745fa473fa5699d80361a8c94428edcd1f", [:mix], [{:cldr_utils, "~> 2.17", [hex: :cldr_utils, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "a178edf61d1fee5bb3c34e14b0f4ee21809ee87cade8738f87337e59e5e66e26"},
"earmark_parser": {:hex, :earmark_parser, "1.4.29", "149d50dcb3a93d9f3d6f3ecf18c918fb5a2d3c001b5d3305c926cddfbd33355b", [:mix], [], "hexpm", "4902af1b3eb139016aed210888748db8070b8125c2342ce3dcae4f38dcc63503"},
- "ecto": {:hex, :ecto, "3.9.1", "67173b1687afeb68ce805ee7420b4261649d5e2deed8fe5550df23bab0bc4396", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "c80bb3d736648df790f7f92f81b36c922d9dd3203ca65be4ff01d067f54eb304"},
- "ecto_sql": {:hex, :ecto_sql, "3.9.0", "2bb21210a2a13317e098a420a8c1cc58b0c3421ab8e3acfa96417dab7817918c", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "a8f3f720073b8b1ac4c978be25fa7960ed7fd44997420c304a4a2e200b596453"},
+ "ecto": {:hex, :ecto, "3.9.2", "017db3bc786ff64271108522c01a5d3f6ba0aea5c84912cfb0dd73bf13684108", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "21466d5177e09e55289ac7eade579a642578242c7a3a9f91ad5c6583337a9d15"},
+ "ecto_sql": {:hex, :ecto_sql, "3.9.1", "9bd5894eecc53d5b39d0c95180d4466aff00e10679e13a5cfa725f6f85c03c22", [:mix], [{:db_connection, "~> 2.5 or ~> 2.4.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.6.0", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.16.0 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "5fd470a4fff2e829bbf9dcceb7f3f9f6d1e49b4241e802f614de6b8b67c51118"},
"elixir_make": {:hex, :elixir_make, "0.6.3", "bc07d53221216838d79e03a8019d0839786703129599e9619f4ab74c8c096eac", [:mix], [], "hexpm", "f5cbd651c5678bcaabdbb7857658ee106b12509cd976c2c2fca99688e1daf716"},
"elixir_talk": {:hex, :elixir_talk, "1.2.0", "f246f401ee3188f0aa5500a1b7cc2aadb0b7075e709bcfcf922ca1e0f517258f", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:yamerl, "~> 0.4.0", [hex: :yamerl, repo: "hexpm", optional: false]}, {:yaml_elixir, "~> 1.0.0", [hex: :yaml_elixir, repo: "hexpm", optional: false]}], "hexpm", "3aa1e22c7f159cb7bf0727b1ab9d070f4348a824a19ac360fca139b5ef38646b"},
"erlex": {:hex, :erlex, "0.2.6", "c7987d15e899c7a2f34f5420d2a2ea0d659682c06ac607572df55a43753aa12e", [:mix], [], "hexpm", "2ed2e25711feb44d52b17d2780eabf998452f6efda104877a3881c2f8c0c0c75"},
@@ -47,7 +47,7 @@
"ex_cldr_lists": {:hex, :ex_cldr_lists, "2.10.0", "4d4c9877da2d0417fd832907d69974e8328969f75fafc79b05ccf85f549f6281", [:mix], [{:ex_cldr_numbers, "~> 2.25", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "adc040cde7b97f7fd7c0b35dd69ddb6fcf607303ae6355bb1851deae1f8b0652"},
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.28.0", "506f5d36a2b72a21bbcb6e55dfdc5c3ff7f1c07d65e516461125158d10661beb", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:digital_token, "~> 0.3 or ~> 1.0", [hex: :digital_token, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.34", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, ">= 2.14.2", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "83342ff668aedf3aa5c54b048ce1da0f91317b6596b14880a5f87d45cd1c49d2"},
"ex_cldr_units": {:hex, :ex_cldr_units, "3.15.0", "3a834dfaf4daa0723cac165d528eacdbc3f9daec580f817b2847007fe07afdca", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.10", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.28", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:ex_doc, "~> 0.18", [hex: :ex_doc, repo: "hexpm", optional: true]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:ratio, "~> 2.4", [hex: :ratio, repo: "hexpm", optional: false]}], "hexpm", "bac7c3f6042482869dd67445adddaec2c263f561a8c2035eac7bd5f9d5ae1691"},
- "ex_doc": {:hex, :ex_doc, "0.29.0", "4a1cb903ce746aceef9c1f9ae8a6c12b742a5461e6959b9d3b24d813ffbea146", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "f096adb8bbca677d35d278223361c7792d496b3fc0d0224c9d4bc2f651af5db1"},
+ "ex_doc": {:hex, :ex_doc, "0.29.1", "b1c652fa5f92ee9cf15c75271168027f92039b3877094290a75abcaac82a9f77", [:mix], [{:earmark_parser, "~> 1.4.19", [hex: :earmark_parser, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.14", [hex: :makeup_elixir, repo: "hexpm", optional: false]}, {:makeup_erlang, "~> 0.1", [hex: :makeup_erlang, repo: "hexpm", optional: false]}], "hexpm", "b7745fa6374a36daf484e2a2012274950e084815b936b1319aeebcf7809574f6"},
"ex_json_schema": {:hex, :ex_json_schema, "0.9.2", "c9a42e04e70cd70eb11a8903a22e8ec344df16edef4cb8e6ec84ed0caffc9f0f", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "4854329cb352b6c01c4c4b8dbfb3be14dc5bea19ea13e0eafade4ff22ba55224"},
"ex_keccak": {:hex, :ex_keccak, "0.6.0", "0e1f8974dd6630dd4fb0b64f9eabbceeffb9675da3ab95dea653798365802cf4", [:mix], [{:rustler, "~> 0.26", [hex: :rustler, repo: "hexpm", optional: false]}], "hexpm", "84b20cfe6a063edab311b2c8ff8b221698c84cbd5fbdba059e51636540142538"},
"ex_machina": {:hex, :ex_machina, "2.7.0", "b792cc3127fd0680fecdb6299235b4727a4944a09ff0fa904cc639272cd92dc7", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm", "419aa7a39bde11894c87a615c4ecaa52d8f107bbdd81d810465186f783245bf8"},
@@ -125,7 +125,7 @@
"ratio": {:hex, :ratio, "2.4.2", "c8518f3536d49b1b00d88dd20d49f8b11abb7819638093314a6348139f14f9f9", [:mix], [{:decimal, "~> 1.6 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}, {:numbers, "~> 5.2.0", [hex: :numbers, repo: "hexpm", optional: false]}], "hexpm", "441ef6f73172a3503de65ccf1769030997b0d533b1039422f1e5e0e0b4cbf89e"},
"recon": {:hex, :recon, "2.5.2", "cba53fa8db83ad968c9a652e09c3ed7ddcc4da434f27c3eaa9ca47ffb2b1ff03", [:mix, :rebar3], [], "hexpm", "2c7523c8dee91dff41f6b3d63cba2bd49eb6d2fe5bf1eec0df7f87eb5e230e1c"},
"redix": {:hex, :redix, "1.2.0", "0d7eb3ccb7b82c461a6ea28b65c2c04486093d816dd6d901a09164800e004df1", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "e1e0deb14599da07c77e66956a12863e85ee270ada826804a0ba8e61657e22a3"},
- "remote_ip": {:hex, :remote_ip, "1.0.0", "3d7fb45204a5704443f480cee9515e464997f52c35e0a60b6ece1f81484067ae", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "9e9fcad4e50c43b5234bb6a9629ed6ab223f3ed07147bd35470e4ee5c8caf907"},
+ "remote_ip": {:hex, :remote_ip, "1.1.0", "cb308841595d15df3f9073b7c39243a1dd6ca56e5020295cb012c76fbec50f2d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:plug, "~> 1.14", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "616ffdf66aaad6a72fc546dabf42eed87e2a99e97b09cbd92b10cc180d02ed74"},
"rustler": {:hex, :rustler, "0.26.0", "06a2773d453ee3e9109efda643cf2ae633dedea709e2455ac42b83637c9249bf", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "42961e9d2083d004d5a53e111ad1f0c347efd9a05cb2eb2ffa1d037cdc74db91"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
"spandex": {:hex, :spandex, "3.2.0", "f8cd40146ea988c87f3c14054150c9a47ba17e53cd4515c00e1f93c29c45404d", [:mix], [{:decorator, "~> 1.2", [hex: :decorator, repo: "hexpm", optional: true]}, {:optimal, "~> 0.3.3", [hex: :optimal, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "d0a7d5aef4c5af9cf5467f2003e8a5d8d2bdae3823a6cc95d776b9a2251d4d03"},
|