<%= get_flash(@conn, :info) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex index 0541d12f69..c3407643b5 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex @@ -1,6 +1,6 @@ -<% minimal_proxy_template = Chain.get_minimal_proxy_template(@address.hash) %> -<% metadata_for_verification = minimal_proxy_template || Chain.get_address_verified_twin_contract(@address.hash).verified_contract %> -<% smart_contract_verified = BlockScoutWeb.AddressView.smart_contract_verified?(@address) %> +<% minimal_proxy_template = if assigns[:custom_abi], do: nil, else: Chain.get_minimal_proxy_template(@address.hash) %> +<% metadata_for_verification = if assigns[:custom_abi], do: nil, else: minimal_proxy_template || Chain.get_address_verified_twin_contract(@address.hash).verified_contract %> +<% smart_contract_verified = if assigns[:custom_abi], do: false, else: BlockScoutWeb.AddressView.smart_contract_verified?(@address) %> <%= unless smart_contract_verified do %> <%= if metadata_for_verification do %> <%= if minimal_proxy_template do %> @@ -21,9 +21,6 @@ <%= if smart_contract_verified && @address.smart_contract.is_changed_bytecode do %> <%= render BlockScoutWeb.CommonComponentsView, "_changed_bytecode_warning.html" %> <% end %> -<%= if @action == "write" or (@read_functions_required_wallet && @read_functions_required_wallet != []) do %> - <%= render BlockScoutWeb.SmartContractView, "_connect_container.html" %> -<% end %> <%= if @contract_type == "proxy" do %>Implementation address:
<%= link( @@ -33,7 +30,7 @@
+
>
<%= counter %>.
<%= case function["type"] do %>
@@ -49,12 +46,6 @@
<%= if queryable?(function["inputs"]) || writable?(function) || Helper.read_with_wallet_method?(function) do %>
-
- From
-
-
- <%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: BlockScoutWeb.AddressView.contract?(from_address), use_custom_tooltip: false, trimmed: false %>
-
-
- <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html",
- additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"],
- clipboard_text: from_address,
- aria_label: gettext("Copy From Address"),
- title: gettext("Copy From Address"),
- style: "position: relative;" %>
-
+
+ From
+
+
+ <%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: BlockScoutWeb.AddressView.contract?(from_address), use_custom_tooltip: false, trimmed: false %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", tags: from_tags %>
+
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html",
+additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"],
+clipboard_text: from_address,
+aria_label: gettext("Copy From Address"),
+title: gettext("Copy From Address"),
+style: "position: relative;" %>
+
-
- To
-
-
- <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %>
-
-
-
- <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html",
- additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"],
- clipboard_text: to_address,
- aria_label: gettext("Copy To Address"),
- title: gettext("Copy To Address"),
- style: "position: relative;"%>
-
+
+ To
+
+
+ <%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", tags: to_tags %>
+
+
+ <%= render BlockScoutWeb.CommonComponentsView, "_btn_copy_for_table.html",
+additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders", "btn-copy-token-transfer"],
+clipboard_text: to_address,
+aria_label: gettext("Copy To Address"),
+title: gettext("Copy To Address"),
+style: "position: relative;"%>
+
-
- For
-
- <% end %>
-
- <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", transfer: @transfer %>
-
+
+ For
+
+<% end %>
+
+ <%= render BlockScoutWeb.TransactionView, "_total_transfers.html", transfer: @transfer %>
+
-
\ No newline at end of file
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
index 217b702e35..e6661c5e05 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
@@ -48,6 +48,11 @@
<%= if status == :pending do %>
@@ -195,6 +200,7 @@
<%= gettext "From" %>
<%= render BlockScoutWeb.AddressView, "_link.html", address: from_address, contract: BlockScoutWeb.AddressView.contract?(from_address), use_custom_tooltip: false, trimmed: false %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @from_tags %>
<%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html",
additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"],
clipboard_text: from_address_hash,
@@ -220,6 +226,7 @@
<% created_address_hash -> %>
[<%= gettext("Contract") %>
<%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @to_tags %>
<%= gettext("created") %>]
<%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html",
additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"],
@@ -228,6 +235,7 @@
title: gettext("Copy To Address") %>
<% recipient_address_hash -> %>
<%= render BlockScoutWeb.AddressView, "_link.html", address: to_address, contract: BlockScoutWeb.AddressView.contract?(to_address), use_custom_tooltip: false, trimmed: false %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", tags: @to_tags %>
<%= render BlockScoutWeb.CommonComponentsView, "_btn_copy.html",
additional_classes: ["btn-copy-icon-small", "btn-copy-icon-custom", "btn-copy-icon-no-borders"],
clipboard_text: recipient_address_hash,
diff --git a/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex
index c89ac10435..ba8960088b 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/access_helpers.ex
@@ -8,6 +8,7 @@ defmodule BlockScoutWeb.AccessHelpers do
alias BlockScoutWeb.API.APILogger
alias BlockScoutWeb.API.RPC.RPCView
alias BlockScoutWeb.WebRouter.Helpers
+ alias Explorer.Account.Api.Key, as: ApiKey
alias Plug.Conn
alias RemoteIp
@@ -81,11 +82,17 @@ defmodule BlockScoutWeb.AccessHelpers do
ip = remote_ip_from_headers || remote_ip
ip_string = to_string(:inet_parse.ntoa(ip))
+ plan = get_plan(conn.query_params)
+
cond do
- conn.query_params && Map.has_key?(conn.query_params, "apikey") &&
- Map.get(conn.query_params, "apikey") == static_api_key ->
+ check_api_key(conn) && get_api_key(conn) == static_api_key ->
rate_limit_by_key(static_api_key, api_rate_limit_by_key)
+ check_api_key(conn) && !is_nil(plan) ->
+ conn
+ |> get_api_key()
+ |> rate_limit_by_key(plan.max_req_per_second)
+
Enum.member?(api_rate_limit_whitelisted_ips(), ip_string) ->
rate_limit_by_ip(ip_string, api_rate_limit_by_ip)
@@ -95,6 +102,26 @@ defmodule BlockScoutWeb.AccessHelpers do
end
end
+ defp check_api_key(conn) do
+ conn.query_params && Map.has_key?(conn.query_params, "apikey")
+ end
+
+ defp get_api_key(conn) do
+ Map.get(conn.query_params, "apikey")
+ end
+
+ defp get_plan(query_params) do
+ with true <- query_params && Map.has_key?(query_params, "apikey"),
+ api_key_value <- Map.get(query_params, "apikey"),
+ api_key <- ApiKey.api_key_with_plan_by_value(api_key_value),
+ false <- is_nil(api_key) do
+ api_key.identity.plan
+ else
+ _ ->
+ nil
+ end
+ end
+
defp rate_limit_by_key(api_key, api_rate_limit_by_key) do
case Hammer.check_rate("api-#{api_key}", 1_000, api_rate_limit_by_key) do
{:allow, _count} ->
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/account_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/account_view.ex
new file mode 100644
index 0000000000..0e3a65e616
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/account_view.ex
@@ -0,0 +1,7 @@
+defmodule BlockScoutWeb.Account.Api.V1.AccountView do
+ def render("message.json", %{message: message}) do
+ %{
+ "message" => message
+ }
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/tags_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/tags_view.ex
new file mode 100644
index 0000000000..d97e35f914
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/tags_view.ex
@@ -0,0 +1,27 @@
+defmodule BlockScoutWeb.Account.Api.V1.TagsView do
+ def render("address_tags.json", %{tags_map: tags_map}) do
+ tags_map
+ end
+
+ def render("transaction_tags.json", %{
+ tags_map: %{
+ personal_tags: personal_tags,
+ watchlist_names: watchlist_names,
+ personal_tx_tag: personal_tx_tag,
+ common_tags: common_tags
+ }
+ }) do
+ %{
+ personal_tx_tag: prepare_transaction_tag(personal_tx_tag),
+ personal_tags: personal_tags,
+ watchlist_names: watchlist_names,
+ common_tags: common_tags
+ }
+ end
+
+ def prepare_transaction_tag(nil), do: nil
+
+ def prepare_transaction_tag(transaction_tag) do
+ %{"label" => transaction_tag.name}
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex
new file mode 100644
index 0000000000..91299034a1
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex
@@ -0,0 +1,141 @@
+defmodule BlockScoutWeb.Account.Api.V1.UserView do
+ alias BlockScoutWeb.Account.Api.V1.AccountView
+ alias Ecto.Changeset
+
+ def render("message.json", assigns) do
+ AccountView.render("message.json", assigns)
+ end
+
+ def render("user_info.json", %{identity: identity}) do
+ %{"name" => identity.name, "email" => identity.email, "avatar" => identity.avatar, "nickname" => identity.nickname}
+ end
+
+ def render("watchlist_addresses.json", %{watchlist_addresses: watchlist_addresses, exchange_rate: exchange_rate}) do
+ Enum.map(watchlist_addresses, &prepare_watchlist_address(&1, exchange_rate))
+ end
+
+ def render("watchlist_address.json", %{watchlist_address: watchlist_address, exchange_rate: exchange_rate}) do
+ prepare_watchlist_address(watchlist_address, exchange_rate)
+ end
+
+ def render("address_tags.json", %{address_tags: address_tags}) do
+ Enum.map(address_tags, &prepare_address_tag/1)
+ end
+
+ def render("address_tag.json", %{address_tag: address_tag}) do
+ prepare_address_tag(address_tag)
+ end
+
+ def render("transaction_tags.json", %{transaction_tags: transaction_tags}) do
+ Enum.map(transaction_tags, &prepare_transaction_tag/1)
+ end
+
+ def render("transaction_tag.json", %{transaction_tag: transaction_tag}) do
+ prepare_transaction_tag(transaction_tag)
+ end
+
+ def render("api_keys.json", %{api_keys: api_keys}) do
+ Enum.map(api_keys, &prepare_api_key/1)
+ end
+
+ def render("api_key.json", %{api_key: api_key}) do
+ prepare_api_key(api_key)
+ end
+
+ def render("custom_abis.json", %{custom_abis: custom_abis}) do
+ Enum.map(custom_abis, &prepare_custom_abi/1)
+ end
+
+ def render("custom_abi.json", %{custom_abi: custom_abi}) do
+ prepare_custom_abi(custom_abi)
+ end
+
+ def render("public_tags_requests.json", %{public_tags_requests: public_tags_requests}) do
+ Enum.map(public_tags_requests, &prepare_public_tags_request/1)
+ end
+
+ def render("public_tags_request.json", %{public_tags_request: public_tags_request}) do
+ prepare_public_tags_request(public_tags_request)
+ end
+
+ def render("changeset_errors.json", %{changeset: changeset}) do
+ %{
+ "errors" =>
+ Changeset.traverse_errors(changeset, fn {msg, opts} ->
+ Regex.replace(~r"%{(\w+)}", msg, fn _, key ->
+ opts |> Keyword.get(String.to_existing_atom(key), key) |> to_string()
+ end)
+ end)
+ }
+ end
+
+ def prepare_watchlist_address(watchlist, exchange_rate) do
+ %{
+ "id" => watchlist.id,
+ "address_hash" => watchlist.address_hash,
+ "name" => watchlist.name,
+ "address_balance" => if(watchlist.fetched_coin_balance, do: watchlist.fetched_coin_balance.value),
+ "exchange_rate" => exchange_rate.usd_value,
+ "notification_settings" => %{
+ "native" => %{
+ "incoming" => watchlist.watch_coin_input,
+ "outcoming" => watchlist.watch_coin_output
+ },
+ "ERC-20" => %{
+ "incoming" => watchlist.watch_erc_20_input,
+ "outcoming" => watchlist.watch_erc_20_output
+ },
+ "ERC-721" => %{
+ "incoming" => watchlist.watch_erc_721_input,
+ "outcoming" => watchlist.watch_erc_721_output
+ }
+ # ,
+ # "ERC-1155" => %{
+ # "incoming" => watchlist.watch_erc_1155_input,
+ # "outcoming" => watchlist.watch_erc_1155_output
+ # }
+ },
+ "notification_methods" => %{
+ "email" => watchlist.notify_email
+ }
+ }
+ end
+
+ def prepare_custom_abi(custom_abi) do
+ %{
+ "id" => custom_abi.id,
+ "contract_address_hash" => custom_abi.address_hash,
+ "name" => custom_abi.name,
+ "abi" => custom_abi.abi
+ }
+ end
+
+ def prepare_api_key(api_key) do
+ %{"api_key" => api_key.value, "name" => api_key.name}
+ end
+
+ def prepare_address_tag(address_tag) do
+ %{"id" => address_tag.id, "address_hash" => address_tag.address_hash, "name" => address_tag.name}
+ end
+
+ def prepare_transaction_tag(nil), do: nil
+
+ def prepare_transaction_tag(transaction_tag) do
+ %{"id" => transaction_tag.id, "transaction_hash" => transaction_tag.tx_hash, "name" => transaction_tag.name}
+ end
+
+ def prepare_public_tags_request(public_tags_request) do
+ %{
+ "id" => public_tags_request.id,
+ "full_name" => public_tags_request.full_name,
+ "email" => public_tags_request.email,
+ "company" => public_tags_request.company,
+ "website" => public_tags_request.website,
+ "tags" => public_tags_request.tags,
+ "addresses" => public_tags_request.addresses,
+ "additional_comment" => public_tags_request.additional_comment,
+ "is_owner" => public_tags_request.is_owner,
+ "submission_date" => public_tags_request.inserted_at
+ }
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex
new file mode 100644
index 0000000000..a0b21b79da
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/api_key_view.ex
@@ -0,0 +1,5 @@
+defmodule BlockScoutWeb.Account.ApiKeyView do
+ use BlockScoutWeb, :view
+
+ alias Explorer.Account.Api.Key
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex
new file mode 100644
index 0000000000..cfbeb001e0
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/auth_view.ex
@@ -0,0 +1,3 @@
+defmodule BlockScoutWeb.Account.AuthView do
+ use BlockScoutWeb, :view
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex
new file mode 100644
index 0000000000..e296ca90d6
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/common_view.ex
@@ -0,0 +1,11 @@
+defmodule BlockScoutWeb.Account.CommonView do
+ use BlockScoutWeb, :view
+
+ def nav_class(active_item, item) do
+ if active_item == item do
+ "dropdown-item active fs-14"
+ else
+ "dropdown-item fs-14"
+ end
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex
new file mode 100644
index 0000000000..3b38effa41
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/custom_abi_view.ex
@@ -0,0 +1,22 @@
+defmodule BlockScoutWeb.Account.CustomABIView do
+ use BlockScoutWeb, :view
+
+ alias Ecto.Changeset
+ alias Explorer.Account.CustomABI
+
+ def format_abi(custom_abi) do
+ with {_type, abi} <- Changeset.fetch_field(custom_abi, :abi),
+ false <- is_nil(abi),
+ {:binary, false} <- {:binary, is_binary(abi)},
+ {:ok, encoded_abi} <- Poison.encode(abi) do
+ encoded_abi
+ else
+ {:binary, true} ->
+ {_type, abi} = Changeset.fetch_field(custom_abi, :abi)
+ abi
+
+ _ ->
+ ""
+ end
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex
new file mode 100644
index 0000000000..2a13dd8d7e
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/public_tags_request_view.ex
@@ -0,0 +1,70 @@
+defmodule BlockScoutWeb.Account.PublicTagsRequestView do
+ use BlockScoutWeb, :view
+ use Phoenix.HTML
+
+ alias Explorer.Account.PublicTagsRequest
+ alias Phoenix.HTML.Form
+
+ def array_input(form, field, attrs \\ []) do
+ values = Form.input_value(form, field) || [""]
+ id = Form.input_id(form, field)
+
+ content_tag :ul,
+ id: container_id(id),
+ data: [index: Enum.count(values), multiple_input_field_container: ""],
+ class: "multiple-input-fields-container" do
+ values
+ |> Enum.map(fn v ->
+ form_elements(form, field, to_string(v), attrs)
+ end)
+ end
+ end
+
+ def array_add_button(form, field, attrs \\ []) do
+ id = Form.input_id(form, field)
+
+ content =
+ form
+ |> form_elements(field, "", attrs)
+ |> safe_to_string
+
+ data = [
+ prototype: content,
+ container: container_id(id)
+ ]
+
+ content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_plus.html"),
+ data: data,
+ class: "add-form-field"
+ )
+ end
+
+ defp form_elements(form, field, k, attrs) do
+ type = Form.input_type(form, field)
+ id = Form.input_id(form, field)
+
+ input_opts =
+ [
+ name: new_field_name(form, field),
+ value: k,
+ id: id,
+ class: "form-control public-tags-address"
+ ] ++ attrs
+
+ content_tag :li, class: "public-tags-address form-group" do
+ [
+ apply(Form, type, [form, field, input_opts]),
+ content_tag(:button, render(BlockScoutWeb.CommonComponentsView, "_svg_minus.html"),
+ data: [container: container_id(id)],
+ class: "remove-form-field ml-1"
+ )
+ ]
+ end
+ end
+
+ defp container_id(id), do: id <> "_container"
+
+ defp new_field_name(form, field) do
+ Form.input_name(form, field) <> "[]"
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex
new file mode 100644
index 0000000000..74886c33e9
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/tag_address_view.ex
@@ -0,0 +1,7 @@
+defmodule BlockScoutWeb.Account.TagAddressView do
+ use BlockScoutWeb, :view
+
+ import BlockScoutWeb.AddressView, only: [trimmed_hash: 1]
+
+ alias Explorer.Account.TagAddress
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex
new file mode 100644
index 0000000000..7edfa1e340
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/tag_transaction_view.ex
@@ -0,0 +1,5 @@
+defmodule BlockScoutWeb.Account.TagTransactionView do
+ use BlockScoutWeb, :view
+
+ alias Explorer.Account.TagTransaction
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex
new file mode 100644
index 0000000000..f3f538399e
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_address_view.ex
@@ -0,0 +1,11 @@
+defmodule BlockScoutWeb.Account.WatchlistAddressView do
+ use BlockScoutWeb, :view
+ import BlockScoutWeb.AddressView, only: [trimmed_hash: 1]
+ import BlockScoutWeb.WeiHelpers, only: [format_wei_value: 2]
+
+ def balance_ether(nil), do: ""
+
+ def balance_ether(balance) do
+ format_wei_value(balance, :ether)
+ end
+end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex
new file mode 100644
index 0000000000..fa39663236
--- /dev/null
+++ b/apps/block_scout_web/lib/block_scout_web/views/account/watchlist_view.ex
@@ -0,0 +1,17 @@
+defmodule BlockScoutWeb.Account.WatchlistView do
+ use BlockScoutWeb, :view
+
+ alias BlockScoutWeb.Account.WatchlistAddressView
+ alias Explorer.Account.WatchlistAddress
+ alias Explorer.ExchangeRates.Token
+ alias Explorer.Market
+ alias Indexer.Fetcher.CoinBalanceOnDemand
+
+ def coin_balance_status(address) do
+ CoinBalanceOnDemand.trigger_fetch(address)
+ end
+
+ def exchange_rate do
+ Market.get_exchange_rate(Explorer.coin()) || Token.null()
+ end
+end
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 f0b262d291..8fff82fb89 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
@@ -4,12 +4,15 @@ defmodule BlockScoutWeb.AddressView do
require Logger
alias BlockScoutWeb.{AccessHelpers, LayoutView}
+ alias Explorer.Account.CustomABI
alias Explorer.{Chain, CustomContractsHelpers, Repo}
alias Explorer.Chain.{Address, Hash, InternalTransaction, SmartContract, Token, TokenTransfer, Transaction, Wei}
alias Explorer.Chain.Block.Reward
alias Explorer.ExchangeRates.Token, as: TokenExchangeRate
alias Explorer.SmartContract.{Helper, Writer}
+ import BlockScoutWeb.Account.AuthController, only: [current_user: 1]
+
@dialyzer :no_match
@tabs [
@@ -446,4 +449,33 @@ defmodule BlockScoutWeb.AddressView do
end
def smart_contract_is_gnosis_safe_proxy?(_address), do: false
+
+ def tag_name_to_label(tag_name) do
+ tag_name
+ |> String.replace(" ", "-")
+ end
+
+ def fetch_custom_abi(conn, address_hash) do
+ if current_user = current_user(conn) do
+ CustomABI.get_custom_abi_by_identity_id_and_address_hash(address_hash, current_user.id)
+ end
+ end
+
+ def has_address_custom_abi_with_read_functions?(conn, address_hash) do
+ custom_abi = fetch_custom_abi(conn, address_hash)
+
+ check_custom_abi_for_having_read_functions(custom_abi)
+ end
+
+ def check_custom_abi_for_having_read_functions(custom_abi),
+ do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &is_read_function?(&1))
+
+ def has_address_custom_abi_with_write_functions?(conn, address_hash) do
+ custom_abi = fetch_custom_abi(conn, address_hash)
+
+ check_custom_abi_for_having_write_functions(custom_abi)
+ end
+
+ def check_custom_abi_for_having_write_functions(custom_abi),
+ do: !is_nil(custom_abi) && Enum.any?(custom_abi.abi, &Writer.write_function?(&1))
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/error_view.ex b/apps/block_scout_web/lib/block_scout_web/views/error_view.ex
index 22da72d0a2..090159d946 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/error_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/error_view.ex
@@ -1,15 +1,28 @@
defmodule BlockScoutWeb.ErrorView do
use BlockScoutWeb, :view
- def render("404.html", _assigns) do
+ # when type in ["json", "html"]
+ def render("404." <> _type, _assigns) do
"Page not found"
end
- def render("422.html", _assigns) do
+ def render("400." <> _type, _assigns) do
+ "Bad request"
+ end
+
+ def render("401." <> _type, _assigns) do
+ "Unauthorized"
+ end
+
+ def render("403." <> _type, _assigns) do
+ "Forbidden"
+ end
+
+ def render("422." <> _type, _assigns) do
"Unprocessable entity"
end
- def render("500.html", _assigns) do
+ def render("500." <> _type, _assigns) do
"Internal server error"
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
index 16907be092..f60152575b 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
@@ -252,4 +252,29 @@ defmodule BlockScoutWeb.LayoutView do
end
defp validate_url(_), do: :error
+
+ def sign_in_link do
+ if Mix.env() == :test do
+ "/auth/auth0"
+ else
+ Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path] <> "auth/auth0"
+ end
+ end
+
+ def sign_out_link do
+ client_id = Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_id]
+ return_to = Application.get_env(:ueberauth, Ueberauth)[:logout_return_to_url]
+ logout_url = Application.get_env(:ueberauth, Ueberauth)[:logout_url]
+
+ if client_id && return_to && logout_url do
+ params = [
+ client_id: client_id,
+ returnTo: return_to
+ ]
+
+ [logout_url, "?", URI.encode_query(params)]
+ else
+ []
+ end
+ end
end
diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
index 31d9feffff..72f300bc4c 100644
--- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
+++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
@@ -2,6 +2,7 @@ defmodule BlockScoutWeb.TransactionView do
use BlockScoutWeb, :view
alias BlockScoutWeb.{AccessHelpers, AddressView, BlockView, TabHelpers}
+ alias BlockScoutWeb.Account.AuthController
alias BlockScoutWeb.Cldr.Number
alias Explorer.{Chain, CustomContractsHelpers, Repo}
alias Explorer.Chain.Block.Reward
@@ -11,7 +12,7 @@ defmodule BlockScoutWeb.TransactionView do
alias Timex.Duration
import BlockScoutWeb.Gettext
- import BlockScoutWeb.AddressView, only: [from_address_hash: 1, short_token_id: 2]
+ import BlockScoutWeb.AddressView, only: [from_address_hash: 1, short_token_id: 2, tag_name_to_label: 1]
import BlockScoutWeb.Tokens.Helpers
@tabs ["token-transfers", "internal-transactions", "logs", "raw-trace"]
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 c3ad18e43d..4350a4fc6f 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
@@ -3,6 +3,9 @@ defmodule BlockScoutWeb.WebRouter do
Router for web app
"""
use BlockScoutWeb, :router
+ require Ueberauth
+
+ alias BlockScoutWeb.Plug.CheckAccountWeb
pipeline :browser do
plug(:accepts, ["html"])
@@ -13,6 +16,69 @@ defmodule BlockScoutWeb.WebRouter do
plug(BlockScoutWeb.ChecksumAddress)
end
+ pipeline :account do
+ plug(:accepts, ["html"])
+ plug(:fetch_session)
+ plug(:fetch_flash)
+ plug(CheckAccountWeb)
+ plug(:protect_from_forgery)
+ plug(BlockScoutWeb.CSPHeader)
+ plug(BlockScoutWeb.ChecksumAddress)
+ end
+
+ if Mix.env() == :dev do
+ forward("/sent_emails", Bamboo.SentEmailViewerPlug)
+ end
+
+ scope "/auth", BlockScoutWeb do
+ pipe_through(:account)
+
+ get("/profile", Account.AuthController, :profile)
+ get("/logout", Account.AuthController, :logout)
+ get("/:provider", Account.AuthController, :request)
+ get("/:provider/callback", Account.AuthController, :callback)
+ end
+
+ scope "/account", BlockScoutWeb do
+ pipe_through(:account)
+
+ resources("/tag_address", Account.TagAddressController,
+ only: [:index, :new, :create, :delete],
+ as: :tag_address
+ )
+
+ resources("/tag_transaction", Account.TagTransactionController,
+ only: [:index, :new, :create, :delete],
+ as: :tag_transaction
+ )
+
+ resources("/watchlist", Account.WatchlistController,
+ only: [:show],
+ singleton: true,
+ as: :watchlist
+ )
+
+ resources("/watchlist_address", Account.WatchlistAddressController,
+ only: [:new, :create, :edit, :update, :delete],
+ as: :watchlist_address
+ )
+
+ resources("/api_key", Account.ApiKeyController,
+ only: [:new, :create, :edit, :update, :delete, :index],
+ as: :api_key
+ )
+
+ resources("/custom_abi", Account.CustomABIController,
+ only: [:new, :create, :edit, :update, :delete, :index],
+ as: :custom_abi
+ )
+
+ resources("/public_tags_request", Account.PublicTagsRequestController,
+ only: [:new, :create, :edit, :update, :delete, :index],
+ as: :public_tags_request
+ )
+ end
+
# Disallows Iframes (write routes)
scope "/", BlockScoutWeb do
pipe_through(:browser)
@@ -40,7 +106,10 @@ defmodule BlockScoutWeb.WebRouter do
resources("/blocks", BlockController, as: :blocks, only: [:index])
- resources "/blocks", BlockController, as: :block_secondary, only: [:show], param: "hash_or_number" do
+ resources "/blocks", BlockController,
+ as: :block_secondary,
+ only: [:show],
+ param: "hash_or_number" do
resources("/transactions", BlockTransactionController, only: [:index], as: :transaction)
end
diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs
index 6de44d043e..459eca37d7 100644
--- a/apps/block_scout_web/mix.exs
+++ b/apps/block_scout_web/mix.exs
@@ -44,6 +44,7 @@ defmodule BlockScoutWeb.Mixfile do
defp extra_applications,
do: [
+ :ueberauth_auth0,
:logger,
:runtime_tools
]
@@ -103,7 +104,7 @@ defmodule BlockScoutWeb.Mixfile do
{:plug_cowboy, "~> 2.2"},
# Waiting for the Pretty Print to be implemented at the Jason lib
# https://github.com/michalmuskala/jason/issues/15
- {:poison, "~> 5.0.0"},
+ {:poison, "~> 4.0.1"},
{:postgrex, ">= 0.0.0"},
# For compatibility with `prometheus_process_collector`, which hasn't been updated yet
{:prometheus, "~> 4.0", override: true},
@@ -127,7 +128,11 @@ defmodule BlockScoutWeb.Mixfile do
# `:cowboy` `~> 2.0` and Phoenix 1.4 compatibility
{:websocket_client, "~> 1.3"},
{:wobserver, "~> 0.2.0", github: "poanetwork/wobserver", branch: "support-https"},
- {:ex_json_schema, "~> 0.9.1"}
+ {:ex_json_schema, "~> 0.9.1"},
+ {:ueberauth, "~> 0.7"},
+ {:ueberauth_auth0, "~> 2.0"},
+ {:bureaucrat, "~> 0.2.9", only: :test},
+ {:poison, "~> 4.0.0"}
]
end
diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot
index c49c49c37a..92fdcb21ae 100644
--- a/apps/block_scout_web/priv/gettext/default.pot
+++ b/apps/block_scout_web/priv/gettext/default.pot
@@ -63,7 +63,7 @@ msgstr ""
msgid "%{subnetwork} Explorer - BlockScout"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:349
+#: lib/block_scout_web/views/transaction_view.ex:350
#, elixir-autogen, elixir-format
msgid "(Awaiting internal transactions for status)"
msgstr ""
@@ -101,6 +101,11 @@ msgstr ""
msgid "A string with the name of the module to be invoked."
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "ABI"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3
#, elixir-autogen, elixir-format
msgid "ABI-encoded Constructor Arguments (if required by the contract)"
@@ -126,48 +131,83 @@ msgstr ""
msgid "APIs"
msgstr ""
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69
#, elixir-autogen, elixir-format
msgid "Action"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:417
+#: lib/block_scout_web/templates/transaction/overview.html.eex:425
#, elixir-autogen, elixir-format
msgid "Actual gas amount used by the transaction."
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8
#: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:11
#, elixir-autogen, elixir-format
msgid "Add"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:44
+#, elixir-autogen, elixir-format
+msgid "Add API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44
+#, elixir-autogen, elixir-format
+msgid "Add Custom ABI"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37
+#, elixir-autogen, elixir-format
+msgid "Add address tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37
+#, elixir-autogen, elixir-format
+msgid "Add transaction tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20
-#: lib/block_scout_web/views/address_view.ex:104
+#: lib/block_scout_web/views/address_view.ex:107
#, elixir-autogen, elixir-format
msgid "Address"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:211
+#: lib/block_scout_web/templates/transaction/overview.html.eex:217
#, elixir-autogen, elixir-format
msgid "Address (external or contract) receiving the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:194
+#: lib/block_scout_web/templates/transaction/overview.html.eex:199
#, elixir-autogen, elixir-format
msgid "Address (external or contract) sending the transaction."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:167
+#: lib/block_scout_web/templates/address/overview.html.eex:149
#, elixir-autogen, elixir-format
msgid "Address balance in"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:50
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51
#, elixir-autogen, elixir-format
msgid "Address of the token contract"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2
+#, elixir-autogen, elixir-format
+msgid "Address*"
+msgstr ""
+
#: lib/block_scout_web/templates/address/index.html.eex:5
#, elixir-autogen, elixir-format
msgid "Addresses"
@@ -194,12 +234,12 @@ msgstr ""
msgid "All metadata displayed below is from that contract. In order to verify current contract, click"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:192
+#: lib/block_scout_web/templates/address/overview.html.eex:174
#, elixir-autogen, elixir-format
msgid "All tokens in the account and total value."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:403
+#: lib/block_scout_web/templates/transaction/overview.html.eex:411
#, elixir-autogen, elixir-format
msgid "Amount of"
msgstr ""
@@ -235,7 +275,8 @@ msgstr ""
msgid "Back Home"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:168
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24
+#: lib/block_scout_web/templates/address/overview.html.eex:150
#: lib/block_scout_web/templates/address_token/overview.html.eex:51
#, elixir-autogen, elixir-format
msgid "Balance"
@@ -257,14 +298,14 @@ msgstr ""
msgid "Base URL:"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:438
+#: lib/block_scout_web/templates/transaction/overview.html.eex:446
#, elixir-autogen, elixir-format
msgid "Binary data included with the transaction. See input / logs below for additional info."
msgstr ""
#: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8
#: lib/block_scout_web/templates/block/overview.html.eex:29
-#: lib/block_scout_web/templates/transaction/overview.html.eex:153
+#: lib/block_scout_web/templates/transaction/overview.html.eex:158
#, elixir-autogen, elixir-format
msgid "Block"
msgstr ""
@@ -296,7 +337,7 @@ msgstr ""
msgid "Block Mined, awaiting import..."
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:33
+#: lib/block_scout_web/views/transaction_view.ex:34
#, elixir-autogen, elixir-format
msgid "Block Pending"
msgstr ""
@@ -311,12 +352,12 @@ msgstr ""
msgid "Block not found, please try again later."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:152
+#: lib/block_scout_web/templates/transaction/overview.html.eex:157
#, elixir-autogen, elixir-format
msgid "Block number containing the transaction."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:275
+#: lib/block_scout_web/templates/address/overview.html.eex:257
#, elixir-autogen, elixir-format
msgid "Block number in which the address was updated."
msgstr ""
@@ -339,9 +380,9 @@ msgid "Blocks Indexed"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:48
-#: lib/block_scout_web/templates/address/overview.html.eex:293
+#: lib/block_scout_web/templates/address/overview.html.eex:275
#: lib/block_scout_web/templates/address_validation/index.html.eex:11
-#: lib/block_scout_web/views/address_view.ex:371
+#: lib/block_scout_web/views/address_view.ex:374
#, elixir-autogen, elixir-format
msgid "Blocks Validated"
msgstr ""
@@ -378,6 +419,7 @@ msgstr ""
msgid "Call Code"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120
#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:145
#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41
@@ -420,13 +462,13 @@ msgstr ""
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149
-#: lib/block_scout_web/views/address_view.ex:364
+#: lib/block_scout_web/views/address_view.ex:367
#, elixir-autogen, elixir-format
msgid "Code"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:34
-#: lib/block_scout_web/views/address_view.ex:370
+#: lib/block_scout_web/views/address_view.ex:373
#, elixir-autogen, elixir-format
msgid "Coin Balance History"
msgstr ""
@@ -436,6 +478,16 @@ msgstr ""
msgid "Collapse"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Company name"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32
+#, elixir-autogen, elixir-format
+msgid "Company website"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3
#, elixir-autogen, elixir-format
msgid "Compiler"
@@ -446,17 +498,17 @@ msgstr ""
msgid "Compiler version"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:342
+#: lib/block_scout_web/views/transaction_view.ex:343
#, elixir-autogen, elixir-format
msgid "Confirmed"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:119
+#: lib/block_scout_web/templates/transaction/overview.html.eex:124
#, elixir-autogen, elixir-format
msgid "Confirmed by "
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:185
+#: lib/block_scout_web/templates/transaction/overview.html.eex:190
#, elixir-autogen, elixir-format
msgid "Confirmed within"
msgstr ""
@@ -467,7 +519,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4
#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6
#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:15
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16
#, elixir-autogen, elixir-format
msgid "Connection Lost"
msgstr ""
@@ -500,8 +552,8 @@ msgstr ""
msgid "Constructor Arguments"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51
-#: lib/block_scout_web/templates/transaction/overview.html.eex:221
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52
+#: lib/block_scout_web/templates/transaction/overview.html.eex:227
#, elixir-autogen, elixir-format
msgid "Contract"
msgstr ""
@@ -511,25 +563,27 @@ msgstr ""
msgid "Contract ABI"
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3
-#: lib/block_scout_web/views/address_view.ex:102
+#: lib/block_scout_web/views/address_view.ex:105
#, elixir-autogen, elixir-format
msgid "Contract Address"
msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16
-#: lib/block_scout_web/views/address_view.ex:42
-#: lib/block_scout_web/views/address_view.ex:76
+#: lib/block_scout_web/views/address_view.ex:45
+#: lib/block_scout_web/views/address_view.ex:79
#, elixir-autogen, elixir-format
msgid "Contract Address Pending"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:457
+#: lib/block_scout_web/views/transaction_view.ex:458
#, elixir-autogen, elixir-format
msgid "Contract Call"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:454
+#: lib/block_scout_web/views/transaction_view.ex:455
#, elixir-autogen, elixir-format
msgid "Contract Creation"
msgstr ""
@@ -546,7 +600,7 @@ msgstr ""
msgid "Contract Libraries"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:93
+#: lib/block_scout_web/templates/address/overview.html.eex:75
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3
#, elixir-autogen, elixir-format
msgid "Contract Name"
@@ -583,16 +637,33 @@ msgstr ""
msgid "Copy ABI"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:37
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#, elixir-autogen, elixir-format
+msgid "Copy API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8
+#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8
+#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11
+#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7
#: lib/block_scout_web/templates/address/overview.html.eex:38
+#: lib/block_scout_web/templates/address/overview.html.eex:39
#: lib/block_scout_web/templates/block/overview.html.eex:104
#: lib/block_scout_web/templates/block/overview.html.eex:105
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:42
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44
#, elixir-autogen, elixir-format
msgid "Copy Address"
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6
+#, elixir-autogen, elixir-format
+msgid "Copy Contract Address"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract/index.html.eex:140
#: lib/block_scout_web/templates/address_contract/index.html.eex:156
#, elixir-autogen, elixir-format
@@ -604,16 +675,17 @@ msgstr ""
msgid "Copy Decompiled Contract Code"
msgstr ""
-#: lib/block_scout_web/templates/address_contract/index.html.eex:183
-#: lib/block_scout_web/templates/address_contract/index.html.eex:193
+#: lib/block_scout_web/templates/address_contract/index.html.eex:177
+#: lib/block_scout_web/templates/address_contract/index.html.eex:187
#, elixir-autogen, elixir-format
msgid "Copy Deployed ByteCode"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:14
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:15
-#: lib/block_scout_web/templates/transaction/overview.html.eex:201
-#: lib/block_scout_web/templates/transaction/overview.html.eex:202
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18
+#: lib/block_scout_web/templates/transaction/overview.html.eex:207
+#: lib/block_scout_web/templates/transaction/overview.html.eex:208
#, elixir-autogen, elixir-format
msgid "Copy From Address"
msgstr ""
@@ -646,12 +718,12 @@ msgstr ""
msgid "Copy Source Code"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:31
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:32
-#: lib/block_scout_web/templates/transaction/overview.html.eex:227
-#: lib/block_scout_web/templates/transaction/overview.html.eex:228
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35
#: lib/block_scout_web/templates/transaction/overview.html.eex:234
#: lib/block_scout_web/templates/transaction/overview.html.eex:235
+#: lib/block_scout_web/templates/transaction/overview.html.eex:242
+#: lib/block_scout_web/templates/transaction/overview.html.eex:243
#, elixir-autogen, elixir-format
msgid "Copy To Address"
msgstr ""
@@ -662,30 +734,30 @@ msgstr ""
msgid "Copy Token ID"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:82
+#: lib/block_scout_web/templates/transaction/overview.html.eex:87
#, elixir-autogen, elixir-format
msgid "Copy Transaction Hash"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:83
+#: lib/block_scout_web/templates/transaction/overview.html.eex:88
#, elixir-autogen, elixir-format
msgid "Copy Txn Hash"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:464
+#: lib/block_scout_web/templates/transaction/overview.html.eex:472
#, elixir-autogen, elixir-format
msgid "Copy Txn Hex Input"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:470
+#: lib/block_scout_web/templates/transaction/overview.html.eex:478
#, elixir-autogen, elixir-format
msgid "Copy Txn UTF-8 Input"
msgstr ""
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20
#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41
-#: lib/block_scout_web/templates/transaction/overview.html.eex:463
-#: lib/block_scout_web/templates/transaction/overview.html.eex:469
+#: lib/block_scout_web/templates/transaction/overview.html.eex:471
+#: lib/block_scout_web/templates/transaction/overview.html.eex:477
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "Copy Value"
@@ -696,12 +768,22 @@ msgstr ""
msgid "Create"
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12
+#, elixir-autogen, elixir-format
+msgid "Create a Custom ABI to interact with contracts."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:12
+#, elixir-autogen, elixir-format
+msgid "Create an API key to use with your RPC и EthRPC API requests."
+msgstr ""
+
#: lib/block_scout_web/views/internal_transaction_view.ex:26
#, elixir-autogen, elixir-format
msgid "Create2"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:120
+#: lib/block_scout_web/templates/address/overview.html.eex:102
#, elixir-autogen, elixir-format
msgid "Creator"
msgstr ""
@@ -712,11 +794,31 @@ msgstr ""
msgid "Curl"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:92
+#: lib/block_scout_web/templates/transaction/overview.html.eex:97
#, elixir-autogen, elixir-format
msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)"
msgstr ""
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "Custom"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:19
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "Custom ABI"
+msgstr ""
+
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23
+#, elixir-autogen, elixir-format
+msgid "Custom ABI from account"
+msgstr ""
+
#: lib/block_scout_web/templates/chain/show.html.eex:69
#, elixir-autogen, elixir-format
msgid "Daily Transactions"
@@ -735,13 +837,13 @@ msgstr ""
msgid "Date & time at which block was produced."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:171
+#: lib/block_scout_web/templates/transaction/overview.html.eex:176
#, elixir-autogen, elixir-format
msgid "Date & time of transaction inclusion, including length of time for confirmation."
msgstr ""
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131
#, elixir-autogen, elixir-format
msgid "Decimals"
msgstr ""
@@ -757,7 +859,7 @@ msgstr ""
msgid "Decoded"
msgstr ""
-#: lib/block_scout_web/views/address_view.ex:365
+#: lib/block_scout_web/views/address_view.ex:368
#, elixir-autogen, elixir-format
msgid "Decompiled Code"
msgstr ""
@@ -782,8 +884,8 @@ msgstr ""
msgid "Delegate Call"
msgstr ""
-#: lib/block_scout_web/templates/address_contract/index.html.eex:181
-#: lib/block_scout_web/templates/address_contract/index.html.eex:189
+#: lib/block_scout_web/templates/address_contract/index.html.eex:175
+#: lib/block_scout_web/templates/address_contract/index.html.eex:183
#, elixir-autogen, elixir-format
msgid "Deployed ByteCode"
msgstr ""
@@ -796,6 +898,11 @@ msgstr ""
msgid "Description"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56
+#, elixir-autogen, elixir-format
+msgid "Description*"
+msgstr ""
+
#: lib/block_scout_web/templates/address/overview.html.eex:30
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127
@@ -838,30 +945,45 @@ msgstr ""
msgid "During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it."
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27
+#, elixir-autogen, elixir-format
+msgid "E-mail*"
+msgstr ""
+
#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6
#, elixir-autogen, elixir-format
msgid "EIP-1167"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:214
+#: lib/block_scout_web/views/transaction_view.ex:215
#, elixir-autogen, elixir-format
msgid "ERC-1155 "
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:212
+#: lib/block_scout_web/views/transaction_view.ex:213
#, elixir-autogen, elixir-format
msgid "ERC-20 "
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:213
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40
+#, elixir-autogen, elixir-format
+msgid "ERC-20 tokens (beta)"
+msgstr ""
+
+#: lib/block_scout_web/views/transaction_view.ex:214
#, elixir-autogen, elixir-format
msgid "ERC-721 "
msgstr ""
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53
+#, elixir-autogen, elixir-format
+msgid "ERC-721, ERC-1155 tokens (NFT) (beta)"
+msgstr ""
+
#: lib/block_scout_web/templates/address_token/overview.html.eex:1
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:146
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:137
#, elixir-autogen, elixir-format
msgid "ETH"
msgstr ""
@@ -883,6 +1005,18 @@ msgstr ""
msgid "Easy Cowboy! This block does not exist yet!"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:16
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27
+#, elixir-autogen, elixir-format
+msgid "Edit"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71
+#, elixir-autogen, elixir-format
+msgid "Email notifications"
+msgstr ""
+
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5
#, elixir-autogen, elixir-format
msgid "Emission Contract"
@@ -909,7 +1043,7 @@ msgstr ""
msgid "Error"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:9
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:11
#, elixir-autogen, elixir-format
msgid "Error in internal transactions"
msgstr ""
@@ -924,17 +1058,17 @@ msgstr ""
msgid "Error trying to fetch balances."
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:353
+#: lib/block_scout_web/views/transaction_view.ex:354
#, elixir-autogen, elixir-format
msgid "Error: %{reason}"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:351
+#: lib/block_scout_web/views/transaction_view.ex:352
#, elixir-autogen, elixir-format
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:137
+#: lib/block_scout_web/templates/address/overview.html.eex:119
#, elixir-autogen, elixir-format
msgid "Error: Could not determine contract creator."
msgstr ""
@@ -944,16 +1078,18 @@ msgstr ""
msgid "Eth RPC"
msgstr ""
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:27
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:21
#: lib/block_scout_web/templates/address/_current_coin_balance.html.eex:11
#: lib/block_scout_web/templates/address/index.html.eex:5
-#: lib/block_scout_web/templates/address/overview.html.eex:181
+#: lib/block_scout_web/templates/address/overview.html.eex:163
#: lib/block_scout_web/templates/block/overview.html.eex:215
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:24
#: lib/block_scout_web/templates/layout/_topnav.html.eex:82
#: lib/block_scout_web/templates/layout/app.html.eex:48
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:43
-#: lib/block_scout_web/templates/transaction/overview.html.eex:403
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:49
+#: lib/block_scout_web/templates/transaction/overview.html.eex:411
#: lib/block_scout_web/views/wei_helpers.ex:78
#, elixir-autogen, elixir-format
msgid "Ether"
@@ -981,7 +1117,7 @@ msgstr ""
msgid "Export Data"
msgstr ""
-#: lib/block_scout_web/templates/address_contract/index.html.eex:224
+#: lib/block_scout_web/templates/address_contract/index.html.eex:212
#, elixir-autogen, elixir-format
msgid "External libraries"
msgstr ""
@@ -1002,12 +1138,12 @@ msgstr ""
msgid "Fast"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:265
+#: lib/block_scout_web/templates/address/overview.html.eex:247
#, elixir-autogen, elixir-format
msgid "Fetching gas used..."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:111
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112
#, elixir-autogen, elixir-format
msgid "Fetching holders..."
msgstr ""
@@ -1017,15 +1153,15 @@ msgstr ""
msgid "Fetching tokens..."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:212
-#: lib/block_scout_web/templates/address/overview.html.eex:220
+#: lib/block_scout_web/templates/address/overview.html.eex:194
+#: lib/block_scout_web/templates/address/overview.html.eex:202
#, elixir-autogen, elixir-format
msgid "Fetching transactions..."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:239
-#: lib/block_scout_web/templates/address/overview.html.eex:247
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:122
+#: lib/block_scout_web/templates/address/overview.html.eex:221
+#: lib/block_scout_web/templates/address/overview.html.eex:229
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123
#, elixir-autogen, elixir-format
msgid "Fetching transfers..."
msgstr ""
@@ -1048,7 +1184,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40
#: lib/block_scout_web/templates/address_transaction/index.html.eex:34
-#: lib/block_scout_web/templates/transaction/overview.html.eex:195
+#: lib/block_scout_web/templates/transaction/overview.html.eex:200
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:10
@@ -1063,24 +1199,24 @@ msgstr ""
#: lib/block_scout_web/templates/block/_tile.html.eex:67
#: lib/block_scout_web/templates/block/overview.html.eex:187
-#: lib/block_scout_web/templates/transaction/overview.html.eex:365
+#: lib/block_scout_web/templates/transaction/overview.html.eex:373
#, elixir-autogen, elixir-format
msgid "Gas Limit"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:345
+#: lib/block_scout_web/templates/transaction/overview.html.eex:353
#, elixir-autogen, elixir-format
msgid "Gas Price"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:258
+#: lib/block_scout_web/templates/address/overview.html.eex:240
#: lib/block_scout_web/templates/block/_tile.html.eex:73
#: lib/block_scout_web/templates/block/overview.html.eex:178
#, elixir-autogen, elixir-format
msgid "Gas Used"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:418
+#: lib/block_scout_web/templates/transaction/overview.html.eex:426
#, elixir-autogen, elixir-format
msgid "Gas Used by Transaction"
msgstr ""
@@ -1091,7 +1227,7 @@ msgstr ""
msgid "Gas tracker"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:257
+#: lib/block_scout_web/templates/address/overview.html.eex:239
#, elixir-autogen, elixir-format
msgid "Gas used by the address."
msgstr ""
@@ -1132,13 +1268,13 @@ msgstr ""
msgid "Hash"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:446
-#: lib/block_scout_web/templates/transaction/overview.html.eex:450
+#: lib/block_scout_web/templates/transaction/overview.html.eex:454
+#: lib/block_scout_web/templates/transaction/overview.html.eex:458
#, elixir-autogen, elixir-format
msgid "Hex (Default)"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108
#, elixir-autogen, elixir-format
msgid "Holders"
msgstr ""
@@ -1154,7 +1290,7 @@ msgid "IMPORTANT: This information is a best guess based on similar functions fr
msgstr ""
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:86
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:92
#, elixir-autogen, elixir-format
msgid "IN"
msgstr ""
@@ -1169,17 +1305,17 @@ msgstr ""
msgid "If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:150
+#: lib/block_scout_web/templates/address/overview.html.eex:132
#, elixir-autogen, elixir-format
msgid "Implementation"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:149
+#: lib/block_scout_web/templates/address/overview.html.eex:131
#, elixir-autogen, elixir-format
msgid "Implementation address of the proxy contract."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:430
+#: lib/block_scout_web/templates/transaction/overview.html.eex:438
#, elixir-autogen, elixir-format
msgid "Index position of Transaction in the block."
msgstr ""
@@ -1204,7 +1340,7 @@ msgstr ""
msgid "Input"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:213
+#: lib/block_scout_web/templates/transaction/overview.html.eex:219
#, elixir-autogen, elixir-format
msgid "Interacted With (To)"
msgstr ""
@@ -1218,8 +1354,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6
-#: lib/block_scout_web/views/address_view.ex:361
-#: lib/block_scout_web/views/transaction_view.ex:512
+#: lib/block_scout_web/views/address_view.ex:364
+#: lib/block_scout_web/views/transaction_view.ex:513
#, elixir-autogen, elixir-format
msgid "Internal Transactions"
msgstr ""
@@ -1229,7 +1365,7 @@ msgstr ""
msgid "Invalid Transaction Hash"
msgstr ""
-#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:15
+#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19
#: lib/block_scout_web/views/tokens/overview_view.ex:42
#, elixir-autogen, elixir-format
@@ -1241,11 +1377,17 @@ msgstr ""
msgid "It could still be in the TX Pool of a different node, waiting to be broadcasted."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:276
+#: lib/block_scout_web/templates/address/overview.html.eex:258
#, elixir-autogen, elixir-format
msgid "Last Balance Update"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:12
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "Learn more"
+msgstr ""
+
#: lib/block_scout_web/templates/layout/app.html.eex:45
#, elixir-autogen, elixir-format
msgid "Less than"
@@ -1271,22 +1413,22 @@ msgstr ""
msgid "License ID"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:297
+#: lib/block_scout_web/templates/transaction/overview.html.eex:305
#, elixir-autogen, elixir-format
msgid "List of ERC-1155 tokens created in the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:281
+#: lib/block_scout_web/templates/transaction/overview.html.eex:289
#, elixir-autogen, elixir-format
msgid "List of token burnt in the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:264
+#: lib/block_scout_web/templates/transaction/overview.html.eex:272
#, elixir-autogen, elixir-format
msgid "List of token minted in the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:248
+#: lib/block_scout_web/templates/transaction/overview.html.eex:256
#, elixir-autogen, elixir-format
msgid "List of token transferred in the transaction."
msgstr ""
@@ -1302,11 +1444,13 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:133
#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49
#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45
-#: lib/block_scout_web/templates/address_read_contract/index.html.eex:12
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49
#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12
-#: lib/block_scout_web/templates/address_write_contract/index.html.eex:12
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47
#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12
-#: lib/block_scout_web/templates/tokens/contract/index.html.eex:16
+#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17
#, elixir-autogen, elixir-format
msgid "Loading..."
msgstr ""
@@ -1325,8 +1469,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_logs/index.html.eex:10
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8
-#: lib/block_scout_web/views/address_view.ex:372
-#: lib/block_scout_web/views/transaction_view.ex:513
+#: lib/block_scout_web/views/address_view.ex:375
+#: lib/block_scout_web/views/transaction_view.ex:514
#, elixir-autogen, elixir-format
msgid "Logs"
msgstr ""
@@ -1338,33 +1482,33 @@ msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:52
#: lib/block_scout_web/templates/layout/app.html.eex:46
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83
-#: lib/block_scout_web/views/address_view.ex:142
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84
+#: lib/block_scout_web/views/address_view.ex:145
#, elixir-autogen, elixir-format
msgid "Market Cap"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:374
+#: lib/block_scout_web/templates/transaction/overview.html.eex:382
#, elixir-autogen, elixir-format
msgid "Max Fee per Gas"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:384
+#: lib/block_scout_web/templates/transaction/overview.html.eex:392
#, elixir-autogen, elixir-format
msgid "Max Priority Fee per Gas"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:319
+#: lib/block_scout_web/views/transaction_view.ex:320
#, elixir-autogen, elixir-format
msgid "Max of"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:364
+#: lib/block_scout_web/templates/transaction/overview.html.eex:372
#, elixir-autogen, elixir-format
msgid "Maximum gas amount approved for the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:373
+#: lib/block_scout_web/templates/transaction/overview.html.eex:381
#, elixir-autogen, elixir-format
msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee."
msgstr ""
@@ -1438,6 +1582,16 @@ msgstr ""
msgid "N/A bytes"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:19
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:28
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4
@@ -1446,6 +1600,27 @@ msgstr ""
msgid "Name"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Name this API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "Name this Custom ABI"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Name this address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19
+#, elixir-autogen, elixir-format
+msgid "Name this transaction"
+msgstr ""
+
#: lib/block_scout_web/templates/address_token/overview.html.eex:44
#, elixir-autogen, elixir-format
msgid "Net Worth"
@@ -1487,7 +1662,7 @@ msgid "No"
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:196
-#: lib/block_scout_web/templates/transaction/overview.html.eex:428
+#: lib/block_scout_web/templates/transaction/overview.html.eex:436
#, elixir-autogen, elixir-format
msgid "Nonce"
msgstr ""
@@ -1497,38 +1672,38 @@ msgstr ""
msgid "Not unique Token"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:106
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107
#, elixir-autogen, elixir-format
msgid "Number of accounts holding the token"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:292
+#: lib/block_scout_web/templates/address/overview.html.eex:274
#, elixir-autogen, elixir-format
msgid "Number of blocks validated by this validator."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:129
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130
#, elixir-autogen, elixir-format
msgid "Number of digits that come after the decimal place when displaying token value"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:203
+#: lib/block_scout_web/templates/address/overview.html.eex:185
#, elixir-autogen, elixir-format
msgid "Number of transactions related to this address."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:117
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
#, elixir-autogen, elixir-format
msgid "Number of transfers for the token"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:230
+#: lib/block_scout_web/templates/address/overview.html.eex:212
#, elixir-autogen, elixir-format
msgid "Number of transfers to/from this address."
msgstr ""
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:82
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:88
#, elixir-autogen, elixir-format
msgid "OUT"
msgstr ""
@@ -1555,6 +1730,13 @@ msgstr ""
msgid "Other Explorers"
msgstr ""
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61
+#, elixir-autogen, elixir-format
+msgid "Outgoing"
+msgstr ""
+
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24
#, elixir-autogen, elixir-format
msgid "Owner Address"
@@ -1583,8 +1765,8 @@ msgid "Parent Hash"
msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:60
-#: lib/block_scout_web/views/transaction_view.ex:348
-#: lib/block_scout_web/views/transaction_view.ex:386
+#: lib/block_scout_web/views/transaction_view.ex:349
+#: lib/block_scout_web/views/transaction_view.ex:387
#, elixir-autogen, elixir-format
msgid "Pending"
msgstr ""
@@ -1600,7 +1782,7 @@ msgstr ""
msgid "Play"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:430
+#: lib/block_scout_web/templates/transaction/overview.html.eex:438
#, elixir-autogen, elixir-format
msgid "Position"
msgstr ""
@@ -1623,23 +1805,23 @@ msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:41
#: lib/block_scout_web/templates/layout/app.html.eex:47
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95
#, elixir-autogen, elixir-format
msgid "Price"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:93
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94
#, elixir-autogen, elixir-format
msgid "Price per token on the exchanges"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:344
+#: lib/block_scout_web/templates/transaction/overview.html.eex:352
#, elixir-autogen, elixir-format
msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:225
-#: lib/block_scout_web/templates/transaction/overview.html.eex:394
+#: lib/block_scout_web/templates/transaction/overview.html.eex:402
#, elixir-autogen, elixir-format
msgid "Priority Fee / Tip"
msgstr ""
@@ -1649,6 +1831,33 @@ msgstr ""
msgid "Priority Fees"
msgstr ""
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:4
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:13
+#, elixir-autogen, elixir-format
+msgid "Profile"
+msgstr ""
+
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19
+#, elixir-autogen, elixir-format
+msgid "Public Tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Public tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:22
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Public tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50
+#, elixir-autogen, elixir-format
+msgid "Public tags* (2 tags maximum, please use \";\" as a divider)"
+msgstr ""
+
#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10
#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83
@@ -1656,7 +1865,7 @@ msgstr ""
msgid "QR Code"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100
#, elixir-autogen, elixir-format
msgid "Query"
msgstr ""
@@ -1666,21 +1875,21 @@ msgstr ""
msgid "RPC"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:439
+#: lib/block_scout_web/templates/transaction/overview.html.eex:447
#, elixir-autogen, elixir-format
msgid "Raw Input"
msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7
-#: lib/block_scout_web/views/transaction_view.ex:514
+#: lib/block_scout_web/views/transaction_view.ex:515
#, elixir-autogen, elixir-format
msgid "Raw Trace"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:81
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27
-#: lib/block_scout_web/views/address_view.ex:366
+#: lib/block_scout_web/views/address_view.ex:369
#: lib/block_scout_web/views/tokens/overview_view.ex:41
#, elixir-autogen, elixir-format
msgid "Read Contract"
@@ -1688,7 +1897,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:88
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41
-#: lib/block_scout_web/views/address_view.ex:367
+#: lib/block_scout_web/views/address_view.ex:370
#, elixir-autogen, elixir-format
msgid "Read Proxy"
msgstr ""
@@ -1698,11 +1907,32 @@ msgstr ""
msgid "Records"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:13
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13
+#, elixir-autogen, elixir-format
+msgid "Remove"
+msgstr ""
+
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155
#, elixir-autogen, elixir-format
msgid "Request URL"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Request a public tag/label"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37
+#, elixir-autogen, elixir-format
+msgid "Request to add public tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Request to edit a public tag/label"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:142
#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:138
@@ -1724,12 +1954,12 @@ msgstr ""
msgid "Responses"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:93
+#: lib/block_scout_web/templates/transaction/overview.html.eex:98
#, elixir-autogen, elixir-format
msgid "Result"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:130
+#: lib/block_scout_web/templates/transaction/overview.html.eex:135
#, elixir-autogen, elixir-format
msgid "Revert reason"
msgstr ""
@@ -1746,6 +1976,15 @@ msgstr ""
msgid "Run"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:26
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83
+#, elixir-autogen, elixir-format
+msgid "Save"
+msgstr ""
+
#: lib/block_scout_web/templates/address_logs/index.html.eex:16
#: lib/block_scout_web/templates/layout/_search.html.eex:34
#, elixir-autogen, elixir-format
@@ -1772,6 +2011,11 @@ msgstr ""
msgid "Self-Destruct"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63
+#, elixir-autogen, elixir-format
+msgid "Send request"
+msgstr ""
+
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124
#, elixir-autogen, elixir-format
@@ -1788,11 +2032,6 @@ msgstr ""
msgid "Show QR Code"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:47
-#, elixir-autogen, elixir-format
-msgid "Show Validator Info"
-msgstr ""
-
#: lib/block_scout_web/templates/address_token/overview.html.eex:52
#, elixir-autogen, elixir-format
msgid "Shows the current"
@@ -1813,6 +2052,11 @@ msgstr ""
msgid "Shows total assets held in the address"
msgstr ""
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Sign out"
+msgstr ""
+
#: lib/block_scout_web/templates/block/overview.html.eex:114
#, elixir-autogen, elixir-format
msgid "Size"
@@ -1828,6 +2072,17 @@ msgstr ""
msgid "Slow"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21
+#, elixir-autogen, elixir-format
+msgid "Smart contract / Address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4
+#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5
+#, elixir-autogen, elixir-format
+msgid "Smart contract / Address (0x...)"
+msgstr ""
+
#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50
#: lib/block_scout_web/templates/address_logs/index.html.eex:23
@@ -1838,11 +2093,11 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:22
#: lib/block_scout_web/templates/chain/show.html.eex:157
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:18
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23
-#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22
-#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21
+#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23
+#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22
#: lib/block_scout_web/templates/transaction/index.html.eex:25
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13
#: lib/block_scout_web/templates/transaction_log/index.html.eex:15
@@ -1886,24 +2141,29 @@ msgstr ""
msgid "Static Call"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:105
+#: lib/block_scout_web/templates/transaction/overview.html.eex:110
#, elixir-autogen, elixir-format
msgid "Status"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22
+#, elixir-autogen, elixir-format
+msgid "Submission date"
+msgstr ""
+
#: lib/block_scout_web/templates/layout/_footer.html.eex:39
#, elixir-autogen, elixir-format
msgid "Submit an Issue"
msgstr ""
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
-#: lib/block_scout_web/views/transaction_view.ex:350
+#: lib/block_scout_web/views/transaction_view.ex:351
#, elixir-autogen, elixir-format
msgid "Success"
msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:46
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:52
#, elixir-autogen, elixir-format
msgid "TX Fee"
msgstr ""
@@ -1928,7 +2188,7 @@ msgstr ""
msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain."
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38
#, elixir-autogen, elixir-format
msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable."
msgstr ""
@@ -1938,12 +2198,12 @@ msgstr ""
msgid "The hash of the block from which this block was generated."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:92
+#: lib/block_scout_web/templates/address/overview.html.eex:74
#, elixir-autogen, elixir-format
msgid "The name found in the source code of the Contract."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:103
+#: lib/block_scout_web/templates/address/overview.html.eex:85
#, elixir-autogen, elixir-format
msgid "The name of the validator."
msgstr ""
@@ -1953,22 +2213,22 @@ msgstr ""
msgid "The number of transactions in the block."
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40
#, elixir-autogen, elixir-format
msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:129
+#: lib/block_scout_web/templates/transaction/overview.html.eex:134
#, elixir-autogen, elixir-format
msgid "The revert reason of the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:104
+#: lib/block_scout_web/templates/transaction/overview.html.eex:109
#, elixir-autogen, elixir-format
msgid "The status of the transaction: Confirmed or Unconfirmed."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68
#, elixir-autogen, elixir-format
msgid "The total amount of tokens issued"
msgstr ""
@@ -1988,7 +2248,7 @@ msgstr ""
msgid "There are no blocks."
msgstr ""
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:28
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29
#, elixir-autogen, elixir-format
msgid "There are no holders for this Token."
msgstr ""
@@ -2033,7 +2293,7 @@ msgstr ""
msgid "There are no tokens for this address."
msgstr ""
-#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:27
+#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28
#, elixir-autogen, elixir-format
msgid "There are no tokens."
msgstr ""
@@ -2055,7 +2315,7 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28
-#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26
+#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27
#, elixir-autogen, elixir-format
msgid "There are no transfers for this Token."
msgstr ""
@@ -2106,13 +2366,13 @@ msgstr ""
msgid "This is useful to allow sending requests to blockscout without having to change anything about the request."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:59
+#: lib/block_scout_web/templates/transaction/overview.html.eex:64
#, elixir-autogen, elixir-format
msgid "This transaction is pending confirmation."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:71
-#: lib/block_scout_web/templates/transaction/overview.html.eex:172
+#: lib/block_scout_web/templates/transaction/overview.html.eex:177
#, elixir-autogen, elixir-format
msgid "Timestamp"
msgstr ""
@@ -2120,7 +2380,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34
#: lib/block_scout_web/templates/address_transaction/index.html.eex:28
-#: lib/block_scout_web/templates/transaction/overview.html.eex:215
+#: lib/block_scout_web/templates/transaction/overview.html.eex:221
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_token_transfer_view.ex:9
#: lib/block_scout_web/views/address_transaction_view.ex:9
@@ -2145,19 +2405,19 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:73
+#: lib/block_scout_web/templates/address/overview.html.eex:55
#, elixir-autogen, elixir-format
msgid "Token"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3
-#: lib/block_scout_web/views/transaction_view.ex:448
+#: lib/block_scout_web/views/transaction_view.ex:449
#, elixir-autogen, elixir-format
msgid "Token Burning"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7
-#: lib/block_scout_web/views/transaction_view.ex:449
+#: lib/block_scout_web/views/transaction_view.ex:450
#, elixir-autogen, elixir-format
msgid "Token Creation"
msgstr ""
@@ -2168,7 +2428,7 @@ msgstr ""
msgid "Token Details"
msgstr ""
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11
@@ -2185,14 +2445,14 @@ msgid "Token ID"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5
-#: lib/block_scout_web/views/transaction_view.ex:447
+#: lib/block_scout_web/views/transaction_view.ex:448
#, elixir-autogen, elixir-format
msgid "Token Minting"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11
-#: lib/block_scout_web/views/transaction_view.ex:450
+#: lib/block_scout_web/views/transaction_view.ex:451
#, elixir-autogen, elixir-format
msgid "Token Transfer"
msgstr ""
@@ -2202,54 +2462,54 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5
-#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14
+#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7
-#: lib/block_scout_web/views/address_view.ex:363
+#: lib/block_scout_web/views/address_view.ex:366
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:195
#: lib/block_scout_web/views/tokens/overview_view.ex:39
-#: lib/block_scout_web/views/transaction_view.ex:511
+#: lib/block_scout_web/views/transaction_view.ex:512
#, elixir-autogen, elixir-format
msgid "Token Transfers"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:72
+#: lib/block_scout_web/templates/address/overview.html.eex:54
#, elixir-autogen, elixir-format
msgid "Token name and symbol."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142
#, elixir-autogen, elixir-format
msgid "Token type"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:21
-#: lib/block_scout_web/templates/address/overview.html.eex:193
+#: lib/block_scout_web/templates/address/overview.html.eex:175
#: lib/block_scout_web/templates/address_token/overview.html.eex:58
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13
#: lib/block_scout_web/templates/layout/_topnav.html.eex:73
#: lib/block_scout_web/templates/tokens/index.html.eex:10
-#: lib/block_scout_web/views/address_view.ex:360
+#: lib/block_scout_web/views/address_view.ex:363
#, elixir-autogen, elixir-format
msgid "Tokens"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:282
+#: lib/block_scout_web/templates/transaction/overview.html.eex:290
#, elixir-autogen, elixir-format
msgid "Tokens Burnt"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:298
+#: lib/block_scout_web/templates/transaction/overview.html.eex:306
#, elixir-autogen, elixir-format
msgid "Tokens Created"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:265
+#: lib/block_scout_web/templates/transaction/overview.html.eex:273
#, elixir-autogen, elixir-format
msgid "Tokens Minted"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:249
+#: lib/block_scout_web/templates/transaction/overview.html.eex:257
#, elixir-autogen, elixir-format
msgid "Tokens Transferred"
msgstr ""
@@ -2275,7 +2535,7 @@ msgstr ""
msgid "Total Difficulty"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:82
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83
#, elixir-autogen, elixir-format
msgid "Total Supply * Price"
msgstr ""
@@ -2295,12 +2555,12 @@ msgstr ""
msgid "Total gas limit provided by all transactions in the block."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:69
#, elixir-autogen, elixir-format
msgid "Total supply"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:329
+#: lib/block_scout_web/templates/transaction/overview.html.eex:337
#, elixir-autogen, elixir-format
msgid "Total transaction fee."
msgstr ""
@@ -2310,8 +2570,10 @@ msgstr ""
msgid "Total transactions"
msgstr ""
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19
-#: lib/block_scout_web/views/transaction_view.ex:460
+#: lib/block_scout_web/views/transaction_view.ex:461
#, elixir-autogen, elixir-format
msgid "Transaction"
msgstr ""
@@ -2326,7 +2588,7 @@ msgstr ""
msgid "Transaction %{transaction}, %{subnetwork} %{transaction}"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:404
+#: lib/block_scout_web/templates/transaction/overview.html.eex:412
#, elixir-autogen, elixir-format
msgid "Transaction Burnt Fee"
msgstr ""
@@ -2336,12 +2598,12 @@ msgstr ""
msgid "Transaction Details"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:330
+#: lib/block_scout_web/templates/transaction/overview.html.eex:338
#, elixir-autogen, elixir-format
msgid "Transaction Fee"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:75
+#: lib/block_scout_web/templates/transaction/overview.html.eex:80
#, elixir-autogen, elixir-format
msgid "Transaction Hash"
msgstr ""
@@ -2352,36 +2614,36 @@ msgstr ""
msgid "Transaction Inputs"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:354
+#: lib/block_scout_web/templates/transaction/overview.html.eex:362
#, elixir-autogen, elixir-format
msgid "Transaction Type"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:427
+#: lib/block_scout_web/templates/transaction/overview.html.eex:435
#, elixir-autogen, elixir-format
msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:353
+#: lib/block_scout_web/templates/transaction/overview.html.eex:361
#, elixir-autogen, elixir-format
msgid "Transaction type, introduced in EIP-2718."
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:7
-#: lib/block_scout_web/templates/address/overview.html.eex:204
-#: lib/block_scout_web/templates/address/overview.html.eex:210
-#: lib/block_scout_web/templates/address/overview.html.eex:218
+#: lib/block_scout_web/templates/address/overview.html.eex:186
+#: lib/block_scout_web/templates/address/overview.html.eex:192
+#: lib/block_scout_web/templates/address/overview.html.eex:200
#: lib/block_scout_web/templates/address_transaction/index.html.eex:13
#: lib/block_scout_web/templates/block/overview.html.eex:80
#: lib/block_scout_web/templates/block_transaction/index.html.eex:10
#: lib/block_scout_web/templates/chain/show.html.eex:213
#: lib/block_scout_web/templates/layout/_topnav.html.eex:48
-#: lib/block_scout_web/views/address_view.ex:362
+#: lib/block_scout_web/views/address_view.ex:365
#, elixir-autogen, elixir-format
msgid "Transactions"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:119
+#: lib/block_scout_web/templates/address/overview.html.eex:101
#, elixir-autogen, elixir-format
msgid "Transactions and address of creation."
msgstr ""
@@ -2391,11 +2653,11 @@ msgstr ""
msgid "Transactions sent"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:231
-#: lib/block_scout_web/templates/address/overview.html.eex:237
-#: lib/block_scout_web/templates/address/overview.html.eex:245
+#: lib/block_scout_web/templates/address/overview.html.eex:213
+#: lib/block_scout_web/templates/address/overview.html.eex:219
+#: lib/block_scout_web/templates/address/overview.html.eex:227
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119
#, elixir-autogen, elixir-format
msgid "Transfers"
msgstr ""
@@ -2422,12 +2684,12 @@ msgstr ""
msgid "Type"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:140
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141
#, elixir-autogen, elixir-format
msgid "Type of the token standard"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:453
+#: lib/block_scout_web/templates/transaction/overview.html.eex:461
#, elixir-autogen, elixir-format
msgid "UTF-8"
msgstr ""
@@ -2443,7 +2705,7 @@ msgstr ""
msgid "Uncles"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:341
+#: lib/block_scout_web/views/transaction_view.ex:342
#, elixir-autogen, elixir-format
msgid "Unconfirmed"
msgstr ""
@@ -2453,17 +2715,17 @@ msgstr ""
msgid "Unique Token"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:74
+#: lib/block_scout_web/templates/transaction/overview.html.eex:79
#, elixir-autogen, elixir-format
msgid "Unique character string (TxID) assigned to every verified transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:383
+#: lib/block_scout_web/templates/transaction/overview.html.eex:391
#, elixir-autogen, elixir-format
msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:393
+#: lib/block_scout_web/templates/transaction/overview.html.eex:401
#, elixir-autogen, elixir-format
msgid "User-defined tip sent to validator for transaction priority/inclusion."
msgstr ""
@@ -2493,26 +2755,27 @@ msgstr ""
msgid "Validator Data"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:51
-#, elixir-autogen, elixir-format
-msgid "Validator Info"
-msgstr ""
-
-#: lib/block_scout_web/templates/address/overview.html.eex:104
+#: lib/block_scout_web/templates/address/overview.html.eex:86
#, elixir-autogen, elixir-format
msgid "Validator Name"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:315
+#: lib/block_scout_web/templates/transaction/overview.html.eex:323
#, elixir-autogen, elixir-format
msgid "Value"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:314
+#: lib/block_scout_web/templates/transaction/overview.html.eex:322
#, elixir-autogen, elixir-format
msgid "Value sent in the native token (and USD) if applicable."
msgstr ""
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15
+#, elixir-autogen, elixir-format
+msgid "Verified"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract/index.html.eex:82
#, elixir-autogen, elixir-format
msgid "Verified at"
@@ -2520,10 +2783,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract/index.html.eex:27
#: lib/block_scout_web/templates/address_contract/index.html.eex:29
-#: lib/block_scout_web/templates/address_contract/index.html.eex:160
-#: lib/block_scout_web/templates/address_contract/index.html.eex:166
-#: lib/block_scout_web/templates/address_contract/index.html.eex:197
-#: lib/block_scout_web/templates/address_contract/index.html.eex:203
+#: lib/block_scout_web/templates/address_contract/index.html.eex:161
+#: lib/block_scout_web/templates/address_contract/index.html.eex:192
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14
#, elixir-autogen, elixir-format
msgid "Verify & Publish"
@@ -2586,12 +2847,12 @@ msgstr ""
msgid "View Contract"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:67
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:73
#, elixir-autogen, elixir-format
msgid "View Less Transfers"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:66
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:72
#, elixir-autogen, elixir-format
msgid "View More Transfers"
msgstr ""
@@ -2626,7 +2887,7 @@ msgstr ""
msgid "Vyper contract"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:145
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:136
#, elixir-autogen, elixir-format
msgid "WEI"
msgstr ""
@@ -2646,26 +2907,33 @@ msgstr ""
msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky."
msgstr ""
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:7
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "Watch list"
+msgstr ""
+
#: lib/block_scout_web/views/wei_helpers.ex:76
#, elixir-autogen, elixir-format
msgid "Wei"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100
#, elixir-autogen, elixir-format
msgid "Write"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:95
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34
-#: lib/block_scout_web/views/address_view.ex:368
+#: lib/block_scout_web/views/address_view.ex:371
#, elixir-autogen, elixir-format
msgid "Write Contract"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:102
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48
-#: lib/block_scout_web/views/address_view.ex:369
+#: lib/block_scout_web/views/address_view.ex:372
#, elixir-autogen, elixir-format
msgid "Write Proxy"
msgstr ""
@@ -2678,7 +2946,32 @@ msgstr ""
msgid "Yes"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:129
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "You can create 3 API keys per account."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "You can create up to 15 Custom ABIs per account."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11
+#, elixir-autogen, elixir-format
+msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15
+#, elixir-autogen, elixir-format
+msgid "Your name"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "Your name*"
+msgstr ""
+
+#: lib/block_scout_web/templates/address/overview.html.eex:111
#, elixir-autogen, elixir-format
msgid "at"
msgstr ""
@@ -2688,7 +2981,7 @@ msgstr ""
msgid "balance of the address"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:403
+#: lib/block_scout_web/templates/transaction/overview.html.eex:411
#, elixir-autogen, elixir-format
msgid "burned for this transaction. Equals Block Base Fee per Gas * Gas Used."
msgstr ""
@@ -2703,7 +2996,7 @@ msgstr ""
msgid "button"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:223
+#: lib/block_scout_web/templates/transaction/overview.html.eex:230
#, elixir-autogen, elixir-format
msgid "created"
msgstr ""
@@ -2713,7 +3006,7 @@ msgstr ""
msgid "custom RPC"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:167
+#: lib/block_scout_web/templates/address/overview.html.eex:149
#, elixir-autogen, elixir-format
msgid "doesn't include ERC20, ERC721, ERC1155 tokens)."
msgstr ""
@@ -2723,7 +3016,7 @@ msgstr ""
msgid "elements are displayed"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38
#, elixir-autogen, elixir-format
msgid "fallback"
msgstr ""
@@ -2765,7 +3058,7 @@ msgstr ""
msgid "page"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40
#, elixir-autogen, elixir-format
msgid "receive"
msgstr ""
@@ -2876,3 +3169,120 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "The requested path was not found on BlockScout."
msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25
+#, elixir-autogen, elixir-format
+msgid "Actions"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38
+#, elixir-autogen, elixir-format
+msgid "Add address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Add address to the Watch list"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81
+#, elixir-autogen, elixir-format
+msgid "Back to Watch list (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Edit Watch list address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77
+#, elixir-autogen, elixir-format
+msgid "Remove from Watch list"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have address tags yet"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have addresses on you watchlist yet"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have transaction tags yet"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:13
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:14
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:29
+#, elixir-autogen, elixir-format
+msgid "API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:7
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:16
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17
+#, elixir-autogen, elixir-format
+msgid "API keys"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:10
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15
+#, elixir-autogen, elixir-format
+msgid "Address Tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:25
+#, elixir-autogen, elixir-format
+msgid "Back to API keys (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "Back to Address Tags (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30
+#, elixir-autogen, elixir-format
+msgid "Back to Custom ABI (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "Back to Transaction Tags (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56
+#, elixir-autogen, elixir-format
+msgid "Incoming"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68
+#, elixir-autogen, elixir-format
+msgid "Please select notification methods:"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "Please select what types of notifications you will receive:"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:13
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16
+#, elixir-autogen, elixir-format
+msgid "Transaction Tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8
+#, elixir-autogen, elixir-format
+msgid "Update"
+msgstr ""
diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
index be81fc4f99..df1c48c61c 100644
--- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
+++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
@@ -63,7 +63,7 @@ msgstr ""
msgid "%{subnetwork} Explorer - BlockScout"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:349
+#: lib/block_scout_web/views/transaction_view.ex:350
#, elixir-autogen, elixir-format
msgid "(Awaiting internal transactions for status)"
msgstr ""
@@ -101,6 +101,11 @@ msgstr ""
msgid "A string with the name of the module to be invoked."
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "ABI"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_constructor_args.html.eex:3
#, elixir-autogen, elixir-format
msgid "ABI-encoded Constructor Arguments (if required by the contract)"
@@ -126,48 +131,83 @@ msgstr ""
msgid "APIs"
msgstr ""
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:24
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:24
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:69
#, elixir-autogen, elixir-format
msgid "Action"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:417
+#: lib/block_scout_web/templates/transaction/overview.html.eex:425
#, elixir-autogen, elixir-format
msgid "Actual gas amount used by the transaction."
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8
#: lib/block_scout_web/templates/layout/_add_chain_to_mm.html.eex:11
#, elixir-autogen, elixir-format
msgid "Add"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:44
+#, elixir-autogen, elixir-format
+msgid "Add API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:44
+#, elixir-autogen, elixir-format
+msgid "Add Custom ABI"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:7
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:37
+#, elixir-autogen, elixir-format
+msgid "Add address tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:7
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:37
+#, elixir-autogen, elixir-format
+msgid "Add transaction tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:11
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:23
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:23
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:12
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:16
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:20
-#: lib/block_scout_web/views/address_view.ex:104
+#: lib/block_scout_web/views/address_view.ex:107
#, elixir-autogen, elixir-format
msgid "Address"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:211
+#: lib/block_scout_web/templates/transaction/overview.html.eex:217
#, elixir-autogen, elixir-format
msgid "Address (external or contract) receiving the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:194
+#: lib/block_scout_web/templates/transaction/overview.html.eex:199
#, elixir-autogen, elixir-format
msgid "Address (external or contract) sending the transaction."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:167
+#: lib/block_scout_web/templates/address/overview.html.eex:149
#, elixir-autogen, elixir-format
msgid "Address balance in"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:50
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51
#, elixir-autogen, elixir-format
msgid "Address of the token contract"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:2
+#, elixir-autogen, elixir-format
+msgid "Address*"
+msgstr ""
+
#: lib/block_scout_web/templates/address/index.html.eex:5
#, elixir-autogen, elixir-format
msgid "Addresses"
@@ -194,12 +234,12 @@ msgstr ""
msgid "All metadata displayed below is from that contract. In order to verify current contract, click"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:192
+#: lib/block_scout_web/templates/address/overview.html.eex:174
#, elixir-autogen, elixir-format
msgid "All tokens in the account and total value."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:403
+#: lib/block_scout_web/templates/transaction/overview.html.eex:411
#, elixir-autogen, elixir-format
msgid "Amount of"
msgstr ""
@@ -235,7 +275,8 @@ msgstr ""
msgid "Back Home"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:168
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:24
+#: lib/block_scout_web/templates/address/overview.html.eex:150
#: lib/block_scout_web/templates/address_token/overview.html.eex:51
#, elixir-autogen, elixir-format
msgid "Balance"
@@ -257,14 +298,14 @@ msgstr ""
msgid "Base URL:"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:438
+#: lib/block_scout_web/templates/transaction/overview.html.eex:446
#, elixir-autogen, elixir-format
msgid "Binary data included with the transaction. See input / logs below for additional info."
msgstr ""
#: lib/block_scout_web/templates/address_coin_balance/_coin_balances.html.eex:8
#: lib/block_scout_web/templates/block/overview.html.eex:29
-#: lib/block_scout_web/templates/transaction/overview.html.eex:153
+#: lib/block_scout_web/templates/transaction/overview.html.eex:158
#, elixir-autogen, elixir-format
msgid "Block"
msgstr ""
@@ -296,7 +337,7 @@ msgstr ""
msgid "Block Mined, awaiting import..."
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:33
+#: lib/block_scout_web/views/transaction_view.ex:34
#, elixir-autogen, elixir-format
msgid "Block Pending"
msgstr ""
@@ -311,12 +352,12 @@ msgstr ""
msgid "Block not found, please try again later."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:152
+#: lib/block_scout_web/templates/transaction/overview.html.eex:157
#, elixir-autogen, elixir-format
msgid "Block number containing the transaction."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:275
+#: lib/block_scout_web/templates/address/overview.html.eex:257
#, elixir-autogen, elixir-format
msgid "Block number in which the address was updated."
msgstr ""
@@ -339,9 +380,9 @@ msgid "Blocks Indexed"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:48
-#: lib/block_scout_web/templates/address/overview.html.eex:293
+#: lib/block_scout_web/templates/address/overview.html.eex:275
#: lib/block_scout_web/templates/address_validation/index.html.eex:11
-#: lib/block_scout_web/views/address_view.ex:371
+#: lib/block_scout_web/views/address_view.ex:374
#, elixir-autogen, elixir-format
msgid "Blocks Validated"
msgstr ""
@@ -378,6 +419,7 @@ msgstr ""
msgid "Call Code"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:62
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:120
#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:145
#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:41
@@ -420,13 +462,13 @@ msgstr ""
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:187
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:126
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:149
-#: lib/block_scout_web/views/address_view.ex:364
+#: lib/block_scout_web/views/address_view.ex:367
#, elixir-autogen, elixir-format
msgid "Code"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:34
-#: lib/block_scout_web/views/address_view.ex:370
+#: lib/block_scout_web/views/address_view.ex:373
#, elixir-autogen, elixir-format
msgid "Coin Balance History"
msgstr ""
@@ -436,6 +478,16 @@ msgstr ""
msgid "Collapse"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Company name"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:32
+#, elixir-autogen, elixir-format
+msgid "Company website"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_compiler_field.html.eex:3
#, elixir-autogen, elixir-format
msgid "Compiler"
@@ -446,17 +498,17 @@ msgstr ""
msgid "Compiler version"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:342
+#: lib/block_scout_web/views/transaction_view.ex:343
#, elixir-autogen, elixir-format
msgid "Confirmed"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:119
+#: lib/block_scout_web/templates/transaction/overview.html.eex:124
#, elixir-autogen, elixir-format
msgid "Confirmed by "
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:185
+#: lib/block_scout_web/templates/transaction/overview.html.eex:190
#, elixir-autogen, elixir-format
msgid "Confirmed within"
msgstr ""
@@ -467,7 +519,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:4
#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:6
#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:4
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:15
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16
#, elixir-autogen, elixir-format
msgid "Connection Lost"
msgstr ""
@@ -500,8 +552,8 @@ msgstr ""
msgid "Constructor Arguments"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:51
-#: lib/block_scout_web/templates/transaction/overview.html.eex:221
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:52
+#: lib/block_scout_web/templates/transaction/overview.html.eex:227
#, elixir-autogen, elixir-format
msgid "Contract"
msgstr ""
@@ -511,25 +563,27 @@ msgstr ""
msgid "Contract ABI"
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:18
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:29
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_address_field.html.eex:3
-#: lib/block_scout_web/views/address_view.ex:102
+#: lib/block_scout_web/views/address_view.ex:105
#, elixir-autogen, elixir-format
msgid "Contract Address"
msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:16
-#: lib/block_scout_web/views/address_view.ex:42
-#: lib/block_scout_web/views/address_view.ex:76
+#: lib/block_scout_web/views/address_view.ex:45
+#: lib/block_scout_web/views/address_view.ex:79
#, elixir-autogen, elixir-format
msgid "Contract Address Pending"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:457
+#: lib/block_scout_web/views/transaction_view.ex:458
#, elixir-autogen, elixir-format
msgid "Contract Call"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:454
+#: lib/block_scout_web/views/transaction_view.ex:455
#, elixir-autogen, elixir-format
msgid "Contract Creation"
msgstr ""
@@ -546,7 +600,7 @@ msgstr ""
msgid "Contract Libraries"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:93
+#: lib/block_scout_web/templates/address/overview.html.eex:75
#: lib/block_scout_web/templates/address_contract_verification_common_fields/_contract_name_field.html.eex:3
#, elixir-autogen, elixir-format
msgid "Contract Name"
@@ -583,16 +637,33 @@ msgstr ""
msgid "Copy ABI"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:37
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:6
+#, elixir-autogen, elixir-format
+msgid "Copy API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8
+#: lib/block_scout_web/templates/account/tag_address/row.html.eex:8
+#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11
+#: lib/block_scout_web/templates/account/tag_transaction/row.html.eex:11
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7
#: lib/block_scout_web/templates/address/overview.html.eex:38
+#: lib/block_scout_web/templates/address/overview.html.eex:39
#: lib/block_scout_web/templates/block/overview.html.eex:104
#: lib/block_scout_web/templates/block/overview.html.eex:105
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:42
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:43
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:44
#, elixir-autogen, elixir-format
msgid "Copy Address"
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:6
+#, elixir-autogen, elixir-format
+msgid "Copy Contract Address"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract/index.html.eex:140
#: lib/block_scout_web/templates/address_contract/index.html.eex:156
#, elixir-autogen, elixir-format
@@ -604,16 +675,17 @@ msgstr ""
msgid "Copy Decompiled Contract Code"
msgstr ""
-#: lib/block_scout_web/templates/address_contract/index.html.eex:183
-#: lib/block_scout_web/templates/address_contract/index.html.eex:193
+#: lib/block_scout_web/templates/address_contract/index.html.eex:177
+#: lib/block_scout_web/templates/address_contract/index.html.eex:187
#, elixir-autogen, elixir-format
msgid "Copy Deployed ByteCode"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:14
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:15
-#: lib/block_scout_web/templates/transaction/overview.html.eex:201
-#: lib/block_scout_web/templates/transaction/overview.html.eex:202
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:7
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:17
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:18
+#: lib/block_scout_web/templates/transaction/overview.html.eex:207
+#: lib/block_scout_web/templates/transaction/overview.html.eex:208
#, elixir-autogen, elixir-format
msgid "Copy From Address"
msgstr ""
@@ -646,12 +718,12 @@ msgstr ""
msgid "Copy Source Code"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:31
-#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:32
-#: lib/block_scout_web/templates/transaction/overview.html.eex:227
-#: lib/block_scout_web/templates/transaction/overview.html.eex:228
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:34
+#: lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex:35
#: lib/block_scout_web/templates/transaction/overview.html.eex:234
#: lib/block_scout_web/templates/transaction/overview.html.eex:235
+#: lib/block_scout_web/templates/transaction/overview.html.eex:242
+#: lib/block_scout_web/templates/transaction/overview.html.eex:243
#, elixir-autogen, elixir-format
msgid "Copy To Address"
msgstr ""
@@ -662,30 +734,30 @@ msgstr ""
msgid "Copy Token ID"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:82
+#: lib/block_scout_web/templates/transaction/overview.html.eex:87
#, elixir-autogen, elixir-format
msgid "Copy Transaction Hash"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:83
+#: lib/block_scout_web/templates/transaction/overview.html.eex:88
#, elixir-autogen, elixir-format
msgid "Copy Txn Hash"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:464
+#: lib/block_scout_web/templates/transaction/overview.html.eex:472
#, elixir-autogen, elixir-format
msgid "Copy Txn Hex Input"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:470
+#: lib/block_scout_web/templates/transaction/overview.html.eex:478
#, elixir-autogen, elixir-format
msgid "Copy Txn UTF-8 Input"
msgstr ""
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:20
#: lib/block_scout_web/templates/transaction/_decoded_input_body.html.eex:41
-#: lib/block_scout_web/templates/transaction/overview.html.eex:463
-#: lib/block_scout_web/templates/transaction/overview.html.eex:469
+#: lib/block_scout_web/templates/transaction/overview.html.eex:471
+#: lib/block_scout_web/templates/transaction/overview.html.eex:477
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:14
#, elixir-autogen, elixir-format
msgid "Copy Value"
@@ -696,12 +768,22 @@ msgstr ""
msgid "Create"
msgstr ""
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:12
+#, elixir-autogen, elixir-format
+msgid "Create a Custom ABI to interact with contracts."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:12
+#, elixir-autogen, elixir-format
+msgid "Create an API key to use with your RPC и EthRPC API requests."
+msgstr ""
+
#: lib/block_scout_web/views/internal_transaction_view.ex:26
#, elixir-autogen, elixir-format
msgid "Create2"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:120
+#: lib/block_scout_web/templates/address/overview.html.eex:102
#, elixir-autogen, elixir-format
msgid "Creator"
msgstr ""
@@ -712,11 +794,31 @@ msgstr ""
msgid "Curl"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:92
+#: lib/block_scout_web/templates/transaction/overview.html.eex:97
#, elixir-autogen, elixir-format
msgid "Current transaction state: Success, Failed (Error), or Pending (In Process)"
msgstr ""
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:20
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "Custom"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:19
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "Custom ABI"
+msgstr ""
+
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:25
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:23
+#, elixir-autogen, elixir-format
+msgid "Custom ABI from account"
+msgstr ""
+
#: lib/block_scout_web/templates/chain/show.html.eex:69
#, elixir-autogen, elixir-format
msgid "Daily Transactions"
@@ -735,13 +837,13 @@ msgstr ""
msgid "Date & time at which block was produced."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:171
+#: lib/block_scout_web/templates/transaction/overview.html.eex:176
#, elixir-autogen, elixir-format
msgid "Date & time of transaction inclusion, including length of time for confirmation."
msgstr ""
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:52
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:131
#, elixir-autogen, elixir-format
msgid "Decimals"
msgstr ""
@@ -757,7 +859,7 @@ msgstr ""
msgid "Decoded"
msgstr ""
-#: lib/block_scout_web/views/address_view.ex:365
+#: lib/block_scout_web/views/address_view.ex:368
#, elixir-autogen, elixir-format
msgid "Decompiled Code"
msgstr ""
@@ -782,8 +884,8 @@ msgstr ""
msgid "Delegate Call"
msgstr ""
-#: lib/block_scout_web/templates/address_contract/index.html.eex:181
-#: lib/block_scout_web/templates/address_contract/index.html.eex:189
+#: lib/block_scout_web/templates/address_contract/index.html.eex:175
+#: lib/block_scout_web/templates/address_contract/index.html.eex:183
#, elixir-autogen, elixir-format
msgid "Deployed ByteCode"
msgstr ""
@@ -796,6 +898,11 @@ msgstr ""
msgid "Description"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:56
+#, elixir-autogen, elixir-format
+msgid "Description*"
+msgstr ""
+
#: lib/block_scout_web/templates/address/overview.html.eex:30
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:166
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:127
@@ -838,30 +945,45 @@ msgstr ""
msgid "During times when the network is busy (i.e during ICOs) it can take a while for your transaction to propagate through the network and for us to index it."
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:27
+#, elixir-autogen, elixir-format
+msgid "E-mail*"
+msgstr ""
+
#: lib/block_scout_web/templates/common_components/_minimal_proxy_pattern.html.eex:6
#, elixir-autogen, elixir-format
msgid "EIP-1167"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:214
+#: lib/block_scout_web/views/transaction_view.ex:215
#, elixir-autogen, elixir-format
msgid "ERC-1155 "
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:212
+#: lib/block_scout_web/views/transaction_view.ex:213
#, elixir-autogen, elixir-format
msgid "ERC-20 "
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:213
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:40
+#, elixir-autogen, elixir-format
+msgid "ERC-20 tokens (beta)"
+msgstr ""
+
+#: lib/block_scout_web/views/transaction_view.ex:214
#, elixir-autogen, elixir-format
msgid "ERC-721 "
msgstr ""
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:53
+#, elixir-autogen, elixir-format
+msgid "ERC-721, ERC-1155 tokens (NFT) (beta)"
+msgstr ""
+
#: lib/block_scout_web/templates/address_token/overview.html.eex:1
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:104
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:146
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:95
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:137
#, elixir-autogen, elixir-format
msgid "ETH"
msgstr ""
@@ -883,6 +1005,18 @@ msgstr ""
msgid "Easy Cowboy! This block does not exist yet!"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:16
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:16
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:27
+#, elixir-autogen, elixir-format
+msgid "Edit"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:71
+#, elixir-autogen, elixir-format
+msgid "Email notifications"
+msgstr ""
+
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:5
#, elixir-autogen, elixir-format
msgid "Emission Contract"
@@ -909,7 +1043,7 @@ msgstr ""
msgid "Error"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:9
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:11
#, elixir-autogen, elixir-format
msgid "Error in internal transactions"
msgstr ""
@@ -924,17 +1058,17 @@ msgstr ""
msgid "Error trying to fetch balances."
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:353
+#: lib/block_scout_web/views/transaction_view.ex:354
#, elixir-autogen, elixir-format
msgid "Error: %{reason}"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:351
+#: lib/block_scout_web/views/transaction_view.ex:352
#, elixir-autogen, elixir-format
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:137
+#: lib/block_scout_web/templates/address/overview.html.eex:119
#, elixir-autogen, elixir-format
msgid "Error: Could not determine contract creator."
msgstr ""
@@ -944,16 +1078,18 @@ msgstr ""
msgid "Eth RPC"
msgstr ""
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:27
+#: lib/block_scout_web/templates/account/watchlist_address/row.html.eex:21
#: lib/block_scout_web/templates/address/_current_coin_balance.html.eex:11
#: lib/block_scout_web/templates/address/index.html.eex:5
-#: lib/block_scout_web/templates/address/overview.html.eex:181
+#: lib/block_scout_web/templates/address/overview.html.eex:163
#: lib/block_scout_web/templates/block/overview.html.eex:215
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:24
#: lib/block_scout_web/templates/layout/_topnav.html.eex:82
#: lib/block_scout_web/templates/layout/app.html.eex:48
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:43
-#: lib/block_scout_web/templates/transaction/overview.html.eex:403
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:49
+#: lib/block_scout_web/templates/transaction/overview.html.eex:411
#: lib/block_scout_web/views/wei_helpers.ex:78
#, elixir-autogen, elixir-format
msgid "Ether"
@@ -981,7 +1117,7 @@ msgstr ""
msgid "Export Data"
msgstr ""
-#: lib/block_scout_web/templates/address_contract/index.html.eex:224
+#: lib/block_scout_web/templates/address_contract/index.html.eex:212
#, elixir-autogen, elixir-format
msgid "External libraries"
msgstr ""
@@ -1002,12 +1138,12 @@ msgstr ""
msgid "Fast"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:265
+#: lib/block_scout_web/templates/address/overview.html.eex:247
#, elixir-autogen, elixir-format
msgid "Fetching gas used..."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:111
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:112
#, elixir-autogen, elixir-format
msgid "Fetching holders..."
msgstr ""
@@ -1017,15 +1153,15 @@ msgstr ""
msgid "Fetching tokens..."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:212
-#: lib/block_scout_web/templates/address/overview.html.eex:220
+#: lib/block_scout_web/templates/address/overview.html.eex:194
+#: lib/block_scout_web/templates/address/overview.html.eex:202
#, elixir-autogen, elixir-format
msgid "Fetching transactions..."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:239
-#: lib/block_scout_web/templates/address/overview.html.eex:247
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:122
+#: lib/block_scout_web/templates/address/overview.html.eex:221
+#: lib/block_scout_web/templates/address/overview.html.eex:229
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:123
#, elixir-autogen, elixir-format
msgid "Fetching transfers..."
msgstr ""
@@ -1048,7 +1184,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:38
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:40
#: lib/block_scout_web/templates/address_transaction/index.html.eex:34
-#: lib/block_scout_web/templates/transaction/overview.html.eex:195
+#: lib/block_scout_web/templates/transaction/overview.html.eex:200
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_token_transfer_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:10
@@ -1063,24 +1199,24 @@ msgstr ""
#: lib/block_scout_web/templates/block/_tile.html.eex:67
#: lib/block_scout_web/templates/block/overview.html.eex:187
-#: lib/block_scout_web/templates/transaction/overview.html.eex:365
+#: lib/block_scout_web/templates/transaction/overview.html.eex:373
#, elixir-autogen, elixir-format
msgid "Gas Limit"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:345
+#: lib/block_scout_web/templates/transaction/overview.html.eex:353
#, elixir-autogen, elixir-format
msgid "Gas Price"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:258
+#: lib/block_scout_web/templates/address/overview.html.eex:240
#: lib/block_scout_web/templates/block/_tile.html.eex:73
#: lib/block_scout_web/templates/block/overview.html.eex:178
#, elixir-autogen, elixir-format
msgid "Gas Used"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:418
+#: lib/block_scout_web/templates/transaction/overview.html.eex:426
#, elixir-autogen, elixir-format
msgid "Gas Used by Transaction"
msgstr ""
@@ -1091,7 +1227,7 @@ msgstr ""
msgid "Gas tracker"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:257
+#: lib/block_scout_web/templates/address/overview.html.eex:239
#, elixir-autogen, elixir-format
msgid "Gas used by the address."
msgstr ""
@@ -1132,13 +1268,13 @@ msgstr ""
msgid "Hash"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:446
-#: lib/block_scout_web/templates/transaction/overview.html.eex:450
+#: lib/block_scout_web/templates/transaction/overview.html.eex:454
+#: lib/block_scout_web/templates/transaction/overview.html.eex:458
#, elixir-autogen, elixir-format
msgid "Hex (Default)"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:108
#, elixir-autogen, elixir-format
msgid "Holders"
msgstr ""
@@ -1154,7 +1290,7 @@ msgid "IMPORTANT: This information is a best guess based on similar functions fr
msgstr ""
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:42
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:86
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:92
#, elixir-autogen, elixir-format
msgid "IN"
msgstr ""
@@ -1169,17 +1305,17 @@ msgstr ""
msgid "If you have just submitted this transaction please wait for at least 30 seconds before refreshing this page."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:150
+#: lib/block_scout_web/templates/address/overview.html.eex:132
#, elixir-autogen, elixir-format
msgid "Implementation"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:149
+#: lib/block_scout_web/templates/address/overview.html.eex:131
#, elixir-autogen, elixir-format
msgid "Implementation address of the proxy contract."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:430
+#: lib/block_scout_web/templates/transaction/overview.html.eex:438
#, elixir-autogen, elixir-format
msgid "Index position of Transaction in the block."
msgstr ""
@@ -1204,7 +1340,7 @@ msgstr ""
msgid "Input"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:213
+#: lib/block_scout_web/templates/transaction/overview.html.eex:219
#, elixir-autogen, elixir-format
msgid "Interacted With (To)"
msgstr ""
@@ -1218,8 +1354,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:17
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:11
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:6
-#: lib/block_scout_web/views/address_view.ex:361
-#: lib/block_scout_web/views/transaction_view.ex:512
+#: lib/block_scout_web/views/address_view.ex:364
+#: lib/block_scout_web/views/transaction_view.ex:513
#, elixir-autogen, elixir-format
msgid "Internal Transactions"
msgstr ""
@@ -1229,7 +1365,7 @@ msgstr ""
msgid "Invalid Transaction Hash"
msgstr ""
-#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:15
+#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:16
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:19
#: lib/block_scout_web/views/tokens/overview_view.ex:42
#, elixir-autogen, elixir-format
@@ -1241,11 +1377,17 @@ msgstr ""
msgid "It could still be in the TX Pool of a different node, waiting to be broadcasted."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:276
+#: lib/block_scout_web/templates/address/overview.html.eex:258
#, elixir-autogen, elixir-format
msgid "Last Balance Update"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:12
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "Learn more"
+msgstr ""
+
#: lib/block_scout_web/templates/layout/app.html.eex:45
#, elixir-autogen, elixir-format
msgid "Less than"
@@ -1271,22 +1413,22 @@ msgstr ""
msgid "License ID"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:297
+#: lib/block_scout_web/templates/transaction/overview.html.eex:305
#, elixir-autogen, elixir-format
msgid "List of ERC-1155 tokens created in the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:281
+#: lib/block_scout_web/templates/transaction/overview.html.eex:289
#, elixir-autogen, elixir-format
msgid "List of token burnt in the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:264
+#: lib/block_scout_web/templates/transaction/overview.html.eex:272
#, elixir-autogen, elixir-format
msgid "List of token minted in the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:248
+#: lib/block_scout_web/templates/transaction/overview.html.eex:256
#, elixir-autogen, elixir-format
msgid "List of token transferred in the transaction."
msgstr ""
@@ -1302,11 +1444,13 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:133
#: lib/block_scout_web/templates/address_contract_verification_via_standard_json_input/new.html.eex:49
#: lib/block_scout_web/templates/address_contract_verification_vyper/new.html.eex:45
-#: lib/block_scout_web/templates/address_read_contract/index.html.eex:12
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:41
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:49
#: lib/block_scout_web/templates/address_read_proxy/index.html.eex:12
-#: lib/block_scout_web/templates/address_write_contract/index.html.eex:12
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:39
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:47
#: lib/block_scout_web/templates/address_write_proxy/index.html.eex:12
-#: lib/block_scout_web/templates/tokens/contract/index.html.eex:16
+#: lib/block_scout_web/templates/tokens/contract/index.html.eex:17
#, elixir-autogen, elixir-format
msgid "Loading..."
msgstr ""
@@ -1325,8 +1469,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_logs/index.html.eex:10
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:17
#: lib/block_scout_web/templates/transaction_log/index.html.eex:8
-#: lib/block_scout_web/views/address_view.ex:372
-#: lib/block_scout_web/views/transaction_view.ex:513
+#: lib/block_scout_web/views/address_view.ex:375
+#: lib/block_scout_web/views/transaction_view.ex:514
#, elixir-autogen, elixir-format
msgid "Logs"
msgstr ""
@@ -1338,33 +1482,33 @@ msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:52
#: lib/block_scout_web/templates/layout/app.html.eex:46
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83
-#: lib/block_scout_web/views/address_view.ex:142
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:84
+#: lib/block_scout_web/views/address_view.ex:145
#, elixir-autogen, elixir-format
msgid "Market Cap"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:374
+#: lib/block_scout_web/templates/transaction/overview.html.eex:382
#, elixir-autogen, elixir-format
msgid "Max Fee per Gas"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:384
+#: lib/block_scout_web/templates/transaction/overview.html.eex:392
#, elixir-autogen, elixir-format
msgid "Max Priority Fee per Gas"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:319
+#: lib/block_scout_web/views/transaction_view.ex:320
#, elixir-autogen, elixir-format
msgid "Max of"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:364
+#: lib/block_scout_web/templates/transaction/overview.html.eex:372
#, elixir-autogen, elixir-format
msgid "Maximum gas amount approved for the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:373
+#: lib/block_scout_web/templates/transaction/overview.html.eex:381
#, elixir-autogen, elixir-format
msgid "Maximum total amount per unit of gas a user is willing to pay for a transaction, including base fee and priority fee."
msgstr ""
@@ -1438,6 +1582,16 @@ msgstr ""
msgid "N/A bytes"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:19
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:28
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:13
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:28
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:18
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:22
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:18
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:22
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:22
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:19
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:52
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:59
#: lib/block_scout_web/templates/log/_data_decoded_view.html.eex:4
@@ -1446,6 +1600,27 @@ msgstr ""
msgid "Name"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Name this API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "Name this Custom ABI"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:19
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Name this address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:19
+#, elixir-autogen, elixir-format
+msgid "Name this transaction"
+msgstr ""
+
#: lib/block_scout_web/templates/address_token/overview.html.eex:44
#, elixir-autogen, elixir-format
msgid "Net Worth"
@@ -1487,7 +1662,7 @@ msgid "No"
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:196
-#: lib/block_scout_web/templates/transaction/overview.html.eex:428
+#: lib/block_scout_web/templates/transaction/overview.html.eex:436
#, elixir-autogen, elixir-format
msgid "Nonce"
msgstr ""
@@ -1497,38 +1672,38 @@ msgstr ""
msgid "Not unique Token"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:106
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:107
#, elixir-autogen, elixir-format
msgid "Number of accounts holding the token"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:292
+#: lib/block_scout_web/templates/address/overview.html.eex:274
#, elixir-autogen, elixir-format
msgid "Number of blocks validated by this validator."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:129
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:130
#, elixir-autogen, elixir-format
msgid "Number of digits that come after the decimal place when displaying token value"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:203
+#: lib/block_scout_web/templates/address/overview.html.eex:185
#, elixir-autogen, elixir-format
msgid "Number of transactions related to this address."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:117
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
#, elixir-autogen, elixir-format
msgid "Number of transfers for the token"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:230
+#: lib/block_scout_web/templates/address/overview.html.eex:212
#, elixir-autogen, elixir-format
msgid "Number of transfers to/from this address."
msgstr ""
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:40
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:82
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:88
#, elixir-autogen, elixir-format
msgid "OUT"
msgstr ""
@@ -1555,6 +1730,13 @@ msgstr ""
msgid "Other Explorers"
msgstr ""
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:35
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:48
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:61
+#, elixir-autogen, elixir-format
+msgid "Outgoing"
+msgstr ""
+
#: lib/block_scout_web/templates/tokens/inventory/_token.html.eex:24
#, elixir-autogen, elixir-format
msgid "Owner Address"
@@ -1583,8 +1765,8 @@ msgid "Parent Hash"
msgstr ""
#: lib/block_scout_web/templates/layout/_topnav.html.eex:60
-#: lib/block_scout_web/views/transaction_view.ex:348
-#: lib/block_scout_web/views/transaction_view.ex:386
+#: lib/block_scout_web/views/transaction_view.ex:349
+#: lib/block_scout_web/views/transaction_view.ex:387
#, elixir-autogen, elixir-format
msgid "Pending"
msgstr ""
@@ -1600,7 +1782,7 @@ msgstr ""
msgid "Play"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:430
+#: lib/block_scout_web/templates/transaction/overview.html.eex:438
#, elixir-autogen, elixir-format
msgid "Position"
msgstr ""
@@ -1623,23 +1805,23 @@ msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:41
#: lib/block_scout_web/templates/layout/app.html.eex:47
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:95
#, elixir-autogen, elixir-format
msgid "Price"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:93
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:94
#, elixir-autogen, elixir-format
msgid "Price per token on the exchanges"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:344
+#: lib/block_scout_web/templates/transaction/overview.html.eex:352
#, elixir-autogen, elixir-format
msgid "Price per unit of gas specified by the sender. Higher gas prices can prioritize transaction inclusion during times of high usage."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:225
-#: lib/block_scout_web/templates/transaction/overview.html.eex:394
+#: lib/block_scout_web/templates/transaction/overview.html.eex:402
#, elixir-autogen, elixir-format
msgid "Priority Fee / Tip"
msgstr ""
@@ -1649,6 +1831,33 @@ msgstr ""
msgid "Priority Fees"
msgstr ""
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:4
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:13
+#, elixir-autogen, elixir-format
+msgid "Profile"
+msgstr ""
+
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:19
+#, elixir-autogen, elixir-format
+msgid "Public Tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Public tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:22
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Public tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:50
+#, elixir-autogen, elixir-format
+msgid "Public tags* (2 tags maximum, please use \";\" as a divider)"
+msgstr ""
+
#: lib/block_scout_web/templates/common_components/_btn_qr_code.html.eex:10
#: lib/block_scout_web/templates/common_components/_modal_qr_code.html.eex:5
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:83
@@ -1656,7 +1865,7 @@ msgstr ""
msgid "QR Code"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100
#, elixir-autogen, elixir-format
msgid "Query"
msgstr ""
@@ -1666,21 +1875,21 @@ msgstr ""
msgid "RPC"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:439
+#: lib/block_scout_web/templates/transaction/overview.html.eex:447
#, elixir-autogen, elixir-format
msgid "Raw Input"
msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:24
#: lib/block_scout_web/templates/transaction_raw_trace/index.html.eex:7
-#: lib/block_scout_web/views/transaction_view.ex:514
+#: lib/block_scout_web/views/transaction_view.ex:515
#, elixir-autogen, elixir-format
msgid "Raw Trace"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:81
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:27
-#: lib/block_scout_web/views/address_view.ex:366
+#: lib/block_scout_web/views/address_view.ex:369
#: lib/block_scout_web/views/tokens/overview_view.ex:41
#, elixir-autogen, elixir-format
msgid "Read Contract"
@@ -1688,7 +1897,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:88
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:41
-#: lib/block_scout_web/views/address_view.ex:367
+#: lib/block_scout_web/views/address_view.ex:370
#, elixir-autogen, elixir-format
msgid "Read Proxy"
msgstr ""
@@ -1698,11 +1907,32 @@ msgstr ""
msgid "Records"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/row.html.eex:13
+#: lib/block_scout_web/templates/account/custom_abi/row.html.eex:13
+#, elixir-autogen, elixir-format
+msgid "Remove"
+msgstr ""
+
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:155
#, elixir-autogen, elixir-format
msgid "Request URL"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Request a public tag/label"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:37
+#, elixir-autogen, elixir-format
+msgid "Request to add public tag"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:7
+#, elixir-autogen, elixir-format
+msgid "Request to edit a public tag/label"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract_verification_via_flattened_code/new.html.eex:142
#: lib/block_scout_web/templates/address_contract_verification_via_json/new.html.eex:38
#: lib/block_scout_web/templates/address_contract_verification_via_multi_part_files/new.html.eex:138
@@ -1724,12 +1954,12 @@ msgstr ""
msgid "Responses"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:93
+#: lib/block_scout_web/templates/transaction/overview.html.eex:98
#, elixir-autogen, elixir-format
msgid "Result"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:130
+#: lib/block_scout_web/templates/transaction/overview.html.eex:135
#, elixir-autogen, elixir-format
msgid "Revert reason"
msgstr ""
@@ -1746,6 +1976,15 @@ msgstr ""
msgid "Run"
msgstr ""
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:26
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:31
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:25
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:25
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:83
+#, elixir-autogen, elixir-format
+msgid "Save"
+msgstr ""
+
#: lib/block_scout_web/templates/address_logs/index.html.eex:16
#: lib/block_scout_web/templates/layout/_search.html.eex:34
#, elixir-autogen, elixir-format
@@ -1772,6 +2011,11 @@ msgstr ""
msgid "Self-Destruct"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:63
+#, elixir-autogen, elixir-format
+msgid "Send request"
+msgstr ""
+
#: lib/block_scout_web/templates/api_docs/_action_tile.html.eex:163
#: lib/block_scout_web/templates/api_docs/_eth_rpc_item.html.eex:124
#, elixir-autogen, elixir-format
@@ -1788,11 +2032,6 @@ msgstr ""
msgid "Show QR Code"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:47
-#, elixir-autogen, elixir-format
-msgid "Show Validator Info"
-msgstr ""
-
#: lib/block_scout_web/templates/address_token/overview.html.eex:52
#, elixir-autogen, elixir-format
msgid "Shows the current"
@@ -1813,6 +2052,11 @@ msgstr ""
msgid "Shows total assets held in the address"
msgstr ""
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:20
+#, elixir-autogen, elixir-format
+msgid "Sign out"
+msgstr ""
+
#: lib/block_scout_web/templates/block/overview.html.eex:114
#, elixir-autogen, elixir-format
msgid "Size"
@@ -1828,6 +2072,17 @@ msgstr ""
msgid "Slow"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:21
+#, elixir-autogen, elixir-format
+msgid "Smart contract / Address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:4
+#: lib/block_scout_web/templates/account/public_tags_request/address_field.html.eex:5
+#, elixir-autogen, elixir-format
+msgid "Smart contract / Address (0x...)"
+msgstr ""
+
#: lib/block_scout_web/templates/address_coin_balance/index.html.eex:30
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:50
#: lib/block_scout_web/templates/address_logs/index.html.eex:23
@@ -1838,11 +2093,11 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:22
#: lib/block_scout_web/templates/chain/show.html.eex:157
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:18
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:23
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:24
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:23
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:23
-#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:22
-#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:21
+#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:23
+#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:22
#: lib/block_scout_web/templates/transaction/index.html.eex:25
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:13
#: lib/block_scout_web/templates/transaction_log/index.html.eex:15
@@ -1886,24 +2141,29 @@ msgstr ""
msgid "Static Call"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:105
+#: lib/block_scout_web/templates/transaction/overview.html.eex:110
#, elixir-autogen, elixir-format
msgid "Status"
msgstr ""
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:22
+#, elixir-autogen, elixir-format
+msgid "Submission date"
+msgstr ""
+
#: lib/block_scout_web/templates/layout/_footer.html.eex:39
#, elixir-autogen, elixir-format
msgid "Submit an Issue"
msgstr ""
#: lib/block_scout_web/templates/transaction/_emission_reward_tile.html.eex:8
-#: lib/block_scout_web/views/transaction_view.ex:350
+#: lib/block_scout_web/views/transaction_view.ex:351
#, elixir-autogen, elixir-format
msgid "Success"
msgstr ""
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:21
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:46
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:52
#, elixir-autogen, elixir-format
msgid "TX Fee"
msgstr ""
@@ -1928,7 +2188,7 @@ msgstr ""
msgid "The block height of a particular block is defined as the number of blocks preceding it in the blockchain."
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38
#, elixir-autogen, elixir-format
msgid "The fallback function is executed on a call to the contract if none of the other functions match the given function signature, or if no data was supplied at all and there is no receive Ether function. The fallback function always receives data, but in order to also receive Ether it must be marked payable."
msgstr ""
@@ -1938,12 +2198,12 @@ msgstr ""
msgid "The hash of the block from which this block was generated."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:92
+#: lib/block_scout_web/templates/address/overview.html.eex:74
#, elixir-autogen, elixir-format
msgid "The name found in the source code of the Contract."
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:103
+#: lib/block_scout_web/templates/address/overview.html.eex:85
#, elixir-autogen, elixir-format
msgid "The name of the validator."
msgstr ""
@@ -1953,22 +2213,22 @@ msgstr ""
msgid "The number of transactions in the block."
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40
#, elixir-autogen, elixir-format
msgid "The receive function is executed on a call to the contract with empty calldata. This is the function that is executed on plain Ether transfers (e.g. via .send() or .transfer()). If no such function exists, but a payable fallback function exists, the fallback function will be called on a plain Ether transfer. If neither a receive Ether nor a payable fallback function is present, the contract cannot receive Ether through regular transactions and throws an exception."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:129
+#: lib/block_scout_web/templates/transaction/overview.html.eex:134
#, elixir-autogen, elixir-format
msgid "The revert reason of the transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:104
+#: lib/block_scout_web/templates/transaction/overview.html.eex:109
#, elixir-autogen, elixir-format
msgid "The status of the transaction: Confirmed or Unconfirmed."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68
#, elixir-autogen, elixir-format
msgid "The total amount of tokens issued"
msgstr ""
@@ -1988,7 +2248,7 @@ msgstr ""
msgid "There are no blocks."
msgstr ""
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:28
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:29
#, elixir-autogen, elixir-format
msgid "There are no holders for this Token."
msgstr ""
@@ -2033,7 +2293,7 @@ msgstr ""
msgid "There are no tokens for this address."
msgstr ""
-#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:27
+#: lib/block_scout_web/templates/tokens/inventory/index.html.eex:28
#, elixir-autogen, elixir-format
msgid "There are no tokens."
msgstr ""
@@ -2055,7 +2315,7 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:28
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:28
-#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:26
+#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:27
#, elixir-autogen, elixir-format
msgid "There are no transfers for this Token."
msgstr ""
@@ -2106,13 +2366,13 @@ msgstr ""
msgid "This is useful to allow sending requests to blockscout without having to change anything about the request."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:59
+#: lib/block_scout_web/templates/transaction/overview.html.eex:64
#, elixir-autogen, elixir-format
msgid "This transaction is pending confirmation."
msgstr ""
#: lib/block_scout_web/templates/block/overview.html.eex:71
-#: lib/block_scout_web/templates/transaction/overview.html.eex:172
+#: lib/block_scout_web/templates/transaction/overview.html.eex:177
#, elixir-autogen, elixir-format
msgid "Timestamp"
msgstr ""
@@ -2120,7 +2380,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:32
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:34
#: lib/block_scout_web/templates/address_transaction/index.html.eex:28
-#: lib/block_scout_web/templates/transaction/overview.html.eex:215
+#: lib/block_scout_web/templates/transaction/overview.html.eex:221
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_token_transfer_view.ex:9
#: lib/block_scout_web/views/address_transaction_view.ex:9
@@ -2145,19 +2405,19 @@ msgstr ""
msgid "Toggle navigation"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:73
+#: lib/block_scout_web/templates/address/overview.html.eex:55
#, elixir-autogen, elixir-format
msgid "Token"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:3
-#: lib/block_scout_web/views/transaction_view.ex:448
+#: lib/block_scout_web/views/transaction_view.ex:449
#, elixir-autogen, elixir-format
msgid "Token Burning"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:7
-#: lib/block_scout_web/views/transaction_view.ex:449
+#: lib/block_scout_web/views/transaction_view.ex:450
#, elixir-autogen, elixir-format
msgid "Token Creation"
msgstr ""
@@ -2168,7 +2428,7 @@ msgstr ""
msgid "Token Details"
msgstr ""
-#: lib/block_scout_web/templates/tokens/holder/index.html.eex:16
+#: lib/block_scout_web/templates/tokens/holder/index.html.eex:17
#: lib/block_scout_web/templates/tokens/instance/holder/index.html.eex:16
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:17
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:11
@@ -2185,14 +2445,14 @@ msgid "Token ID"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:5
-#: lib/block_scout_web/views/transaction_view.ex:447
+#: lib/block_scout_web/views/transaction_view.ex:448
#, elixir-autogen, elixir-format
msgid "Token Minting"
msgstr ""
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:9
#: lib/block_scout_web/templates/common_components/_token_transfer_type_display_name.html.eex:11
-#: lib/block_scout_web/views/transaction_view.ex:450
+#: lib/block_scout_web/views/transaction_view.ex:451
#, elixir-autogen, elixir-format
msgid "Token Transfer"
msgstr ""
@@ -2202,54 +2462,54 @@ msgstr ""
#: lib/block_scout_web/templates/tokens/instance/overview/_tabs.html.eex:3
#: lib/block_scout_web/templates/tokens/instance/transfer/index.html.eex:16
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:5
-#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:14
+#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:15
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:7
-#: lib/block_scout_web/views/address_view.ex:363
+#: lib/block_scout_web/views/address_view.ex:366
#: lib/block_scout_web/views/tokens/instance/overview_view.ex:195
#: lib/block_scout_web/views/tokens/overview_view.ex:39
-#: lib/block_scout_web/views/transaction_view.ex:511
+#: lib/block_scout_web/views/transaction_view.ex:512
#, elixir-autogen, elixir-format
msgid "Token Transfers"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:72
+#: lib/block_scout_web/templates/address/overview.html.eex:54
#, elixir-autogen, elixir-format
msgid "Token name and symbol."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:142
#, elixir-autogen, elixir-format
msgid "Token type"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:21
-#: lib/block_scout_web/templates/address/overview.html.eex:193
+#: lib/block_scout_web/templates/address/overview.html.eex:175
#: lib/block_scout_web/templates/address_token/overview.html.eex:58
#: lib/block_scout_web/templates/address_token_transfer/index.html.eex:13
#: lib/block_scout_web/templates/layout/_topnav.html.eex:73
#: lib/block_scout_web/templates/tokens/index.html.eex:10
-#: lib/block_scout_web/views/address_view.ex:360
+#: lib/block_scout_web/views/address_view.ex:363
#, elixir-autogen, elixir-format
msgid "Tokens"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:282
+#: lib/block_scout_web/templates/transaction/overview.html.eex:290
#, elixir-autogen, elixir-format
msgid "Tokens Burnt"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:298
+#: lib/block_scout_web/templates/transaction/overview.html.eex:306
#, elixir-autogen, elixir-format
msgid "Tokens Created"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:265
+#: lib/block_scout_web/templates/transaction/overview.html.eex:273
#, elixir-autogen, elixir-format
msgid "Tokens Minted"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:249
+#: lib/block_scout_web/templates/transaction/overview.html.eex:257
#, elixir-autogen, elixir-format
msgid "Tokens Transferred"
msgstr ""
@@ -2275,7 +2535,7 @@ msgstr ""
msgid "Total Difficulty"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:82
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:83
#, elixir-autogen, elixir-format
msgid "Total Supply * Price"
msgstr ""
@@ -2295,12 +2555,12 @@ msgstr ""
msgid "Total gas limit provided by all transactions in the block."
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:68
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:69
#, elixir-autogen, elixir-format
msgid "Total supply"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:329
+#: lib/block_scout_web/templates/transaction/overview.html.eex:337
#, elixir-autogen, elixir-format
msgid "Total transaction fee."
msgstr ""
@@ -2310,8 +2570,10 @@ msgstr ""
msgid "Total transactions"
msgstr ""
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:11
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:23
#: lib/block_scout_web/templates/address_logs/_logs.html.eex:19
-#: lib/block_scout_web/views/transaction_view.ex:460
+#: lib/block_scout_web/views/transaction_view.ex:461
#, elixir-autogen, elixir-format
msgid "Transaction"
msgstr ""
@@ -2326,7 +2588,7 @@ msgstr ""
msgid "Transaction %{transaction}, %{subnetwork} %{transaction}"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:404
+#: lib/block_scout_web/templates/transaction/overview.html.eex:412
#, elixir-autogen, elixir-format
msgid "Transaction Burnt Fee"
msgstr ""
@@ -2336,12 +2598,12 @@ msgstr ""
msgid "Transaction Details"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:330
+#: lib/block_scout_web/templates/transaction/overview.html.eex:338
#, elixir-autogen, elixir-format
msgid "Transaction Fee"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:75
+#: lib/block_scout_web/templates/transaction/overview.html.eex:80
#, elixir-autogen, elixir-format
msgid "Transaction Hash"
msgstr ""
@@ -2352,36 +2614,36 @@ msgstr ""
msgid "Transaction Inputs"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:354
+#: lib/block_scout_web/templates/transaction/overview.html.eex:362
#, elixir-autogen, elixir-format
msgid "Transaction Type"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:427
+#: lib/block_scout_web/templates/transaction/overview.html.eex:435
#, elixir-autogen, elixir-format
msgid "Transaction number from the sending address. Each transaction sent from an address increments the nonce by 1."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:353
+#: lib/block_scout_web/templates/transaction/overview.html.eex:361
#, elixir-autogen, elixir-format
msgid "Transaction type, introduced in EIP-2718."
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:7
-#: lib/block_scout_web/templates/address/overview.html.eex:204
-#: lib/block_scout_web/templates/address/overview.html.eex:210
-#: lib/block_scout_web/templates/address/overview.html.eex:218
+#: lib/block_scout_web/templates/address/overview.html.eex:186
+#: lib/block_scout_web/templates/address/overview.html.eex:192
+#: lib/block_scout_web/templates/address/overview.html.eex:200
#: lib/block_scout_web/templates/address_transaction/index.html.eex:13
#: lib/block_scout_web/templates/block/overview.html.eex:80
#: lib/block_scout_web/templates/block_transaction/index.html.eex:10
#: lib/block_scout_web/templates/chain/show.html.eex:213
#: lib/block_scout_web/templates/layout/_topnav.html.eex:48
-#: lib/block_scout_web/views/address_view.ex:362
+#: lib/block_scout_web/views/address_view.ex:365
#, elixir-autogen, elixir-format
msgid "Transactions"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:119
+#: lib/block_scout_web/templates/address/overview.html.eex:101
#, elixir-autogen, elixir-format
msgid "Transactions and address of creation."
msgstr ""
@@ -2391,11 +2653,11 @@ msgstr ""
msgid "Transactions sent"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:231
-#: lib/block_scout_web/templates/address/overview.html.eex:237
-#: lib/block_scout_web/templates/address/overview.html.eex:245
+#: lib/block_scout_web/templates/address/overview.html.eex:213
+#: lib/block_scout_web/templates/address/overview.html.eex:219
+#: lib/block_scout_web/templates/address/overview.html.eex:227
#: lib/block_scout_web/templates/tokens/instance/overview/_details.html.eex:50
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:119
#, elixir-autogen, elixir-format
msgid "Transfers"
msgstr ""
@@ -2422,12 +2684,12 @@ msgstr ""
msgid "Type"
msgstr ""
-#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:140
+#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:141
#, elixir-autogen, elixir-format
msgid "Type of the token standard"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:453
+#: lib/block_scout_web/templates/transaction/overview.html.eex:461
#, elixir-autogen, elixir-format
msgid "UTF-8"
msgstr ""
@@ -2443,7 +2705,7 @@ msgstr ""
msgid "Uncles"
msgstr ""
-#: lib/block_scout_web/views/transaction_view.ex:341
+#: lib/block_scout_web/views/transaction_view.ex:342
#, elixir-autogen, elixir-format
msgid "Unconfirmed"
msgstr ""
@@ -2453,17 +2715,17 @@ msgstr ""
msgid "Unique Token"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:74
+#: lib/block_scout_web/templates/transaction/overview.html.eex:79
#, elixir-autogen, elixir-format
msgid "Unique character string (TxID) assigned to every verified transaction."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:383
+#: lib/block_scout_web/templates/transaction/overview.html.eex:391
#, elixir-autogen, elixir-format
msgid "User defined maximum fee (tip) per unit of gas paid to validator for transaction prioritization."
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:393
+#: lib/block_scout_web/templates/transaction/overview.html.eex:401
#, elixir-autogen, elixir-format
msgid "User-defined tip sent to validator for transaction priority/inclusion."
msgstr ""
@@ -2493,26 +2755,27 @@ msgstr ""
msgid "Validator Data"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:51
-#, elixir-autogen, elixir-format
-msgid "Validator Info"
-msgstr ""
-
-#: lib/block_scout_web/templates/address/overview.html.eex:104
+#: lib/block_scout_web/templates/address/overview.html.eex:86
#, elixir-autogen, elixir-format
msgid "Validator Name"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:315
+#: lib/block_scout_web/templates/transaction/overview.html.eex:323
#, elixir-autogen, elixir-format
msgid "Value"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:314
+#: lib/block_scout_web/templates/transaction/overview.html.eex:322
#, elixir-autogen, elixir-format
msgid "Value sent in the native token (and USD) if applicable."
msgstr ""
+#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
+#: lib/block_scout_web/templates/address_write_contract/index.html.eex:15
+#, elixir-autogen, elixir-format
+msgid "Verified"
+msgstr ""
+
#: lib/block_scout_web/templates/address_contract/index.html.eex:82
#, elixir-autogen, elixir-format
msgid "Verified at"
@@ -2520,10 +2783,8 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract/index.html.eex:27
#: lib/block_scout_web/templates/address_contract/index.html.eex:29
-#: lib/block_scout_web/templates/address_contract/index.html.eex:160
-#: lib/block_scout_web/templates/address_contract/index.html.eex:166
-#: lib/block_scout_web/templates/address_contract/index.html.eex:197
-#: lib/block_scout_web/templates/address_contract/index.html.eex:203
+#: lib/block_scout_web/templates/address_contract/index.html.eex:161
+#: lib/block_scout_web/templates/address_contract/index.html.eex:192
#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:14
#, elixir-autogen, elixir-format
msgid "Verify & Publish"
@@ -2586,12 +2847,12 @@ msgstr ""
msgid "View Contract"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:67
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:73
#, elixir-autogen, elixir-format
msgid "View Less Transfers"
msgstr ""
-#: lib/block_scout_web/templates/transaction/_tile.html.eex:66
+#: lib/block_scout_web/templates/transaction/_tile.html.eex:72
#, elixir-autogen, elixir-format
msgid "View More Transfers"
msgstr ""
@@ -2626,7 +2887,7 @@ msgstr ""
msgid "Vyper contract"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:145
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:136
#, elixir-autogen, elixir-format
msgid "WEI"
msgstr ""
@@ -2646,26 +2907,33 @@ msgstr ""
msgid "Warning! Contract bytecode has been changed and doesn't match the verified one. Therefore, interaction with this smart contract may be risky."
msgstr ""
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:7
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "Watch list"
+msgstr ""
+
#: lib/block_scout_web/views/wei_helpers.ex:76
#, elixir-autogen, elixir-format
msgid "Wei"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:109
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:100
#, elixir-autogen, elixir-format
msgid "Write"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:95
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:34
-#: lib/block_scout_web/views/address_view.ex:368
+#: lib/block_scout_web/views/address_view.ex:371
#, elixir-autogen, elixir-format
msgid "Write Contract"
msgstr ""
#: lib/block_scout_web/templates/address/_tabs.html.eex:102
#: lib/block_scout_web/templates/tokens/overview/_tabs.html.eex:48
-#: lib/block_scout_web/views/address_view.ex:369
+#: lib/block_scout_web/views/address_view.ex:372
#, elixir-autogen, elixir-format
msgid "Write Proxy"
msgstr ""
@@ -2678,7 +2946,32 @@ msgstr ""
msgid "Yes"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:129
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "You can create 3 API keys per account."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/index.html.eex:18
+#, elixir-autogen, elixir-format
+msgid "You can create up to 15 Custom ABIs per account."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/index.html.eex:11
+#, elixir-autogen, elixir-format
+msgid "You can request a public category tag which is displayed to all Blockscout users. Public tags may be added to contract or external addresses, and any associated transactions will inherit that tag. Clicking a tag opens a page with related information and helps provide context and data organization. Requests are sent to a moderator for review and approval. This process can take several days."
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:15
+#, elixir-autogen, elixir-format
+msgid "Your name"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/public_tags_request/form.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "Your name*"
+msgstr ""
+
+#: lib/block_scout_web/templates/address/overview.html.eex:111
#, elixir-autogen, elixir-format
msgid "at"
msgstr ""
@@ -2688,7 +2981,7 @@ msgstr ""
msgid "balance of the address"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:403
+#: lib/block_scout_web/templates/transaction/overview.html.eex:411
#, elixir-autogen, elixir-format
msgid "burned for this transaction. Equals Block Base Fee per Gas * Gas Used."
msgstr ""
@@ -2703,7 +2996,7 @@ msgstr ""
msgid "button"
msgstr ""
-#: lib/block_scout_web/templates/transaction/overview.html.eex:223
+#: lib/block_scout_web/templates/transaction/overview.html.eex:230
#, elixir-autogen, elixir-format
msgid "created"
msgstr ""
@@ -2713,7 +3006,7 @@ msgstr ""
msgid "custom RPC"
msgstr ""
-#: lib/block_scout_web/templates/address/overview.html.eex:167
+#: lib/block_scout_web/templates/address/overview.html.eex:149
#, elixir-autogen, elixir-format
msgid "doesn't include ERC20, ERC721, ERC1155 tokens)."
msgstr ""
@@ -2723,7 +3016,7 @@ msgstr ""
msgid "elements are displayed"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:41
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:38
#, elixir-autogen, elixir-format
msgid "fallback"
msgstr ""
@@ -2765,7 +3058,7 @@ msgstr ""
msgid "page"
msgstr ""
-#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:43
+#: lib/block_scout_web/templates/smart_contract/_functions.html.eex:40
#, elixir-autogen, elixir-format
msgid "receive"
msgstr ""
@@ -2876,3 +3169,120 @@ msgstr ""
#, elixir-autogen, elixir-format
msgid "The requested path was not found on BlockScout."
msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:25
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Actions"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:38
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Add address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Add address to the Watch list"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:81
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Back to Watch list (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:7
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Edit Watch list address"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:77
+#, elixir-autogen, elixir-format
+msgid "Remove from Watch list"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have address tags yet"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist/show.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have addresses on you watchlist yet"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:14
+#, elixir-autogen, elixir-format
+msgid "You don't have transaction tags yet"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:13
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:14
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:29
+#, elixir-autogen, elixir-format, fuzzy
+msgid "API key"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/index.html.eex:7
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:16
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:17
+#, elixir-autogen, elixir-format
+msgid "API keys"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:10
+#: lib/block_scout_web/templates/account/tag_address/index.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:15
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Address Tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:25
+#, elixir-autogen, elixir-format
+msgid "Back to API keys (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_address/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "Back to Address Tags (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:30
+#, elixir-autogen, elixir-format
+msgid "Back to Custom ABI (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/tag_transaction/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "Back to Transaction Tags (Cancel)"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:30
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:43
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:56
+#, elixir-autogen, elixir-format
+msgid "Incoming"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:68
+#, elixir-autogen, elixir-format
+msgid "Please select notification methods:"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/watchlist_address/form.html.eex:24
+#, elixir-autogen, elixir-format
+msgid "Please select what types of notifications you will receive:"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/common/_nav.html.eex:13
+#: lib/block_scout_web/templates/account/tag_transaction/index.html.eex:7
+#: lib/block_scout_web/templates/layout/_account_menu_item.html.eex:16
+#, elixir-autogen, elixir-format, fuzzy
+msgid "Transaction Tags"
+msgstr ""
+
+#: lib/block_scout_web/templates/account/api_key/form.html.eex:7
+#: lib/block_scout_web/templates/account/custom_abi/form.html.eex:8
+#, elixir-autogen, elixir-format
+msgid "Update"
+msgstr ""
diff --git a/apps/block_scout_web/test/block_scout_web/chain_test.exs b/apps/block_scout_web/test/block_scout_web/chain_test.exs
index c215da886a..7656ff2259 100644
--- a/apps/block_scout_web/test/block_scout_web/chain_test.exs
+++ b/apps/block_scout_web/test/block_scout_web/chain_test.exs
@@ -76,7 +76,7 @@ defmodule BlockScoutWeb.ChainTest do
test "correctly encodes decimal values" do
val = Decimal.from_float(5.55)
- assert "5.55" == Poison.encode!(val)
+ assert "\"5.55\"" == Poison.encode!(val)
end
end
end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs
new file mode 100644
index 0000000000..2ce2a92d62
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs
@@ -0,0 +1,919 @@
+defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias BlockScoutWeb.Models.UserFromAuth
+
+ setup %{conn: conn} do
+ auth = build(:auth)
+
+ {:ok, user} = UserFromAuth.find_or_create(auth)
+
+ {:ok, user: user, conn: Plug.Test.init_test_session(conn, current_user: user)}
+ end
+
+ describe "Test account/api/v1/user" do
+ test "get user info", %{conn: conn, user: user} do
+ result_conn =
+ conn
+ |> get("/api/account/v1/user/info")
+ |> doc(description: "Get info about user")
+
+ assert json_response(result_conn, 200) == %{
+ "nickname" => user.nickname,
+ "name" => user.name,
+ "email" => user.email,
+ "avatar" => user.avatar
+ }
+ end
+
+ test "post private address tag", %{conn: conn} do
+ tag_address_response =
+ conn
+ |> post("/api/account/v1/user/tags/address", %{
+ "address_hash" => "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b",
+ "name" => "MyName"
+ })
+ |> doc(description: "Add private address tag")
+ |> json_response(200)
+
+ conn
+ |> get("/api/account/v1/tags/address/0x3e9ac8f16c92bc4f093357933b5befbf1e16987b")
+ |> doc(description: "Get tags for address")
+ |> json_response(200)
+
+ assert tag_address_response["address_hash"] == "0x3e9ac8f16c92bc4f093357933b5befbf1e16987b"
+ assert tag_address_response["name"] == "MyName"
+ assert tag_address_response["id"]
+ end
+
+ test "edit private address tag", %{conn: conn} do
+ address_tag = build(:tag_address)
+
+ tag_address_response =
+ conn
+ |> post("/api/account/v1/user/tags/address", address_tag)
+ |> json_response(200)
+
+ response =
+ conn
+ |> get("/api/account/v1/user/tags/address")
+ |> json_response(200) == [tag_address_response]
+
+ assert tag_address_response["address_hash"] == address_tag["address_hash"]
+ assert tag_address_response["name"] == address_tag["name"]
+ assert tag_address_response["id"]
+
+ new_address_tag = build(:tag_address)
+
+ new_tag_address_response =
+ conn
+ |> put("/api/account/v1/user/tags/address/#{tag_address_response["id"]}", new_address_tag)
+ |> doc(description: "Edit private address tag")
+ |> json_response(200)
+
+ assert new_tag_address_response["address_hash"] == new_address_tag["address_hash"]
+ assert new_tag_address_response["name"] == new_address_tag["name"]
+ assert new_tag_address_response["id"] == tag_address_response["id"]
+ end
+
+ test "get address tags after inserting one private tags", %{conn: conn} do
+ addresses = Enum.map(0..2, fn _x -> to_string(build(:address).hash) end)
+ names = Enum.map(0..2, fn x -> "name#{x}" end)
+ zipped = Enum.zip(addresses, names)
+
+ created =
+ Enum.map(zipped, fn {addr, name} ->
+ id =
+ (conn
+ |> post("/api/account/v1/user/tags/address", %{
+ "address_hash" => addr,
+ "name" => name
+ })
+ |> json_response(200))["id"]
+
+ {addr, %{"display_name" => name, "label" => name, "address_hash" => addr},
+ %{"address_hash" => addr, "id" => id, "name" => name}}
+ end)
+
+ assert Enum.all?(created, fn {addr, map_tag, _} ->
+ response =
+ conn
+ |> get("/api/account/v1/tags/address/#{addr}")
+ |> json_response(200)
+
+ response["personal_tags"] == [map_tag]
+ end)
+
+ response =
+ conn
+ |> get("/api/account/v1/user/tags/address")
+ |> doc(description: "Get private addresses tags")
+ |> json_response(200)
+
+ assert Enum.all?(created, fn {_, _, map} -> map in response end)
+ end
+
+ test "delete address tag", %{conn: conn} do
+ addresses = Enum.map(0..2, fn _x -> to_string(build(:address).hash) end)
+ names = Enum.map(0..2, fn x -> "name#{x}" end)
+ zipped = Enum.zip(addresses, names)
+
+ created =
+ Enum.map(zipped, fn {addr, name} ->
+ id =
+ (conn
+ |> post("/api/account/v1/user/tags/address", %{
+ "address_hash" => addr,
+ "name" => name
+ })
+ |> json_response(200))["id"]
+
+ {addr, %{"display_name" => name, "label" => name, "address_hash" => addr},
+ %{"address_hash" => addr, "id" => id, "name" => name}}
+ end)
+
+ assert Enum.all?(created, fn {addr, map_tag, _} ->
+ response =
+ conn
+ |> get("/api/account/v1/tags/address/#{addr}")
+ |> json_response(200)
+
+ response["personal_tags"] == [map_tag]
+ end)
+
+ response =
+ conn
+ |> get("/api/account/v1/user/tags/address")
+ |> json_response(200)
+
+ assert Enum.all?(created, fn {_, _, map} -> map in response end)
+
+ {_, _, %{"id" => id}} = Enum.at(created, 0)
+
+ assert conn
+ |> delete("/api/account/v1/user/tags/address/#{id}")
+ |> doc("Delete private address tag")
+ |> json_response(200) == %{"message" => "OK"}
+
+ assert Enum.all?(Enum.drop(created, 1), fn {_, _, %{"id" => id}} ->
+ conn
+ |> delete("/api/account/v1/user/tags/address/#{id}")
+ |> json_response(200) == %{"message" => "OK"}
+ end)
+
+ assert conn
+ |> get("/api/account/v1/user/tags/address")
+ |> json_response(200) == []
+
+ assert Enum.all?(created, fn {addr, _, _} ->
+ response =
+ conn
+ |> get("/api/account/v1/tags/address/#{addr}")
+ |> json_response(200)
+
+ response["personal_tags"] == []
+ end)
+ end
+
+ test "post private transaction tag", %{conn: conn} do
+ tx_hash_non_existing = to_string(build(:transaction).hash)
+ tx_hash = to_string(insert(:transaction).hash)
+
+ assert conn
+ |> post("/api/account/v1/user/tags/transaction", %{
+ "transaction_hash" => tx_hash_non_existing,
+ "name" => "MyName"
+ })
+ |> doc(description: "Error on try to create private transaction tag for tx does not exist")
+ |> json_response(422) == %{"errors" => %{"tx_hash" => ["Transaction does not exist"]}}
+
+ tag_transaction_response =
+ conn
+ |> post("/api/account/v1/user/tags/transaction", %{
+ "transaction_hash" => tx_hash,
+ "name" => "MyName"
+ })
+ |> doc(description: "Create private transaction tag")
+ |> json_response(200)
+
+ conn
+ |> get("/api/account/v1/tags/transaction/#{tx_hash}")
+ |> doc(description: "Get tags for transaction")
+ |> json_response(200)
+
+ assert tag_transaction_response["transaction_hash"] == tx_hash
+ assert tag_transaction_response["name"] == "MyName"
+ assert tag_transaction_response["id"]
+ end
+
+ test "edit private transaction tag", %{conn: conn} do
+ tx_tag = build(:tag_transaction)
+
+ tag_response =
+ conn
+ |> post("/api/account/v1/user/tags/transaction", tx_tag)
+ |> json_response(200)
+
+ response =
+ conn
+ |> get("/api/account/v1/user/tags/transaction")
+ |> json_response(200) == [tag_response]
+
+ assert tag_response["address_hash"] == tx_tag["address_hash"]
+ assert tag_response["name"] == tx_tag["name"]
+ assert tag_response["id"]
+
+ new_tx_tag = build(:tag_transaction)
+
+ new_tag_response =
+ conn
+ |> put("/api/account/v1/user/tags/transaction/#{tag_response["id"]}", new_tx_tag)
+ |> doc(description: "Edit private transaction tag")
+ |> json_response(200)
+
+ assert new_tag_response["address_hash"] == new_tx_tag["address_hash"]
+ assert new_tag_response["name"] == new_tx_tag["name"]
+ assert new_tag_response["id"] == tag_response["id"]
+ end
+
+ test "get transaction tags after inserting one private tags", %{conn: conn} do
+ transactions = Enum.map(0..2, fn _x -> to_string(insert(:transaction).hash) end)
+ names = Enum.map(0..2, fn x -> "name#{x}" end)
+ zipped = Enum.zip(transactions, names)
+
+ created =
+ Enum.map(zipped, fn {tx_hash, name} ->
+ id =
+ (conn
+ |> post("/api/account/v1/user/tags/transaction", %{
+ "transaction_hash" => tx_hash,
+ "name" => name
+ })
+ |> json_response(200))["id"]
+
+ {tx_hash, %{"label" => name}, %{"transaction_hash" => tx_hash, "id" => id, "name" => name}}
+ end)
+
+ assert Enum.all?(created, fn {tx_hash, map_tag, _} ->
+ response =
+ conn
+ |> get("/api/account/v1/tags/transaction/#{tx_hash}")
+ |> json_response(200)
+
+ response["personal_tx_tag"] == map_tag
+ end)
+
+ response =
+ conn
+ |> get("/api/account/v1/user/tags/transaction")
+ |> doc(description: "Get private transactions tags")
+ |> json_response(200)
+
+ assert Enum.all?(created, fn {_, _, map} -> map in response end)
+ end
+
+ test "delete transaction tag", %{conn: conn} do
+ transactions = Enum.map(0..2, fn _x -> to_string(insert(:transaction).hash) end)
+ names = Enum.map(0..2, fn x -> "name#{x}" end)
+ zipped = Enum.zip(transactions, names)
+
+ created =
+ Enum.map(zipped, fn {tx_hash, name} ->
+ id =
+ (conn
+ |> post("/api/account/v1/user/tags/transaction", %{
+ "transaction_hash" => tx_hash,
+ "name" => name
+ })
+ |> json_response(200))["id"]
+
+ {tx_hash, %{"label" => name}, %{"transaction_hash" => tx_hash, "id" => id, "name" => name}}
+ end)
+
+ assert Enum.all?(created, fn {tx_hash, map_tag, _} ->
+ response =
+ conn
+ |> get("/api/account/v1/tags/transaction/#{tx_hash}")
+ |> json_response(200)
+
+ response["personal_tx_tag"] == map_tag
+ end)
+
+ response =
+ conn
+ |> get("/api/account/v1/user/tags/transaction")
+ |> json_response(200)
+
+ assert Enum.all?(created, fn {_, _, map} -> map in response end)
+
+ {_, _, %{"id" => id}} = Enum.at(created, 0)
+
+ assert conn
+ |> delete("/api/account/v1/user/tags/transaction/#{id}")
+ |> doc("Delete private transaction tag")
+ |> json_response(200) == %{"message" => "OK"}
+
+ assert Enum.all?(Enum.drop(created, 1), fn {_, _, %{"id" => id}} ->
+ conn
+ |> delete("/api/account/v1/user/tags/transaction/#{id}")
+ |> json_response(200) == %{"message" => "OK"}
+ end)
+
+ assert conn
+ |> get("/api/account/v1/user/tags/transaction")
+ |> json_response(200) == []
+
+ assert Enum.all?(created, fn {addr, _, _} ->
+ response =
+ conn
+ |> get("/api/account/v1/tags/transaction/#{addr}")
+ |> json_response(200)
+
+ response["personal_tx_tag"] == nil
+ end)
+ end
+
+ test "post && get watchlist address", %{conn: conn} do
+ watchlist_address_map = build(:watchlist_address)
+
+ post_watchlist_address_response =
+ conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ watchlist_address_map
+ )
+ |> doc(description: "Add address to watch list")
+ |> json_response(200)
+
+ assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"]
+ assert post_watchlist_address_response["name"] == watchlist_address_map["name"]
+ assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"]
+ assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"]
+
+ get_watchlist_address_response = conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0)
+
+ assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"]
+ assert get_watchlist_address_response["name"] == watchlist_address_map["name"]
+ assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"]
+ assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"]
+ assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"]
+
+ watchlist_address_map_1 = build(:watchlist_address)
+
+ post_watchlist_address_response_1 =
+ conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ watchlist_address_map_1
+ )
+ |> json_response(200)
+
+ get_watchlist_address_response_1_0 =
+ conn
+ |> get("/api/account/v1/user/watchlist")
+ |> doc(description: "Get addresses from watchlists")
+ |> json_response(200)
+ |> Enum.at(1)
+
+ get_watchlist_address_response_1_1 =
+ conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0)
+
+ assert get_watchlist_address_response_1_0 == get_watchlist_address_response
+
+ assert get_watchlist_address_response_1_1["notification_settings"] ==
+ watchlist_address_map_1["notification_settings"]
+
+ assert get_watchlist_address_response_1_1["name"] == watchlist_address_map_1["name"]
+
+ assert get_watchlist_address_response_1_1["notification_methods"] ==
+ watchlist_address_map_1["notification_methods"]
+
+ assert get_watchlist_address_response_1_1["address_hash"] == watchlist_address_map_1["address_hash"]
+ assert get_watchlist_address_response_1_1["id"] == post_watchlist_address_response_1["id"]
+ end
+
+ test "delete watchlist address", %{conn: conn} do
+ watchlist_address_map = build(:watchlist_address)
+
+ post_watchlist_address_response =
+ conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ watchlist_address_map
+ )
+ |> json_response(200)
+
+ assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"]
+ assert post_watchlist_address_response["name"] == watchlist_address_map["name"]
+ assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"]
+ assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"]
+
+ get_watchlist_address_response = conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0)
+
+ assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"]
+ assert get_watchlist_address_response["name"] == watchlist_address_map["name"]
+ assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"]
+ assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"]
+ assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"]
+
+ watchlist_address_map_1 = build(:watchlist_address)
+
+ post_watchlist_address_response_1 =
+ conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ watchlist_address_map_1
+ )
+ |> json_response(200)
+
+ get_watchlist_address_response_1_0 =
+ conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(1)
+
+ get_watchlist_address_response_1_1 =
+ conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0)
+
+ assert get_watchlist_address_response_1_0 == get_watchlist_address_response
+
+ assert get_watchlist_address_response_1_1["notification_settings"] ==
+ watchlist_address_map_1["notification_settings"]
+
+ assert get_watchlist_address_response_1_1["name"] == watchlist_address_map_1["name"]
+
+ assert get_watchlist_address_response_1_1["notification_methods"] ==
+ watchlist_address_map_1["notification_methods"]
+
+ assert get_watchlist_address_response_1_1["address_hash"] == watchlist_address_map_1["address_hash"]
+ assert get_watchlist_address_response_1_1["id"] == post_watchlist_address_response_1["id"]
+
+ assert conn
+ |> delete("/api/account/v1/user/watchlist/#{get_watchlist_address_response_1_1["id"]}")
+ |> doc(description: "Delete address from watchlist by id")
+ |> json_response(200) == %{"message" => "OK"}
+
+ assert conn
+ |> delete("/api/account/v1/user/watchlist/#{get_watchlist_address_response_1_0["id"]}")
+ |> json_response(200) == %{"message" => "OK"}
+
+ assert conn |> get("/api/account/v1/user/watchlist") |> json_response(200) == []
+ end
+
+ test "put watchlist address", %{conn: conn} do
+ watchlist_address_map = build(:watchlist_address)
+
+ post_watchlist_address_response =
+ conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ watchlist_address_map
+ )
+ |> json_response(200)
+
+ assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"]
+ assert post_watchlist_address_response["name"] == watchlist_address_map["name"]
+ assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"]
+ assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"]
+
+ get_watchlist_address_response = conn |> get("/api/account/v1/user/watchlist") |> json_response(200) |> Enum.at(0)
+
+ assert get_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"]
+ assert get_watchlist_address_response["name"] == watchlist_address_map["name"]
+ assert get_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"]
+ assert get_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"]
+ assert get_watchlist_address_response["id"] == post_watchlist_address_response["id"]
+
+ new_watchlist_address_map = build(:watchlist_address)
+
+ put_watchlist_address_response =
+ conn
+ |> put(
+ "/api/account/v1/user/watchlist/#{post_watchlist_address_response["id"]}",
+ new_watchlist_address_map
+ )
+ |> doc(description: "Edit watchlist address")
+ |> json_response(200)
+
+ assert put_watchlist_address_response["notification_settings"] ==
+ new_watchlist_address_map["notification_settings"]
+
+ assert put_watchlist_address_response["name"] == new_watchlist_address_map["name"]
+ assert put_watchlist_address_response["notification_methods"] == new_watchlist_address_map["notification_methods"]
+ assert put_watchlist_address_response["address_hash"] == new_watchlist_address_map["address_hash"]
+ assert get_watchlist_address_response["id"] == put_watchlist_address_response["id"]
+ end
+
+ test "cannot create duplicate of watchlist address", %{conn: conn} do
+ watchlist_address_map = build(:watchlist_address)
+
+ post_watchlist_address_response =
+ conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ watchlist_address_map
+ )
+ |> json_response(200)
+
+ assert post_watchlist_address_response["notification_settings"] == watchlist_address_map["notification_settings"]
+ assert post_watchlist_address_response["name"] == watchlist_address_map["name"]
+ assert post_watchlist_address_response["notification_methods"] == watchlist_address_map["notification_methods"]
+ assert post_watchlist_address_response["address_hash"] == watchlist_address_map["address_hash"]
+
+ assert conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ watchlist_address_map
+ )
+ |> doc(description: "Example of error on creating watchlist address")
+ |> json_response(422) == %{"errors" => %{"watchlist_id" => ["Address already added to the watch list"]}}
+
+ new_watchlist_address_map = build(:watchlist_address)
+
+ post_watchlist_address_response_1 =
+ conn
+ |> post(
+ "/api/account/v1/user/watchlist",
+ new_watchlist_address_map
+ )
+ |> json_response(200)
+
+ assert conn
+ |> put(
+ "/api/account/v1/user/watchlist/#{post_watchlist_address_response_1["id"]}",
+ watchlist_address_map
+ )
+ |> doc(description: "Example of error on editing watchlist address")
+ |> json_response(422) == %{"errors" => %{"watchlist_id" => ["Address already added to the watch list"]}}
+ end
+
+ test "post api key", %{conn: conn} do
+ post_api_key_response =
+ conn
+ |> post(
+ "/api/account/v1/user/api_keys",
+ %{"name" => "test"}
+ )
+ |> doc(description: "Add api key")
+ |> json_response(200)
+
+ assert post_api_key_response["name"] == "test"
+ assert post_api_key_response["api_key"]
+ end
+
+ test "can create not more than 3 api keys + get api keys", %{conn: conn} do
+ Enum.each(0..2, fn _x ->
+ conn
+ |> post(
+ "/api/account/v1/user/api_keys",
+ %{"name" => "test"}
+ )
+ |> json_response(200)
+ end)
+
+ assert conn
+ |> post(
+ "/api/account/v1/user/api_keys",
+ %{"name" => "test"}
+ )
+ |> doc(description: "Example of error on creating api key")
+ |> json_response(422) == %{"errors" => %{"name" => ["Max 3 keys per account"]}}
+
+ assert conn
+ |> get("/api/account/v1/user/api_keys")
+ |> doc(description: "Get api keys list")
+ |> json_response(200)
+ |> Enum.count() == 3
+ end
+
+ test "edit api key", %{conn: conn} do
+ post_api_key_response =
+ conn
+ |> post(
+ "/api/account/v1/user/api_keys",
+ %{"name" => "test"}
+ )
+ |> json_response(200)
+
+ assert post_api_key_response["name"] == "test"
+ assert post_api_key_response["api_key"]
+
+ put_api_key_response =
+ conn
+ |> put(
+ "/api/account/v1/user/api_keys/#{post_api_key_response["api_key"]}",
+ %{"name" => "test_1"}
+ )
+ |> doc(description: "Edit api key")
+ |> json_response(200)
+
+ assert put_api_key_response["api_key"] == post_api_key_response["api_key"]
+ assert put_api_key_response["name"] == "test_1"
+
+ assert conn
+ |> get("/api/account/v1/user/api_keys")
+ |> json_response(200) == [put_api_key_response]
+ end
+
+ test "delete api key", %{conn: conn} do
+ post_api_key_response =
+ conn
+ |> post(
+ "/api/account/v1/user/api_keys",
+ %{"name" => "test"}
+ )
+ |> json_response(200)
+
+ assert post_api_key_response["name"] == "test"
+ assert post_api_key_response["api_key"]
+
+ assert conn
+ |> get("/api/account/v1/user/api_keys")
+ |> json_response(200)
+ |> Enum.count() == 1
+
+ assert conn
+ |> delete("/api/account/v1/user/api_keys/#{post_api_key_response["api_key"]}")
+ |> doc(description: "Delete api key")
+ |> json_response(200) == %{"message" => "OK"}
+
+ assert conn
+ |> get("/api/account/v1/user/api_keys")
+ |> json_response(200) == []
+ end
+
+ test "post custom abi", %{conn: conn} do
+ custom_abi = build(:custom_abi)
+
+ post_custom_abi_response =
+ conn
+ |> post(
+ "/api/account/v1/user/custom_abis",
+ custom_abi
+ )
+ |> doc(description: "Add custom abi")
+ |> json_response(200)
+
+ assert post_custom_abi_response["name"] == custom_abi["name"]
+ assert post_custom_abi_response["abi"] == custom_abi["abi"]
+ assert post_custom_abi_response["contract_address_hash"] == custom_abi["contract_address_hash"]
+ assert post_custom_abi_response["id"]
+ end
+
+ test "can create not more than 15 custom abis + get custom abi", %{conn: conn} do
+ Enum.each(0..14, fn _x ->
+ conn
+ |> post(
+ "/api/account/v1/user/custom_abis",
+ build(:custom_abi)
+ )
+ |> json_response(200)
+ end)
+
+ assert conn
+ |> post(
+ "/api/account/v1/user/custom_abis",
+ build(:custom_abi)
+ )
+ |> doc(description: "Example of error on creating custom abi")
+ |> json_response(422) == %{"errors" => %{"name" => ["Max 15 ABIs per account"]}}
+
+ assert conn
+ |> get("/api/account/v1/user/custom_abis")
+ |> doc(description: "Get custom abis list")
+ |> json_response(200)
+ |> Enum.count() == 15
+ end
+
+ test "edit custom abi", %{conn: conn} do
+ custom_abi = build(:custom_abi)
+
+ post_custom_abi_response =
+ conn
+ |> post(
+ "/api/account/v1/user/custom_abis",
+ custom_abi
+ )
+ |> json_response(200)
+
+ assert post_custom_abi_response["name"] == custom_abi["name"]
+ assert post_custom_abi_response["abi"] == custom_abi["abi"]
+ assert post_custom_abi_response["contract_address_hash"] == custom_abi["contract_address_hash"]
+ assert post_custom_abi_response["id"]
+
+ custom_abi_1 = build(:custom_abi)
+
+ put_custom_abi_response =
+ conn
+ |> put(
+ "/api/account/v1/user/custom_abis/#{post_custom_abi_response["id"]}",
+ custom_abi_1
+ )
+ |> doc(description: "Edit custom abi")
+ |> json_response(200)
+
+ assert put_custom_abi_response["name"] == custom_abi_1["name"]
+ assert put_custom_abi_response["id"] == post_custom_abi_response["id"]
+ assert put_custom_abi_response["contract_address_hash"] == custom_abi_1["contract_address_hash"]
+ assert put_custom_abi_response["abi"] == custom_abi_1["abi"]
+
+ assert conn
+ |> get("/api/account/v1/user/custom_abis")
+ |> json_response(200) == [put_custom_abi_response]
+ end
+
+ test "delete custom abi", %{conn: conn} do
+ custom_abi = build(:custom_abi)
+
+ post_custom_abi_response =
+ conn
+ |> post(
+ "/api/account/v1/user/custom_abis",
+ custom_abi
+ )
+ |> json_response(200)
+
+ assert post_custom_abi_response["name"] == custom_abi["name"]
+ assert post_custom_abi_response["id"]
+
+ assert conn
+ |> get("/api/account/v1/user/custom_abis")
+ |> json_response(200)
+ |> Enum.count() == 1
+
+ assert conn
+ |> delete("/api/account/v1/user/custom_abis/#{post_custom_abi_response["id"]}")
+ |> doc(description: "Delete custom abi")
+ |> json_response(200) == %{"message" => "OK"}
+
+ assert conn
+ |> get("/api/account/v1/user/custom_abis")
+ |> json_response(200) == []
+ end
+ end
+
+ describe "public tags" do
+ test "create public tags reuqest", %{conn: conn} do
+ public_tags_request = build(:public_tags_request)
+
+ post_public_tasg_request_response =
+ conn
+ |> post(
+ "/api/account/v1/user/public_tags",
+ public_tags_request
+ )
+ |> doc(description: "Submit request to add a public tag")
+ |> json_response(200)
+
+ assert post_public_tasg_request_response["full_name"] == public_tags_request["full_name"]
+ assert post_public_tasg_request_response["email"] == public_tags_request["email"]
+ assert post_public_tasg_request_response["tags"] == public_tags_request["tags"]
+ assert post_public_tasg_request_response["website"] == public_tags_request["website"]
+ assert post_public_tasg_request_response["additional_comment"] == public_tags_request["additional_comment"]
+ assert post_public_tasg_request_response["addresses"] == public_tags_request["addresses"]
+ assert post_public_tasg_request_response["company"] == public_tags_request["company"]
+ assert post_public_tasg_request_response["is_owner"] == public_tags_request["is_owner"]
+ assert post_public_tasg_request_response["id"]
+ end
+
+ test "get one public tags requests", %{conn: conn} do
+ public_tags_request = build(:public_tags_request)
+
+ post_public_tasg_request_response =
+ conn
+ |> post(
+ "/api/account/v1/user/public_tags",
+ public_tags_request
+ )
+ |> json_response(200)
+
+ assert post_public_tasg_request_response["full_name"] == public_tags_request["full_name"]
+ assert post_public_tasg_request_response["email"] == public_tags_request["email"]
+ assert post_public_tasg_request_response["tags"] == public_tags_request["tags"]
+ assert post_public_tasg_request_response["website"] == public_tags_request["website"]
+ assert post_public_tasg_request_response["additional_comment"] == public_tags_request["additional_comment"]
+ assert post_public_tasg_request_response["addresses"] == public_tags_request["addresses"]
+ assert post_public_tasg_request_response["company"] == public_tags_request["company"]
+ assert post_public_tasg_request_response["is_owner"] == public_tags_request["is_owner"]
+ assert post_public_tasg_request_response["id"]
+
+ assert conn
+ |> get("/api/account/v1/user/public_tags")
+ |> json_response(200)
+ |> Enum.map(&convert_date/1) ==
+ [post_public_tasg_request_response]
+ |> Enum.map(&convert_date/1)
+ end
+
+ test "get and delete several public tags requests", %{conn: conn} do
+ public_tags_list = build_list(10, :public_tags_request)
+
+ final_list =
+ public_tags_list
+ |> Enum.map(fn request ->
+ response =
+ conn
+ |> post(
+ "/api/account/v1/user/public_tags",
+ request
+ )
+ |> json_response(200)
+
+ assert response["full_name"] == request["full_name"]
+ assert response["email"] == request["email"]
+ assert response["tags"] == request["tags"]
+ assert response["website"] == request["website"]
+ assert response["additional_comment"] == request["additional_comment"]
+ assert response["addresses"] == request["addresses"]
+ assert response["company"] == request["company"]
+ assert response["is_owner"] == request["is_owner"]
+ assert response["id"]
+
+ convert_date(response)
+ end)
+ |> Enum.reverse()
+
+ assert conn
+ |> get("/api/account/v1/user/public_tags")
+ |> doc(description: "Get list of requests to add a public tag")
+ |> json_response(200)
+ |> Enum.map(&convert_date/1) == final_list
+
+ %{"id" => id} = Enum.at(final_list, 0)
+
+ assert conn
+ |> delete("/api/account/v1/user/public_tags/#{id}", %{"remove_reason" => "reason"})
+ |> doc(description: "Delete public tags request")
+ |> json_response(200) == %{"message" => "OK"}
+
+ Enum.each(Enum.drop(final_list, 1), fn request ->
+ assert conn
+ |> delete("/api/account/v1/user/public_tags/#{request["id"]}", %{"remove_reason" => "reason"})
+ |> json_response(200) == %{"message" => "OK"}
+ end)
+
+ assert conn
+ |> get("/api/account/v1/user/public_tags")
+ |> json_response(200) == []
+ end
+
+ test "edit public tags request", %{conn: conn} do
+ public_tags_request = build(:public_tags_request)
+
+ post_public_tasg_request_response =
+ conn
+ |> post(
+ "/api/account/v1/user/public_tags",
+ public_tags_request
+ )
+ |> json_response(200)
+
+ assert post_public_tasg_request_response["full_name"] == public_tags_request["full_name"]
+ assert post_public_tasg_request_response["email"] == public_tags_request["email"]
+ assert post_public_tasg_request_response["tags"] == public_tags_request["tags"]
+ assert post_public_tasg_request_response["website"] == public_tags_request["website"]
+ assert post_public_tasg_request_response["additional_comment"] == public_tags_request["additional_comment"]
+ assert post_public_tasg_request_response["addresses"] == public_tags_request["addresses"]
+ assert post_public_tasg_request_response["company"] == public_tags_request["company"]
+ assert post_public_tasg_request_response["is_owner"] == public_tags_request["is_owner"]
+ assert post_public_tasg_request_response["id"]
+
+ assert conn
+ |> get("/api/account/v1/user/public_tags")
+ |> json_response(200)
+ |> Enum.map(&convert_date/1) ==
+ [post_public_tasg_request_response]
+ |> Enum.map(&convert_date/1)
+
+ new_public_tags_request = build(:public_tags_request)
+
+ put_public_tasg_request_response =
+ conn
+ |> put(
+ "/api/account/v1/user/public_tags/#{post_public_tasg_request_response["id"]}",
+ new_public_tags_request
+ )
+ |> doc(description: "Edit request to add a public tag")
+ |> json_response(200)
+
+ assert put_public_tasg_request_response["full_name"] == new_public_tags_request["full_name"]
+ assert put_public_tasg_request_response["email"] == new_public_tags_request["email"]
+ assert put_public_tasg_request_response["tags"] == new_public_tags_request["tags"]
+ assert put_public_tasg_request_response["website"] == new_public_tags_request["website"]
+ assert put_public_tasg_request_response["additional_comment"] == new_public_tags_request["additional_comment"]
+ assert put_public_tasg_request_response["addresses"] == new_public_tags_request["addresses"]
+ assert put_public_tasg_request_response["company"] == new_public_tags_request["company"]
+ assert put_public_tasg_request_response["is_owner"] == new_public_tags_request["is_owner"]
+ assert put_public_tasg_request_response["id"] == post_public_tasg_request_response["id"]
+
+ assert conn
+ |> get("/api/account/v1/user/public_tags")
+ |> json_response(200)
+ |> Enum.map(&convert_date/1) ==
+ [put_public_tasg_request_response]
+ |> Enum.map(&convert_date/1)
+ end
+ end
+
+ def convert_date(request) do
+ {:ok, time, _} = DateTime.from_iso8601(request["submission_date"])
+ %{request | "submission_date" => Calendar.strftime(time, "%b %d, %Y")}
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs
new file mode 100644
index 0000000000..89ff276f69
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/controllers/account/custom_abi_controller_test.exs
@@ -0,0 +1,186 @@
+defmodule BlockScoutWeb.Account.CustomABIControllerTest do
+ use BlockScoutWeb.ConnCase
+
+ alias BlockScoutWeb.Models.UserFromAuth
+
+ @custom_abi "[{\"type\":\"function\",\"outputs\":[{\"type\":\"string\",\"name\":\"\"}],\"name\":\"name\",\"inputs\":[],\"constant\":true}]"
+
+ setup %{conn: conn} do
+ auth = build(:auth)
+
+ {:ok, user} = UserFromAuth.find_or_create(auth)
+
+ {:ok, conn: Plug.Test.init_test_session(conn, current_user: user)}
+ end
+
+ describe "test custom ABI functionality" do
+ test "custom ABI page opens correctly", %{conn: conn} do
+ result_conn =
+ conn
+ |> get(custom_abi_path(conn, :index))
+
+ assert html_response(result_conn, 200) =~ "Create a Custom ABI to interact with contracts."
+ end
+
+ test "do not add custom ABI with wrong ABI", %{conn: conn} do
+ contract_address = insert(:address, contract_code: "0x0102")
+
+ custom_abi = %{
+ "name" => "1",
+ "address_hash" => to_string(contract_address),
+ "abi" => ""
+ }
+
+ result_conn =
+ conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi}))
+
+ assert html_response(result_conn, 200) =~ "Add Custom ABI"
+ assert html_response(result_conn, 200) =~ to_string(contract_address.hash)
+ assert html_response(result_conn, 200) =~ "Required"
+
+ result_conn_1 =
+ conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => Map.put(custom_abi, "abi", "123")}))
+
+ assert html_response(result_conn_1, 200) =~ "Add Custom ABI"
+ assert html_response(result_conn_1, 200) =~ to_string(contract_address.hash)
+ assert html_response(result_conn_1, 200) =~ "Invalid format"
+
+ result_conn_2 =
+ conn
+ |> get(custom_abi_path(conn, :index))
+
+ assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts."
+ refute html_response(result_conn_2, 200) =~ to_string(contract_address.hash)
+ end
+
+ test "add one custom abi and do not allow to create duplicates", %{conn: conn} do
+ contract_address = insert(:contract_address, contract_code: "0x0102")
+
+ custom_abi = %{
+ "name" => "1",
+ "address_hash" => to_string(contract_address),
+ "abi" => @custom_abi
+ }
+
+ result_conn =
+ conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi}))
+
+ assert redirected_to(result_conn) == custom_abi_path(conn, :index)
+
+ result_conn_2 = get(result_conn, custom_abi_path(conn, :index))
+ assert html_response(result_conn_2, 200) =~ to_string(contract_address.hash)
+ assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts."
+
+ result_conn_1 =
+ conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi}))
+
+ assert html_response(result_conn_1, 200) =~ "Add Custom ABI"
+ assert html_response(result_conn_1, 200) =~ to_string(contract_address.hash)
+ assert html_response(result_conn_1, 200) =~ "Custom ABI for this address has already been added before"
+ end
+
+ test "show error on address which is not smart contract", %{conn: conn} do
+ contract_address = insert(:address)
+
+ custom_abi = %{
+ "name" => "1",
+ "address_hash" => to_string(contract_address),
+ "abi" => @custom_abi
+ }
+
+ result_conn =
+ conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi}))
+
+ assert html_response(result_conn, 200) =~ "Add Custom ABI"
+ assert html_response(result_conn, 200) =~ to_string(contract_address.hash)
+ assert html_response(result_conn, 200) =~ "Address is not a smart contract"
+ end
+
+ test "user can add up to 15 custom ABIs", %{conn: conn} do
+ addresses =
+ Enum.map(1..15, fn _x ->
+ address = insert(:contract_address, contract_code: "0x0102")
+
+ custom_abi = %{
+ "name" => "1",
+ "address_hash" => to_string(address),
+ "abi" => @custom_abi
+ }
+
+ assert conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi}))
+ |> redirected_to() == custom_abi_path(conn, :index)
+
+ to_string(address.hash)
+ end)
+
+ assert abi_list =
+ conn
+ |> get(custom_abi_path(conn, :index))
+ |> html_response(200)
+
+ Enum.each(addresses, fn address -> assert abi_list =~ address end)
+
+ address = insert(:contract_address, contract_code: "0x0102")
+
+ custom_abi = %{
+ "name" => "1",
+ "address_hash" => to_string(address),
+ "abi" => @custom_abi
+ }
+
+ assert error_form =
+ conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi}))
+ |> html_response(200)
+
+ assert error_form =~ "Add Custom ABI"
+ assert error_form =~ "Max 15 ABIs per account"
+ assert error_form =~ to_string(address.hash)
+
+ assert abi_list_new =
+ conn
+ |> get(custom_abi_path(conn, :index))
+ |> html_response(200)
+
+ Enum.each(addresses, fn address -> assert abi_list_new =~ address end)
+
+ refute abi_list_new =~ to_string(address.hash)
+ assert abi_list_new =~ "You can create up to 15 Custom ABIs per account."
+ end
+
+ test "after adding custom ABI on address page appear Read/Write Contract tab", %{conn: conn} do
+ contract_address = insert(:contract_address, contract_code: "0x0102")
+
+ custom_abi = %{
+ "name" => "1",
+ "address_hash" => to_string(contract_address),
+ "abi" =>
+ "[{\"type\":\"function\",\"outputs\":[{\"type\":\"string\",\"name\":\"\"}],\"name\":\"name\",\"inputs\":[],\"constant\":true},{\"type\":\"function\",\"outputs\":[{\"type\":\"bool\",\"name\":\"success\"}],\"name\":\"approve\",\"inputs\":[{\"type\":\"address\",\"name\":\"_spender\"},{\"type\":\"uint256\",\"name\":\"_value\"}],\"constant\":false}]"
+ }
+
+ result_conn =
+ conn
+ |> post(custom_abi_path(conn, :create, %{"custom_abi" => custom_abi}))
+
+ assert redirected_to(result_conn) == custom_abi_path(conn, :index)
+
+ result_conn_2 = get(result_conn, custom_abi_path(conn, :index))
+ assert html_response(result_conn_2, 200) =~ to_string(contract_address.hash)
+ assert html_response(result_conn_2, 200) =~ "Create a Custom ABI to interact with contracts."
+
+ assert contract_page =
+ result_conn
+ |> get(address_contract_path(result_conn, :index, to_string(contract_address)))
+ |> html_response(200)
+
+ assert contract_page =~ "Write Contract"
+ assert contract_page =~ "Read Contract"
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/block_scout_web/models/user_from_auth_test.exs b/apps/block_scout_web/test/block_scout_web/models/user_from_auth_test.exs
new file mode 100644
index 0000000000..2d13f11600
--- /dev/null
+++ b/apps/block_scout_web/test/block_scout_web/models/user_from_auth_test.exs
@@ -0,0 +1,107 @@
+defmodule UserFromAuthTest do
+ use Explorer.DataCase
+
+ alias BlockScoutWeb.Models.UserFromAuth
+ alias Explorer.Account.Identity
+ alias Explorer.Account.Watchlist
+ alias Explorer.Repo
+ alias Ueberauth.Auth
+ alias Ueberauth.Auth.Info
+ alias Ueberauth.Strategy.Auth0
+
+ describe "get user info" do
+ test "from github" do
+ auth = %Auth{
+ info: %Info{
+ birthday: nil,
+ description: nil,
+ email: "john@blockscout.com",
+ first_name: nil,
+ image: "https://avatars.githubusercontent.com/u/666666=4",
+ last_name: nil,
+ location: nil,
+ name: "John Snow",
+ nickname: "johnnny",
+ phone: nil,
+ urls: %{profile: nil, website: nil}
+ },
+ provider: :auth0,
+ strategy: Auth0,
+ uid: "github|666666"
+ }
+
+ user_data = UserFromAuth.find_or_create(auth)
+
+ %{
+ id: identity_id,
+ email: "john@blockscout.com",
+ name: "John Snow",
+ uid: "github|666666"
+ } = Identity |> first |> Repo.account_repo().one()
+
+ %{
+ id: watchlist_id,
+ identity_id: ^identity_id,
+ name: "default"
+ } = Watchlist |> first |> Repo.account_repo().one()
+
+ assert {:ok,
+ %{
+ avatar: "https://avatars.githubusercontent.com/u/666666=4",
+ email: "john@blockscout.com",
+ id: ^identity_id,
+ name: "John Snow",
+ nickname: "johnnny",
+ uid: "github|666666",
+ watchlist_id: ^watchlist_id
+ }} = user_data
+ end
+
+ test "from google" do
+ auth = %Auth{
+ info: %Info{
+ birthday: nil,
+ description: nil,
+ email: "john@blockscout.com",
+ first_name: "John",
+ image: "https://lh3.googleusercontent.com/a/xxx666-yyy777=s99-c",
+ last_name: "Snow",
+ location: nil,
+ name: "John Snow",
+ nickname: "johnnny",
+ phone: nil,
+ urls: %{profile: nil, website: nil}
+ },
+ provider: :auth0,
+ strategy: Auth0,
+ uid: "google-oauth2|666666"
+ }
+
+ user_data = UserFromAuth.find_or_create(auth)
+
+ %{
+ id: identity_id,
+ email: "john@blockscout.com",
+ name: "John Snow",
+ uid: "google-oauth2|666666"
+ } = Identity |> first |> Repo.account_repo().one()
+
+ %{
+ id: watchlist_id,
+ identity_id: ^identity_id,
+ name: "default"
+ } = Watchlist |> first |> Repo.account_repo().one()
+
+ assert {:ok,
+ %{
+ avatar: "https://lh3.googleusercontent.com/a/xxx666-yyy777=s99-c",
+ email: "john@blockscout.com",
+ id: ^identity_id,
+ name: "John Snow",
+ nickname: "johnnny",
+ uid: "google-oauth2|666666",
+ watchlist_id: ^watchlist_id
+ }} = user_data
+ end
+ end
+end
diff --git a/apps/block_scout_web/test/support/conn_case.ex b/apps/block_scout_web/test/support/conn_case.ex
index f9ae650bf9..5ae6216f0c 100644
--- a/apps/block_scout_web/test/support/conn_case.ex
+++ b/apps/block_scout_web/test/support/conn_case.ex
@@ -22,6 +22,7 @@ defmodule BlockScoutWeb.ConnCase do
import Phoenix.ConnTest
import BlockScoutWeb.Router.Helpers
import BlockScoutWeb.WebRouter.Helpers, except: [static_path: 2]
+ import Bureaucrat.Helpers
# The default endpoint for testing
@endpoint BlockScoutWeb.Endpoint
@@ -36,9 +37,11 @@ defmodule BlockScoutWeb.ConnCase do
@dialyzer {:nowarn_function, __ex_unit_setup_0: 1}
setup tags do
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo)
+ :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()})
+ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()})
end
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
diff --git a/apps/block_scout_web/test/test_helper.exs b/apps/block_scout_web/test/test_helper.exs
index 9944181688..a99ba9a234 100644
--- a/apps/block_scout_web/test/test_helper.exs
+++ b/apps/block_scout_web/test/test_helper.exs
@@ -11,12 +11,21 @@ Application.put_env(:wallaby, :base_url, BlockScoutWeb.Endpoint.url())
{:ok, _} = Application.ensure_all_started(:ex_machina)
-ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter])
+Bureaucrat.start(
+ writer: Bureaucrat.ApiBlueprintWriter,
+ default_path: "API blueprint.md",
+ env_var: "DOC"
+)
+
+# Bureaucrat.start()
+
+ExUnit.configure(formatters: [JUnitFormatter, ExUnit.CLIFormatter, Bureaucrat.Formatter])
ExUnit.start()
Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :manual)
+Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :manual)
Absinthe.Test.prime(BlockScoutWeb.Schema)
diff --git a/apps/explorer/config/config.exs b/apps/explorer/config/config.exs
index cc58587a54..c58d3bfd37 100644
--- a/apps/explorer/config/config.exs
+++ b/apps/explorer/config/config.exs
@@ -7,7 +7,7 @@ import Config
# General application configuration
config :explorer,
- ecto_repos: [Explorer.Repo],
+ ecto_repos: [Explorer.Repo, Explorer.Repo.Account],
token_functions_reader_max_retries: 3
config :explorer, Explorer.Counters.AverageBlockTime,
diff --git a/apps/explorer/config/dev.exs b/apps/explorer/config/dev.exs
index 7d7bd024a7..1ccfb34b6d 100644
--- a/apps/explorer/config/dev.exs
+++ b/apps/explorer/config/dev.exs
@@ -6,6 +6,9 @@ config :explorer, Explorer.Repo, timeout: :timer.seconds(80)
# Configure API database
config :explorer, Explorer.Repo.Replica1, timeout: :timer.seconds(80)
+# Configure Account database
+config :explorer, Explorer.Repo.Account, timeout: :timer.seconds(80)
+
config :explorer, Explorer.Tracer, env: "dev", disabled?: true
config :logger, :explorer,
diff --git a/apps/explorer/config/prod.exs b/apps/explorer/config/prod.exs
index 8b42271e33..2d389e59d6 100644
--- a/apps/explorer/config/prod.exs
+++ b/apps/explorer/config/prod.exs
@@ -10,6 +10,11 @@ config :explorer, Explorer.Repo.Replica1,
prepare: :unnamed,
timeout: :timer.seconds(60)
+# Configures Account database
+config :explorer, Explorer.Repo.Account,
+ prepare: :unnamed,
+ timeout: :timer.seconds(60)
+
config :explorer, Explorer.Tracer, env: "production", disabled?: true
config :logger, :explorer,
diff --git a/apps/explorer/config/test.exs b/apps/explorer/config/test.exs
index 3c7fc40fec..1b3b160791 100644
--- a/apps/explorer/config/test.exs
+++ b/apps/explorer/config/test.exs
@@ -23,6 +23,16 @@ config :explorer, Explorer.Repo.Replica1,
timeout: :timer.seconds(60),
queue_target: 1000
+# Configure API database
+config :explorer, Explorer.Repo.Account,
+ database: "explorer_test_account",
+ 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
+
config :logger, :explorer,
level: :warn,
path: Path.absname("logs/test/explorer.log")
diff --git a/apps/explorer/lib/encrypt.ex b/apps/explorer/lib/encrypt.ex
new file mode 100644
index 0000000000..e105feb11d
--- /dev/null
+++ b/apps/explorer/lib/encrypt.ex
@@ -0,0 +1,116 @@
+defmodule Mix.Tasks.Encrypt do
+ @moduledoc "The encrypt mix task: `mix help encrypt`"
+ use Mix.Task
+
+ alias Ecto.Changeset
+
+ alias Explorer.Account.{
+ CustomABI,
+ Identity,
+ PublicTagsRequest,
+ TagAddress,
+ TagTransaction,
+ WatchlistAddress,
+ WatchlistNotification
+ }
+
+ alias Explorer.Repo.Account
+ alias Mix.Task
+
+ @shortdoc "Encrypt"
+ def run(_) do
+ Task.run("app.start")
+
+ Identity
+ |> Account.all()
+ |> Enum.each(fn element ->
+ element
+ |> Changeset.change(%{
+ encrypted_uid: element.uid,
+ encrypted_email: element.email,
+ encrypted_name: element.name,
+ encrypted_nickname: element.nickname,
+ encrypted_avatar: element.avatar,
+ uid_hash: element.uid
+ })
+ |> Account.update!()
+ end)
+
+ TagAddress
+ |> Account.all()
+ |> Enum.each(fn element ->
+ element
+ |> Changeset.change(%{
+ encrypted_name: element.name,
+ encrypted_address_hash: element.address_hash,
+ address_hash_hash: element.address_hash |> to_string() |> String.downcase()
+ })
+ |> Account.update!()
+ end)
+
+ TagTransaction
+ |> Account.all()
+ |> Enum.each(fn element ->
+ element
+ |> Changeset.change(%{
+ encrypted_name: element.name,
+ encrypted_tx_hash: element.tx_hash,
+ tx_hash_hash: element.tx_hash |> to_string() |> String.downcase()
+ })
+ |> Account.update!()
+ end)
+
+ CustomABI
+ |> Account.all()
+ |> Enum.each(fn element ->
+ element
+ |> Changeset.change(%{
+ encrypted_name: element.name,
+ encrypted_address_hash: element.address_hash,
+ address_hash_hash: element.address_hash |> to_string() |> String.downcase()
+ })
+ |> Account.update!()
+ end)
+
+ WatchlistAddress
+ |> Account.all()
+ |> Enum.each(fn element ->
+ element
+ |> Changeset.change(%{
+ encrypted_name: element.name,
+ encrypted_address_hash: element.address_hash,
+ address_hash_hash: element.address_hash |> to_string() |> String.downcase()
+ })
+ |> Account.update!()
+ end)
+
+ WatchlistNotification
+ |> Account.all()
+ |> Enum.each(fn element ->
+ element
+ |> Changeset.change(%{
+ encrypted_name: element.name,
+ encrypted_from_address_hash: element.from_address_hash,
+ encrypted_to_address_hash: element.to_address_hash,
+ encrypted_transaction_hash: element.transaction_hash,
+ encrypted_subject: element.subject,
+ from_address_hash_hash: element.from_address_hash |> to_string() |> String.downcase(),
+ to_address_hash_hash: element.to_address_hash |> to_string() |> String.downcase(),
+ transaction_hash_hash: element.transaction_hash |> to_string() |> String.downcase(),
+ subject_hash: element.subject
+ })
+ |> Account.update!()
+ end)
+
+ PublicTagsRequest
+ |> Account.all()
+ |> Enum.each(fn element ->
+ element
+ |> Changeset.change(%{
+ encrypted_full_name: element.full_name,
+ encrypted_email: element.email
+ })
+ |> Account.update!()
+ end)
+ end
+end
diff --git a/apps/explorer/lib/explorer/account.ex b/apps/explorer/lib/explorer/account.ex
new file mode 100644
index 0000000000..5f2155b1f4
--- /dev/null
+++ b/apps/explorer/lib/explorer/account.ex
@@ -0,0 +1,9 @@
+defmodule Explorer.Account do
+ @moduledoc """
+ Context for Account module.
+ """
+
+ def enabled? do
+ Application.get_env(:explorer, __MODULE__)[:enabled]
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/api/key.ex b/apps/explorer/lib/explorer/account/api/key.ex
new file mode 100644
index 0000000000..ab80d41ec2
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/api/key.ex
@@ -0,0 +1,129 @@
+defmodule Explorer.Account.Api.Key do
+ @moduledoc """
+ Module is responsible for schema for API keys, keys is used to track number of requests to the API endpoints
+ """
+ use Explorer.Schema
+
+ alias Explorer.Account.Identity
+ alias Ecto.{Changeset, UUID}
+ alias Explorer.Repo
+
+ import Ecto.Changeset
+
+ @max_key_per_account 3
+
+ @primary_key false
+ schema "account_api_keys" do
+ field(:name, :string)
+ field(:value, UUID, primary_key: true)
+ belongs_to(:identity, Identity)
+
+ timestamps()
+ end
+
+ @attrs ~w(value name identity_id)a
+
+ def changeset do
+ %__MODULE__{}
+ |> cast(%{}, @attrs)
+ end
+
+ def changeset(%__MODULE__{} = api_key, attrs \\ %{}) do
+ api_key
+ |> cast(attrs, @attrs)
+ |> validate_required(@attrs, message: "Required")
+ |> validate_length(:name, min: 1, max: 255)
+ |> unique_constraint(:value, message: "API key already exists")
+ |> foreign_key_constraint(:identity_id, message: "User not found")
+ |> api_key_count_constraint()
+ end
+
+ def create(attrs) do
+ %__MODULE__{}
+ |> changeset(Map.put(attrs, :value, generate_api_key()))
+ |> Repo.account_repo().insert()
+ end
+
+ def api_key_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = api_key) do
+ if identity_id
+ |> api_keys_by_identity_id_query()
+ |> limit(@max_key_per_account)
+ |> Repo.account_repo().aggregate(:count, :value) >= @max_key_per_account do
+ api_key
+ |> add_error(:name, "Max #{@max_key_per_account} keys per account")
+ else
+ api_key
+ end
+ end
+
+ def api_key_count_constraint(changeset), do: changeset
+
+ def generate_api_key do
+ UUID.generate()
+ end
+
+ def api_keys_by_identity_id_query(id) when not is_nil(id) do
+ __MODULE__
+ |> where([api_key], api_key.identity_id == ^id)
+ |> order_by([api_key], desc: api_key.inserted_at)
+ end
+
+ def api_keys_by_identity_id_query(_), do: nil
+
+ def api_key_by_value_and_identity_id_query(api_key_value, identity_id)
+ when not is_nil(api_key_value) and not is_nil(identity_id) do
+ __MODULE__
+ |> where([api_key], api_key.identity_id == ^identity_id and api_key.value == ^api_key_value)
+ end
+
+ def api_key_by_value_and_identity_id_query(_, _), do: nil
+
+ def get_api_key_by_value_and_identity_id(value, identity_id) when not is_nil(value) and not is_nil(identity_id) do
+ value
+ |> api_key_by_value_and_identity_id_query(identity_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_api_key_by_value_and_identity_id(_, _), do: nil
+
+ def update(%{value: api_key_value, identity_id: identity_id} = attrs) do
+ with api_key <- get_api_key_by_value_and_identity_id(api_key_value, identity_id),
+ false <- is_nil(api_key) do
+ api_key |> changeset(attrs) |> Repo.account_repo().update()
+ else
+ true ->
+ {:error, %{reason: :item_not_found}}
+ end
+ end
+
+ def delete(api_key_value, identity_id) when not is_nil(api_key_value) and not is_nil(identity_id) do
+ api_key_value
+ |> api_key_by_value_and_identity_id_query(identity_id)
+ |> Repo.account_repo().delete_all()
+ end
+
+ def delete(_, _), do: nil
+
+ def get_api_keys_by_identity_id(id) when not is_nil(id) do
+ id
+ |> api_keys_by_identity_id_query()
+ |> Repo.account_repo().all()
+ end
+
+ def get_api_keys_by_identity_id(_), do: nil
+
+ def api_key_with_plan_by_value(api_key_value) when not is_nil(api_key_value) do
+ if match?({:ok, _casted_api_key}, UUID.cast(api_key_value)) do
+ __MODULE__
+ |> where([api_key], api_key.value == ^api_key_value)
+ |> Repo.account_repo().one()
+ |> Repo.account_repo().preload(identity: :plan)
+ else
+ nil
+ end
+ end
+
+ def api_key_with_plan_by_value(_), do: nil
+
+ def get_max_api_keys_count, do: @max_key_per_account
+end
diff --git a/apps/explorer/lib/explorer/account/api/plan.ex b/apps/explorer/lib/explorer/account/api/plan.ex
new file mode 100644
index 0000000000..b89b33bc9c
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/api/plan.ex
@@ -0,0 +1,13 @@
+defmodule Explorer.Account.Api.Plan do
+ @moduledoc """
+ Module is responsible for schema for API plans, each plan contains its name and maximum number of requests per second
+ """
+ use Explorer.Schema
+
+ schema "account_api_plans" do
+ field(:name, :string)
+ field(:max_req_per_second, :integer)
+
+ timestamps()
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/custom_abi.ex b/apps/explorer/lib/explorer/account/custom_abi.ex
new file mode 100644
index 0000000000..94f62f482a
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/custom_abi.ex
@@ -0,0 +1,226 @@
+defmodule Explorer.Account.CustomABI do
+ @moduledoc """
+ Module is responsible for schema for API keys, keys is used to track number of requests to the API endpoints
+ """
+ use Explorer.Schema
+
+ alias ABI.FunctionSelector
+ alias Ecto.Changeset
+ alias Explorer.Account.Identity
+ alias Explorer.{Chain, Repo}
+
+ import Explorer.Chain, only: [hash_to_lower_case_string: 1]
+ import Ecto.Changeset
+
+ @max_abis_per_account 15
+
+ schema "account_custom_abis" do
+ field(:abi, {:array, :map})
+ field(:given_abi, :string, virtual: true)
+ field(:abi_validating_error, :string, virtual: true)
+ field(:address_hash_hash, Cloak.Ecto.SHA256)
+ field(:address_hash, Explorer.Encrypted.AddressHash, null: false)
+ field(:name, Explorer.Encrypted.Binary)
+
+ belongs_to(:identity, Identity)
+
+ timestamps()
+ end
+
+ @attrs ~w(name abi identity_id address_hash)a
+
+ def changeset(%__MODULE__{} = custom_abi \\ %__MODULE__{}, attrs \\ %{}) do
+ custom_abi
+ |> cast(check_is_abi_valid?(attrs), @attrs ++ [:id, :given_abi, :abi_validating_error])
+ |> validate_required(@attrs, message: "Required")
+ |> validate_custom_abi()
+ |> check_smart_contract_address()
+ |> foreign_key_constraint(:identity_id, message: "User not found")
+ |> put_hashed_fields()
+ |> unique_constraint([:identity_id, :address_hash_hash],
+ message: "Custom ABI for this address has already been added before"
+ )
+ |> custom_abi_count_constraint()
+ end
+
+ def changeset_without_constraints(%__MODULE__{} = custom_abi \\ %__MODULE__{}, attrs \\ %{}) do
+ custom_abi
+ |> cast(attrs, [:id | @attrs])
+ |> validate_required(@attrs, message: "Required")
+ end
+
+ defp put_hashed_fields(changeset) do
+ changeset
+ |> put_change(:address_hash_hash, hash_to_lower_case_string(get_field(changeset, :address_hash)))
+ end
+
+ defp check_smart_contract_address(%Changeset{changes: %{address_hash: address_hash}} = custom_abi) do
+ check_smart_contract_address_inner(custom_abi, address_hash)
+ end
+
+ defp check_smart_contract_address(%Changeset{data: %{address_hash: address_hash}} = custom_abi) do
+ check_smart_contract_address_inner(custom_abi, address_hash)
+ end
+
+ defp check_smart_contract_address(custom_abi), do: custom_abi
+
+ defp check_smart_contract_address_inner(changeset, address_hash) do
+ if Chain.is_address_hash_is_smart_contract?(address_hash) do
+ changeset
+ else
+ add_error(changeset, :address_hash, "Address is not a smart contract")
+ end
+ end
+
+ defp validate_custom_abi(%Changeset{changes: %{given_abi: given_abi, abi_validating_error: error}} = custom_abi) do
+ custom_abi
+ |> add_error(:abi, error)
+ |> force_change(:abi, given_abi)
+ end
+
+ defp validate_custom_abi(custom_abi), do: custom_abi
+
+ defp check_is_abi_valid?(%{abi: abi} = custom_abi) when is_binary(abi) do
+ with {:ok, decoded} <- Jason.decode(abi),
+ true <- is_list(decoded) do
+ custom_abi
+ |> Map.put(:abi, decoded)
+ |> check_is_abi_valid?(abi)
+ else
+ _ ->
+ custom_abi
+ |> Map.put(:abi, "")
+ |> Map.put(:given_abi, abi)
+ |> Map.put(:abi_validating_error, "Invalid format")
+ end
+ end
+
+ defp check_is_abi_valid?(custom_abi, given_abi \\ nil)
+
+ defp check_is_abi_valid?(%{abi: abi} = custom_abi, given_abi) when is_list(abi) do
+ with true <- length(abi) > 0,
+ filtered_abi <- filter_abi(abi),
+ true <- Enum.count(filtered_abi) > 0 do
+ Map.put(custom_abi, :abi, filtered_abi)
+ else
+ _ ->
+ custom_abi
+ |> Map.put(:abi, "")
+ |> (&if(is_nil(given_abi),
+ do: Map.put(&1, :given_abi, Jason.encode!(abi)),
+ else: Map.put(&1, :given_abi, given_abi)
+ )).()
+ |> Map.put(:abi_validating_error, "ABI must contain functions")
+ end
+ end
+
+ defp check_is_abi_valid?(custom_abi, _), do: custom_abi
+
+ defp filter_abi(abi_list) when is_list(abi_list) do
+ Enum.filter(abi_list, &is_abi_function(&1))
+ end
+
+ defp is_abi_function(abi_item) when is_map(abi_item) do
+ case ABI.parse_specification([abi_item], include_events?: false) do
+ [%FunctionSelector{type: :constructor}] ->
+ false
+
+ [_] ->
+ true
+
+ _ ->
+ false
+ end
+ end
+
+ def custom_abi_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = custom_abi) do
+ if identity_id
+ |> custom_abis_by_identity_id_query()
+ |> limit(@max_abis_per_account)
+ |> Repo.account_repo().aggregate(:count, :id) >= @max_abis_per_account do
+ add_error(custom_abi, :name, "Max #{@max_abis_per_account} ABIs per account")
+ else
+ custom_abi
+ end
+ end
+
+ def custom_abi_count_constraint(%Changeset{} = custom_abi), do: custom_abi
+
+ def create(attrs) do
+ %__MODULE__{}
+ |> changeset(attrs)
+ |> Repo.account_repo().insert()
+ end
+
+ def custom_abis_by_identity_id_query(id) when not is_nil(id) do
+ __MODULE__
+ |> where([abi], abi.identity_id == ^id)
+ |> order_by([abi], desc: abi.id)
+ end
+
+ def custom_abis_by_identity_id_query(_), do: nil
+
+ def custom_abi_by_id_and_identity_id_query(id, identity_id)
+ when not is_nil(id) and not is_nil(identity_id) do
+ __MODULE__
+ |> where([custom_abi], custom_abi.identity_id == ^identity_id and custom_abi.id == ^id)
+ end
+
+ def custom_abi_by_id_and_identity_id_query(_, _), do: nil
+
+ def custom_abi_by_identity_id_and_address_hash_query(address_hash, identity_id)
+ when not is_nil(identity_id) and not is_nil(address_hash) do
+ __MODULE__
+ |> where([custom_abi], custom_abi.identity_id == ^identity_id and custom_abi.address_hash_hash == ^address_hash)
+ end
+
+ def custom_abi_by_identity_id_and_address_hash_query(_, _), do: nil
+
+ def get_custom_abi_by_identity_id_and_address_hash(address_hash, identity_id)
+ when not is_nil(identity_id) and not is_nil(address_hash) do
+ address_hash
+ |> hash_to_lower_case_string()
+ |> custom_abi_by_identity_id_and_address_hash_query(identity_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_custom_abi_by_identity_id_and_address_hash(_, _), do: nil
+
+ def get_custom_abi_by_id_and_identity_id(id, identity_id) when not is_nil(id) and not is_nil(identity_id) do
+ id
+ |> custom_abi_by_id_and_identity_id_query(identity_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_custom_abi_by_id_and_identity_id(_, _), do: nil
+
+ def get_custom_abis_by_identity_id(id) when not is_nil(id) do
+ id
+ |> custom_abis_by_identity_id_query()
+ |> Repo.account_repo().all()
+ end
+
+ def get_custom_abis_by_identity_id(_), do: nil
+
+ def delete(id, identity_id) when not is_nil(id) and not is_nil(identity_id) do
+ id
+ |> custom_abi_by_id_and_identity_id_query(identity_id)
+ |> Repo.account_repo().delete_all()
+ end
+
+ def delete(_, _), do: nil
+
+ def update(%{id: id, identity_id: identity_id} = attrs) do
+ with custom_abi <- get_custom_abi_by_id_and_identity_id(id, identity_id),
+ false <- is_nil(custom_abi) do
+ custom_abi
+ |> changeset(attrs)
+ |> Repo.account_repo().update()
+ else
+ true ->
+ {:error, %{reason: :item_not_found}}
+ end
+ end
+
+ def get_max_custom_abis_count, do: @max_abis_per_account
+end
diff --git a/apps/explorer/lib/explorer/account/identity.ex b/apps/explorer/lib/explorer/account/identity.ex
new file mode 100644
index 0000000000..d0766d7d35
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/identity.ex
@@ -0,0 +1,41 @@
+defmodule Explorer.Account.Identity do
+ @moduledoc """
+ Identity of user fetched via Oauth
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+
+ alias Explorer.Account.Api.Plan
+ alias Explorer.Account.{TagAddress, Watchlist}
+
+ schema "account_identities" do
+ field(:uid_hash, Cloak.Ecto.SHA256)
+ field(:uid, Explorer.Encrypted.Binary)
+ field(:email, Explorer.Encrypted.Binary)
+ field(:name, Explorer.Encrypted.Binary)
+ field(:nickname, Explorer.Encrypted.Binary)
+ field(:avatar, Explorer.Encrypted.Binary)
+
+ has_many(:tag_addresses, TagAddress)
+ has_many(:watchlists, Watchlist)
+
+ belongs_to(:plan, Plan)
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(identity, attrs) do
+ identity
+ |> cast(attrs, [:uid, :email, :name, :nickname, :avatar])
+ |> validate_required([:uid, :email, :name])
+ |> put_hashed_fields()
+ end
+
+ defp put_hashed_fields(changeset) do
+ changeset
+ |> put_change(:uid_hash, get_field(changeset, :uid))
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/notifier/email.ex b/apps/explorer/lib/explorer/account/notifier/email.ex
new file mode 100644
index 0000000000..35555e3169
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/notifier/email.ex
@@ -0,0 +1,155 @@
+defmodule Explorer.Account.Notifier.Email do
+ @moduledoc """
+ Composing an email to sendgrid
+ """
+
+ require Logger
+
+ alias BlockScoutWeb.WebRouter.Helpers
+ alias Explorer.Account.{Identity, Watchlist, WatchlistAddress, WatchlistNotification}
+ alias Explorer.Repo
+
+ import Bamboo.{Email, SendGridHelper}
+
+ def compose(notification, %{notify_email: notify}) when notify do
+ notification = preload(notification)
+
+ email = compose_email(notification)
+ Logger.debug("--- composed email", fetcher: :account)
+ Logger.debug(email, fetcher: :account)
+ email
+ end
+
+ def compose(_, _), do: nil
+
+ defp compose_email(notification) do
+ email = new_email(from: sender(), to: email(notification))
+
+ email
+ |> with_template(template())
+ |> add_dynamic_field("username", username(notification))
+ |> add_dynamic_field("address_hash", address_hash_string(notification))
+ |> add_dynamic_field("address_name", notification.watchlist_address.name)
+ |> add_dynamic_field("transaction_hash", hash_string(notification.transaction_hash))
+ |> add_dynamic_field("from_address_hash", hash_string(notification.from_address_hash))
+ |> add_dynamic_field("to_address_hash", hash_string(notification.to_address_hash))
+ |> add_dynamic_field("block_number", notification.block_number)
+ |> add_dynamic_field("amount", amount(notification))
+ |> add_dynamic_field("name", notification.name)
+ |> add_dynamic_field("tx_fee", notification.tx_fee)
+ |> add_dynamic_field("direction", direction(notification))
+ |> add_dynamic_field("method", notification.method)
+ |> add_dynamic_field("transaction_url", transaction_url(notification))
+ |> add_dynamic_field("address_url", address_url(notification.watchlist_address.address_hash))
+ |> add_dynamic_field("from_url", address_url(notification.from_address_hash))
+ |> add_dynamic_field("to_url", address_url(notification.to_address_hash))
+ |> add_dynamic_field("block_url", block_url(notification))
+ end
+
+ defp amount(%WatchlistNotification{amount: amount, subject: subject, type: type}) do
+ case type do
+ "COIN" ->
+ amount
+
+ "ERC-20" ->
+ amount
+
+ "ERC-721" ->
+ "Token ID: " <> subject <> " of "
+
+ "ERC-1155" ->
+ "Token ID: " <> subject <> " of "
+ end
+ end
+
+ defp email(%WatchlistNotification{
+ watchlist_address: %WatchlistAddress{
+ watchlist: %Watchlist{
+ identity: %Identity{
+ email: email
+ }
+ }
+ }
+ }),
+ do: email
+
+ defp username(%WatchlistNotification{
+ watchlist_address: %WatchlistAddress{
+ watchlist: %Watchlist{
+ identity: %Identity{
+ name: name
+ }
+ }
+ }
+ }),
+ do: name
+
+ defp address_hash_string(%WatchlistNotification{
+ watchlist_address: %WatchlistAddress{address_hash: address_hash}
+ }),
+ do: hash_string(address_hash)
+
+ defp hash_string(hash) do
+ "0x" <> Base.encode16(hash.bytes, case: :lower)
+ end
+
+ defp direction(notification) do
+ affect(notification) <> " " <> place(notification)
+ end
+
+ defp place(%WatchlistNotification{direction: direction}) do
+ case direction do
+ "incoming" -> "at"
+ "outgoing" -> "from"
+ _ -> "unknown"
+ end
+ end
+
+ defp affect(%WatchlistNotification{direction: direction}) do
+ case direction do
+ "incoming" -> "received"
+ "outgoing" -> "sent"
+ _ -> "unknown"
+ end
+ end
+
+ defp preload(notification) do
+ Repo.account_repo().preload(notification, watchlist_address: [watchlist: :identity])
+ end
+
+ defp address_url(address_hash) do
+ Helpers.address_url(uri(), :show, address_hash)
+ end
+
+ defp block_url(notification) do
+ URI.to_string(uri()) <> "block/" <> Integer.to_string(notification.block_number)
+ end
+
+ defp transaction_url(notification) do
+ Helpers.transaction_url(uri(), :show, notification.transaction_hash)
+ end
+
+ defp uri do
+ %URI{scheme: "https", host: host(), path: path()}
+ end
+
+ defp host do
+ if System.get_env("MIX_ENV") == "prod" do
+ "blockscout.com"
+ else
+ Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host]
+ end
+ end
+
+ defp path do
+ Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path]
+ end
+
+ defp sender do
+ Application.get_env(:explorer, Explorer.Account)[:sendgrid][:sender]
+ end
+
+ defp template do
+ Application.get_env(:explorer, Explorer.Account)[:sendgrid][:template]
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex
new file mode 100644
index 0000000000..f873864ff1
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/notifier/forbidden_address.ex
@@ -0,0 +1,67 @@
+defmodule Explorer.Account.Notifier.ForbiddenAddress do
+ @moduledoc """
+ Check if address is forbidden to notify
+ """
+
+ @blacklist [
+ "0x0000000000000000000000000000000000000000",
+ "0x000000000000000000000000000000000000dEaD"
+ ]
+
+ alias Explorer.Chain.Token
+ alias Explorer.Repo
+
+ import Ecto.Query, only: [from: 2]
+ import Explorer.Chain, only: [string_to_address_hash: 1]
+
+ def check(address_string) when is_bitstring(address_string) do
+ case format_address(address_string) do
+ {:error, message} ->
+ {:error, message}
+
+ address_hash ->
+ check(address_hash)
+ end
+ end
+
+ def check(%Explorer.Chain.Hash{} = address_hash) do
+ cond do
+ address_hash in blacklist() ->
+ {:error, "This address is blacklisted"}
+
+ is_contract(address_hash) ->
+ {:error, "This address isn't personal"}
+
+ address_hash ->
+ {:ok, address_hash}
+ end
+ end
+
+ defp is_contract(%Explorer.Chain.Hash{} = address_hash) do
+ query =
+ from(
+ token in Token,
+ where: token.contract_address_hash == ^address_hash
+ )
+
+ contract_addresses = Repo.all(query)
+ List.first(contract_addresses)
+ end
+
+ defp format_address(address_hash_string) do
+ case string_to_address_hash(address_hash_string) do
+ {:ok, address_hash} ->
+ address_hash
+
+ :error ->
+ {:error, "Address is invalid"}
+ end
+ end
+
+ defp blacklist do
+ Enum.map(
+ @blacklist,
+ &format_address(&1)
+ )
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/notifier/notify.ex b/apps/explorer/lib/explorer/account/notifier/notify.ex
new file mode 100644
index 0000000000..b64b4bd47a
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/notifier/notify.ex
@@ -0,0 +1,153 @@
+defmodule Explorer.Account.Notifier.Notify do
+ @moduledoc """
+ Composing notification, store and send it to email
+ """
+
+ alias Explorer.Account.Notifier.{Email, ForbiddenAddress, Summary}
+ alias Explorer.Account.{WatchlistAddress, WatchlistNotification}
+ alias Explorer.Chain.{TokenTransfer, Transaction}
+ alias Explorer.{Mailer, Repo}
+
+ require Logger
+
+ import Ecto.Query, only: [from: 2]
+ import Explorer.Chain, only: [hash_to_lower_case_string: 1]
+
+ def call(nil), do: nil
+ def call([]), do: nil
+
+ def call(transactions) when is_list(transactions) do
+ Enum.map(transactions, fn transaction -> process(transaction) end)
+ end
+
+ defp process(%TokenTransfer{} = transfer) do
+ Logger.debug(transfer, fetcher: :account)
+
+ transfer
+ |> Summary.process()
+ |> Enum.map(fn summary -> notify_watchlists(summary) end)
+ end
+
+ defp process(%Transaction{} = transaction) do
+ Logger.debug(transaction, fetcher: :account)
+
+ transaction
+ |> Summary.process()
+ |> Enum.map(fn summary -> notify_watchlists(summary) end)
+ end
+
+ defp process(_), do: nil
+
+ defp notify_watchlists(%Summary{from_address_hash: nil}), do: nil
+ defp notify_watchlists(%Summary{to_address_hash: nil}), do: nil
+
+ defp notify_watchlists(%Summary{} = summary) do
+ incoming_addresses = find_watchlists_addresses(summary.to_address_hash)
+ outgoing_addresses = find_watchlists_addresses(summary.from_address_hash)
+
+ Logger.debug("--- filled summary", fetcher: :account)
+ Logger.debug(summary, fetcher: :account)
+
+ Enum.each(incoming_addresses, fn address -> notify_watchlist(address, summary, :incoming) end)
+ Enum.each(outgoing_addresses, fn address -> notify_watchlist(address, summary, :outgoing) end)
+ end
+
+ defp notify_watchlists(nil), do: nil
+
+ defp notify_watchlist(%WatchlistAddress{} = address, summary, direction) do
+ case ForbiddenAddress.check(address.address_hash) do
+ {:ok, _address_hash} ->
+ with %WatchlistNotification{} = notification <-
+ build_watchlist_notification(
+ address,
+ summary,
+ direction
+ ) do
+ notification
+ |> query_notification(address)
+ |> Repo.account_repo().all()
+ |> case do
+ [] -> save_and_send_notification(notification, address)
+ _ -> :ok
+ end
+ end
+
+ {:error, _message} ->
+ nil
+ end
+ end
+
+ defp query_notification(notification, watchlist_address) do
+ from(wn in WatchlistNotification,
+ where:
+ wn.watchlist_address_id == ^watchlist_address.id and
+ wn.from_address_hash_hash == ^notification.from_address_hash and
+ wn.to_address_hash_hash == ^notification.to_address_hash and
+ wn.transaction_hash_hash == ^notification.transaction_hash and
+ wn.block_number == ^notification.block_number and
+ wn.direction == ^notification.direction and
+ wn.subject_hash == ^notification.subject and
+ wn.amount == ^notification.amount
+ )
+ end
+
+ defp save_and_send_notification(%WatchlistNotification{} = notification, %WatchlistAddress{} = address) do
+ Repo.account_repo().insert(notification)
+
+ email = Email.compose(notification, address)
+
+ case Mailer.deliver_now(email, response: true) do
+ {:ok, _email, response} ->
+ Logger.info("--- email delivery response: SUCCESS", fetcher: :account)
+ Logger.info(response, fetcher: :account)
+
+ {:error, error} ->
+ Logger.info("--- email delivery response: FAILED", fetcher: :account)
+ Logger.info(error, fetcher: :account)
+ end
+ end
+
+ @doc """
+ direction = :incoming || :outgoing
+ """
+ def build_watchlist_notification(%Explorer.Account.WatchlistAddress{} = address, summary, direction) do
+ if is_watched(address, summary, direction) do
+ %WatchlistNotification{
+ watchlist_address_id: address.id,
+ transaction_hash: summary.transaction_hash,
+ from_address_hash: summary.from_address_hash,
+ to_address_hash: summary.to_address_hash,
+ direction: to_string(direction),
+ method: summary.method,
+ block_number: summary.block_number,
+ amount: summary.amount,
+ subject: summary.subject,
+ tx_fee: summary.tx_fee,
+ name: summary.name,
+ type: summary.type,
+ from_address_hash_hash: hash_to_lower_case_string(summary.from_address_hash),
+ to_address_hash_hash: hash_to_lower_case_string(summary.to_address_hash),
+ transaction_hash_hash: hash_to_lower_case_string(summary.transaction_hash),
+ subject_hash: summary.subject
+ }
+ end
+ end
+
+ defp is_watched(%WatchlistAddress{} = address, %{type: type}, direction) do
+ case {type, direction} do
+ {"COIN", :incoming} -> address.watch_coin_input
+ {"COIN", :outgoing} -> address.watch_coin_output
+ {"ERC-20", :incoming} -> address.watch_erc_20_input
+ {"ERC-20", :outgoing} -> address.watch_erc_20_output
+ {"ERC-721", :incoming} -> address.watch_erc_721_input
+ {"ERC-721", :outgoing} -> address.watch_erc_721_output
+ {"ERC-1155", :incoming} -> address.watch_erc_1155_input
+ {"ERC-1155", :outgoing} -> address.watch_erc_1155_output
+ end
+ end
+
+ defp find_watchlists_addresses(%Explorer.Chain.Hash{} = address_hash) do
+ query = from(wa in WatchlistAddress, where: wa.address_hash_hash == ^address_hash)
+ Repo.account_repo().all(query)
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/notifier/summary.ex b/apps/explorer/lib/explorer/account/notifier/summary.ex
new file mode 100644
index 0000000000..a327074492
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/notifier/summary.ex
@@ -0,0 +1,226 @@
+defmodule Explorer.Account.Notifier.Summary do
+ @moduledoc """
+ Compose a summary from transactions
+ """
+
+ require Logger
+
+ alias Explorer.Account.Notifier.Summary
+ alias Explorer.{Chain, Repo}
+ alias Explorer.Chain.Wei
+
+ defstruct [
+ :transaction_hash,
+ :from_address_hash,
+ :to_address_hash,
+ :method,
+ :block_number,
+ :amount,
+ :tx_fee,
+ :name,
+ :subject,
+ :type
+ ]
+
+ def process(%Chain.Transaction{} = transaction) do
+ preloaded_transaction = preload(transaction)
+
+ transfers_summaries =
+ handle_collection(
+ transaction,
+ preloaded_transaction.token_transfers
+ )
+
+ transaction_summary = fetch_summary(transaction)
+
+ [transaction_summary | transfers_summaries]
+ |> Enum.filter(fn summary ->
+ not (is_nil(summary) or
+ summary == :nothing or
+ is_nil(summary.amount) or
+ summary.amount == Decimal.new(0))
+ end)
+ end
+
+ def process(%Chain.TokenTransfer{} = transfer) do
+ preloaded_transfer = preload(transfer)
+
+ summary = fetch_summary(preloaded_transfer.transaction, preloaded_transfer)
+
+ if summary != :nothing do
+ [summary]
+ else
+ []
+ end
+ end
+
+ def process(_), do: nil
+
+ def handle_collection(_transaction, []), do: []
+
+ def handle_collection(transaction, transfers_list) do
+ Enum.map(
+ transfers_list,
+ fn transfer ->
+ transaction
+ |> fetch_summary(transfer)
+ end
+ )
+ end
+
+ def fetch_summary(%Chain.Transaction{block_number: nil}), do: :nothing
+
+ def fetch_summary(%Chain.Transaction{created_contract_address_hash: nil} = transaction) do
+ %Summary{
+ transaction_hash: transaction.hash,
+ method: method(transaction),
+ from_address_hash: transaction.from_address_hash,
+ to_address_hash: transaction.to_address_hash,
+ block_number: transaction.block_number,
+ amount: amount(transaction),
+ tx_fee: fee(transaction),
+ name: Application.get_env(:explorer, :coin_name),
+ subject: "Coin transaction",
+ type: "COIN"
+ }
+ end
+
+ def fetch_summary(%Chain.Transaction{to_address_hash: nil} = transaction) do
+ %Summary{
+ transaction_hash: transaction.hash,
+ method: "contract_creation",
+ from_address_hash: transaction.from_address_hash,
+ to_address_hash: transaction.created_contract_address_hash,
+ block_number: transaction.block_number,
+ amount: amount(transaction),
+ tx_fee: fee(transaction),
+ name: Application.get_env(:explorer, :coin_name),
+ subject: "Contract creation",
+ type: "COIN"
+ }
+ end
+
+ def fetch_summary(_), do: :nothing
+
+ def fetch_summary(%Chain.Transaction{block_number: nil}, _), do: :nothing
+
+ def fetch_summary(
+ %Chain.Transaction{} = transaction,
+ %Chain.TokenTransfer{} = transfer
+ ) do
+ case transfer.token.type do
+ "ERC-20" ->
+ %Summary{
+ transaction_hash: transaction.hash,
+ method: method(transfer),
+ from_address_hash: transfer.from_address_hash,
+ to_address_hash: transfer.to_address_hash,
+ block_number: transfer.block_number,
+ amount: amount(transfer),
+ subject: transfer.token.type,
+ tx_fee: fee(transaction),
+ name: transfer.token.name,
+ type: transfer.token.type
+ }
+
+ "ERC-721" ->
+ %Summary{
+ amount: 0,
+ transaction_hash: transaction.hash,
+ method: method(transfer),
+ from_address_hash: transfer.from_address_hash,
+ to_address_hash: transfer.to_address_hash,
+ block_number: transfer.block_number,
+ subject: to_string(transfer.token_id),
+ tx_fee: fee(transaction),
+ name: transfer.token.name,
+ type: transfer.token.type
+ }
+
+ "ERC-1155" ->
+ %Summary{
+ amount: 0,
+ transaction_hash: transaction.hash,
+ method: method(transfer),
+ from_address_hash: transfer.from_address_hash,
+ to_address_hash: transfer.to_address_hash,
+ block_number: transfer.block_number,
+ subject: token_ids(transfer),
+ tx_fee: fee(transaction),
+ name: transfer.token.name,
+ type: transfer.token.type
+ }
+ end
+ end
+
+ def fetch_summary(_, _), do: :nothing
+
+ @burn_address "0x0000000000000000000000000000000000000000"
+
+ def method(%{from_address_hash: from, to_address_hash: to}) do
+ {:ok, burn_address} = format_address(@burn_address)
+
+ cond do
+ burn_address == from -> "mint"
+ burn_address == to -> "burn"
+ true -> "transfer"
+ end
+ end
+
+ def format_address(address_hash_string) do
+ Chain.string_to_address_hash(address_hash_string)
+ end
+
+ def amount(%Chain.Transaction{} = transaction) do
+ Wei.to(transaction.value, :ether)
+ end
+
+ def amount(%Chain.TokenTransfer{amount: amount}) when is_nil(amount), do: nil
+
+ def amount(%Chain.TokenTransfer{amount: amount} = transfer) do
+ decimals =
+ Decimal.new(
+ Integer.pow(
+ 10,
+ Decimal.to_integer(token_decimals(transfer))
+ )
+ )
+
+ Decimal.div(
+ amount,
+ decimals
+ )
+ end
+
+ def token_ids(%Chain.TokenTransfer{token_id: token_id, token_ids: token_ids}) do
+ case token_id do
+ nil ->
+ Enum.map_join(token_ids, ", ", fn id -> to_string(id) end)
+
+ _ ->
+ to_string(token_id)
+ end
+ end
+
+ def token_decimals(%Chain.TokenTransfer{} = transfer) do
+ transfer.token.decimals || Decimal.new(1)
+ end
+
+ def type(%Chain.Transaction{}), do: :coin
+ def type(%Chain.InternalTransaction{}), do: :coin
+
+ def fee(%Chain.Transaction{} = transaction) do
+ {_, fee} = Chain.fee(transaction, :gwei)
+ fee
+ end
+
+ def preload(%Chain.Transaction{} = transaction) do
+ Repo.preload(transaction, [:internal_transactions, token_transfers: :token])
+ end
+
+ def preload(%Chain.TokenTransfer{} = transfer) do
+ Repo.preload(transfer, [:transaction, :token])
+ end
+
+ def preload(_), do: nil
+end
diff --git a/apps/explorer/lib/explorer/account/notify.ex b/apps/explorer/lib/explorer/account/notify.ex
new file mode 100644
index 0000000000..fc10579294
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/notify.ex
@@ -0,0 +1,44 @@
+defmodule Explorer.Account.Notify do
+ @moduledoc """
+ Interface for notifier, for import and call from other modules
+ """
+
+ alias Explorer.Account
+ alias Explorer.Account.Notifier.Notify
+
+ require Logger
+
+ def async(transactions) do
+ Task.async(fn -> process(transactions) end)
+ end
+
+ defp process(transactions) do
+ if Account.enabled?() do
+ check_envs()
+ Notify.call(transactions)
+ end
+ rescue
+ err ->
+ Logger.info("--- Notifier error", fetcher: :account)
+ Logger.info(err, fetcher: :account)
+ end
+
+ defp check_envs do
+ check_auth0()
+ check_sendgrid()
+ end
+
+ defp check_auth0 do
+ (Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_id] &&
+ Application.get_env(:ueberauth, Ueberauth.Strategy.Auth0.OAuth)[:client_secret] &&
+ Application.get_env(:ueberauth, Ueberauth)[:logout_return_to_url] &&
+ Application.get_env(:ueberauth, Ueberauth)[:logout_url]) ||
+ raise "Auth0 not configured"
+ end
+
+ defp check_sendgrid do
+ (Application.get_env(:explorer, Explorer.Account)[:sendgrid][:sender] &&
+ Application.get_env(:explorer, Explorer.Account)[:sendgrid][:template]) ||
+ raise "SendGrid not configured"
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/public_tags_request.ex b/apps/explorer/lib/explorer/account/public_tags_request.ex
new file mode 100644
index 0000000000..eb989e5e76
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/public_tags_request.ex
@@ -0,0 +1,255 @@
+defmodule Explorer.Account.PublicTagsRequest do
+ @moduledoc """
+ Module is responsible for requests for public tags
+ """
+ use Explorer.Schema
+
+ alias Ecto.Changeset
+ alias Explorer.Account.Identity
+ alias Explorer.Chain.Hash
+ alias Explorer.Repo
+ alias Explorer.ThirdPartyIntegrations.AirTable
+
+ import Ecto.Changeset
+
+ @distance_between_same_addresses 24 * 3600
+
+ @max_public_tags_request_per_account 15
+ @max_addresses_per_request 10
+ @max_tags_per_request 2
+ @max_tag_length 35
+
+ schema("account_public_tags_requests") do
+ field(:company, :string)
+ field(:website, :string)
+ field(:tags, :string)
+ field(:addresses, {:array, Hash.Address})
+ field(:description, :string)
+ field(:additional_comment, :string)
+ field(:request_type, :string)
+ field(:is_owner, :boolean, default: true)
+ field(:remove_reason, :string)
+ field(:request_id, :string)
+ field(:full_name, Explorer.Encrypted.Binary)
+ field(:email, Explorer.Encrypted.Binary)
+
+ belongs_to(:identity, Identity)
+
+ timestamps()
+ end
+
+ @local_fields [:__meta__, :inserted_at, :updated_at, :id, :request_id]
+
+ def to_map(%__MODULE__{} = request) do
+ association_fields = request.__struct__.__schema__(:associations)
+ waste_fields = association_fields ++ @local_fields
+
+ network =
+ Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host] <>
+ Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path]
+
+ request |> Map.from_struct() |> Map.drop(waste_fields) |> Map.put(:network, network)
+ end
+
+ @attrs ~w(company website description remove_reason request_id)a
+ @required_attrs ~w(full_name email tags addresses additional_comment request_type is_owner identity_id)a
+
+ def changeset(%__MODULE__{} = public_tags_request, attrs \\ %{}) do
+ public_tags_request
+ |> cast(trim_empty_addresses(attrs), @attrs ++ @required_attrs)
+ |> validate_tags()
+ |> validate_required(@required_attrs, message: "Required")
+ |> validate_format(:email, ~r/^[A-Z0-9._%+-]+@[A-Z0-9-]+.+.[A-Z]{2,4}$/i, message: "is invalid")
+ |> validate_length(:addresses, min: 1, max: @max_addresses_per_request)
+ |> extract_and_validate_addresses()
+ |> foreign_key_constraint(:identity_id)
+ |> public_tags_request_count_constraint()
+ |> public_tags_request_time_interval_uniqueness()
+ end
+
+ def changeset_without_constraints(%__MODULE__{} = public_tags_request \\ %__MODULE__{}, attrs \\ %{}) do
+ public_tags_request
+ |> cast(attrs, @attrs ++ @required_attrs)
+ end
+
+ def create(attrs) do
+ %__MODULE__{}
+ |> changeset(Map.put(attrs, :request_type, "add"))
+ |> Repo.account_repo().insert()
+ |> AirTable.submit()
+ end
+
+ defp trim_empty_addresses(%{addresses: addresses} = attrs) when is_list(addresses) do
+ filtered_addresses = Enum.filter(addresses, fn addr -> addr != "" and !is_nil(addr) end)
+ Map.put(attrs, :addresses, if(filtered_addresses == [], do: [""], else: filtered_addresses))
+ end
+
+ defp trim_empty_addresses(attrs), do: attrs
+
+ def public_tags_request_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = request) do
+ if identity_id
+ |> public_tags_requests_by_identity_id_query()
+ |> limit(@max_public_tags_request_per_account)
+ |> Repo.account_repo().aggregate(:count, :id) >= @max_public_tags_request_per_account do
+ request
+ |> add_error(:tags, "Max #{@max_public_tags_request_per_account} public tags requests per account")
+ else
+ request
+ end
+ end
+
+ def public_tags_request_count_constraint(changeset), do: changeset
+
+ defp public_tags_request_time_interval_uniqueness(%Changeset{changes: %{addresses: addresses}} = request) do
+ prepared_addresses =
+ if request.data && request.data.addresses, do: addresses -- request.data.addresses, else: addresses
+
+ public_tags_request =
+ request
+ |> fetch_field!(:identity_id)
+ |> public_tags_requests_by_identity_id_query()
+ |> where(
+ [public_tags_request],
+ fragment("? && ?", public_tags_request.addresses, ^Enum.map(prepared_addresses, fn x -> x.bytes end))
+ )
+ |> limit(1)
+ |> Repo.account_repo().one()
+
+ now = DateTime.utc_now()
+
+ if !is_nil(public_tags_request) &&
+ public_tags_request.inserted_at
+ |> DateTime.add(@distance_between_same_addresses, :second)
+ |> DateTime.compare(now) == :gt do
+ request
+ |> add_error(:addresses, "You have already submitted the same public tag address in the last 24 hours")
+ else
+ request
+ end
+ end
+
+ defp public_tags_request_time_interval_uniqueness(changeset), do: changeset
+
+ defp extract_and_validate_addresses(%Changeset{} = changeset) do
+ with {:fetch, {_src, addresses}} <- {:fetch, fetch_field(changeset, :addresses)},
+ false <- is_nil(addresses),
+ {:uniqueness, true} <- {:uniqueness, Enum.count(Enum.uniq(addresses)) == Enum.count(addresses)} do
+ changeset
+ else
+ {:uniqueness, false} ->
+ add_error(changeset, :addresses, "All addresses should be unique")
+
+ _ ->
+ add_error(changeset, :addresses, "No addresses")
+ end
+ end
+
+ defp validate_tags(%Changeset{} = changeset) do
+ with {:fetch, {_src, tags}} <- {:fetch, fetch_field(changeset, :tags)},
+ false <- is_nil(tags),
+ trimmed_tags <- String.trim(tags),
+ tags_list <- String.split(trimmed_tags, ";"),
+ {:filter_empty, [_ | _] = filtered_tags} <- {:filter_empty, Enum.filter(tags_list, fn tag -> tag != "" end)},
+ trimmed_spaces_tags <- Enum.map(filtered_tags, fn tag -> String.trim(tag) end),
+ {:validate, false} <- {:validate, Enum.any?(tags_list, fn tag -> String.length(tag) > @max_tag_length end)},
+ {:uniqueness, true} <-
+ {:uniqueness,
+ Enum.count(Enum.uniq_by(trimmed_spaces_tags, &String.downcase(&1))) == Enum.count(trimmed_spaces_tags)},
+ trimmed_tags_list <- Enum.take(trimmed_spaces_tags, @max_tags_per_request) do
+ force_change(changeset, :tags, Enum.join(trimmed_tags_list, ";"))
+ else
+ {:uniqueness, false} ->
+ add_error(changeset, :tags, "All tags should be unique")
+
+ {:filter_empty, _} ->
+ add_error(changeset, :tags, "All tags are empty strings")
+
+ {:validate, _} ->
+ add_error(changeset, :tags, "Tags should contain less than #{@max_tag_length} characters")
+
+ _ ->
+ add_error(changeset, :tags, "No tags")
+ end
+ end
+
+ def public_tags_requests_by_identity_id_query(id) when not is_nil(id) do
+ __MODULE__
+ |> where(
+ [request],
+ request.identity_id == ^id and request.request_type != "delete" and not is_nil(request.request_id)
+ )
+ |> order_by([request], desc: request.id)
+ end
+
+ def public_tags_requests_by_identity_id_query(_), do: nil
+
+ def public_tags_request_by_id_and_identity_id_query(id, identity_id)
+ when not is_nil(id) and not is_nil(identity_id) do
+ __MODULE__
+ |> where([public_tags_request], public_tags_request.identity_id == ^identity_id and public_tags_request.id == ^id)
+ end
+
+ def public_tags_request_by_id_and_identity_id_query(_, _), do: nil
+
+ def get_public_tags_request_by_id_and_identity_id(id, identity_id) when not is_nil(id) and not is_nil(identity_id) do
+ id |> public_tags_request_by_id_and_identity_id_query(identity_id) |> Repo.account_repo().one()
+ end
+
+ def get_public_tags_request_by_id_and_identity_id(_, _), do: nil
+
+ def get_public_tags_requests_by_identity_id(id) when not is_nil(id) do
+ id
+ |> public_tags_requests_by_identity_id_query()
+ |> Repo.account_repo().all()
+ end
+
+ def get_public_tags_requests_by_identity_id(_), do: nil
+
+ def delete_public_tags_request(identity_id, id) when not is_nil(id) and not is_nil(identity_id) do
+ id
+ |> public_tags_request_by_id_and_identity_id_query(identity_id)
+ |> Repo.account_repo().delete_all()
+ end
+
+ def delete_public_tags_request(_, _), do: nil
+
+ def update(%{id: id, identity_id: identity_id} = attrs) do
+ with public_tags_request <- get_public_tags_request_by_id_and_identity_id(id, identity_id),
+ false <- is_nil(public_tags_request),
+ {:ok, changeset} <-
+ public_tags_request |> changeset(Map.put(attrs, :request_type, "edit")) |> Repo.account_repo().update() do
+ AirTable.submit({:ok, changeset})
+ else
+ true ->
+ {:error, %{reason: :item_not_found}}
+
+ other ->
+ other
+ end
+ end
+
+ def mark_as_deleted_public_tags_request(%{id: id, identity_id: identity_id, remove_reason: remove_reason}) do
+ with public_tags_request <- get_public_tags_request_by_id_and_identity_id(id, identity_id),
+ false <- is_nil(public_tags_request),
+ {:ok, changeset} <-
+ public_tags_request
+ |> changeset_without_constraints(%{request_type: "delete", remove_reason: remove_reason})
+ |> Repo.account_repo().update() do
+ case AirTable.submit({:ok, changeset}) do
+ {:error, changeset} ->
+ changeset
+
+ _ ->
+ true
+ end
+ else
+ {:error, changeset} ->
+ changeset
+
+ _ ->
+ false
+ end
+ end
+
+ def get_max_public_tags_request_count, do: @max_public_tags_request_per_account
+end
diff --git a/apps/explorer/lib/explorer/account/tag_address.ex b/apps/explorer/lib/explorer/account/tag_address.ex
new file mode 100644
index 0000000000..75d4e8d6c8
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/tag_address.ex
@@ -0,0 +1,156 @@
+defmodule Explorer.Account.TagAddress do
+ @moduledoc """
+ Watchlist is root entity for WatchlistAddresses
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+
+ alias Ecto.Changeset
+ alias Explorer.Account.Identity
+ alias Explorer.{Chain, Repo}
+ alias Explorer.Chain.{Address, Hash}
+
+ import Explorer.Chain, only: [hash_to_lower_case_string: 1]
+
+ @max_tag_address_per_account 15
+
+ schema "account_tag_addresses" do
+ field(:address_hash_hash, Cloak.Ecto.SHA256)
+ field(:name, Explorer.Encrypted.Binary)
+ field(:address_hash, Explorer.Encrypted.AddressHash, null: false)
+
+ belongs_to(:identity, Identity)
+
+ timestamps()
+ end
+
+ @attrs ~w(name identity_id address_hash)a
+
+ def changeset do
+ %__MODULE__{}
+ |> cast(%{}, @attrs)
+ end
+
+ @doc false
+ def changeset(tag, attrs) do
+ tag
+ |> cast(attrs, @attrs)
+ |> validate_required(@attrs, message: "Required")
+ |> validate_length(:name, min: 1, max: 35)
+ |> put_hashed_fields()
+ |> unique_constraint([:identity_id, :address_hash_hash], message: "Address tag already exists")
+ |> check_existance_or_create_address()
+ |> tag_address_count_constraint()
+ end
+
+ def create(attrs) do
+ %__MODULE__{}
+ |> changeset(attrs)
+ |> Repo.account_repo().insert()
+ end
+
+ defp put_hashed_fields(changeset) do
+ changeset
+ |> put_change(:address_hash_hash, hash_to_lower_case_string(get_field(changeset, :address_hash)))
+ end
+
+ defp check_existance_or_create_address(%Changeset{changes: %{address_hash: address_hash}, valid?: true} = changeset) do
+ check_existance_or_create_address_inner(changeset, address_hash)
+ end
+
+ defp check_existance_or_create_address(changeset), do: changeset
+
+ defp check_existance_or_create_address_inner(changeset, address_hash) do
+ with {:ok, hash} <- Hash.Address.cast(address_hash),
+ {:ok, %Address{}} <- Chain.find_or_insert_address_from_hash(hash, []) do
+ changeset
+ end
+ end
+
+ def tag_address_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = tag_address) do
+ if identity_id
+ |> tags_address_by_identity_id_query()
+ |> limit(@max_tag_address_per_account)
+ |> Repo.account_repo().aggregate(:count, :id) >= @max_tag_address_per_account do
+ tag_address
+ |> add_error(:name, "Max #{@max_tag_address_per_account} tags per account")
+ else
+ tag_address
+ end
+ end
+
+ def tag_address_count_constraint(changeset), do: changeset
+
+ def tags_address_by_identity_id_query(id) when not is_nil(id) do
+ __MODULE__
+ |> where([tag], tag.identity_id == ^id)
+ |> order_by([tag], desc: tag.id)
+ end
+
+ def tags_address_by_identity_id_query(_), do: nil
+
+ def get_tags_address_by_identity_id(id) when not is_nil(id) do
+ id
+ |> tags_address_by_identity_id_query()
+ |> Repo.account_repo().all()
+ end
+
+ def get_tags_address_by_identity_id(_), do: nil
+
+ def tag_address_by_address_hash_and_identity_id_query(address_hash, identity_id)
+ when not is_nil(address_hash) and not is_nil(identity_id) do
+ __MODULE__
+ |> where([tag], tag.identity_id == ^identity_id and tag.address_hash == ^address_hash)
+ end
+
+ def tag_address_by_address_hash_and_identity_id_query(_, _), do: nil
+
+ def get_tag_address_by_address_hash_and_identity_id(address_hash, identity_id)
+ when not is_nil(address_hash) and not is_nil(identity_id) do
+ address_hash
+ |> hash_to_lower_case_string()
+ |> tag_address_by_address_hash_and_identity_id_query(identity_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_tag_address_by_address_hash_and_identity_id(_, _), do: nil
+
+ def tag_address_by_id_and_identity_id_query(tag_id, identity_id)
+ when not is_nil(tag_id) and not is_nil(identity_id) do
+ __MODULE__
+ |> where([tag], tag.identity_id == ^identity_id and tag.id == ^tag_id)
+ end
+
+ def tag_address_by_id_and_identity_id_query(_, _), do: nil
+
+ def get_tag_address_by_id_and_identity_id_query(tag_id, identity_id)
+ when not is_nil(tag_id) and not is_nil(identity_id) do
+ tag_id
+ |> tag_address_by_id_and_identity_id_query(identity_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_tag_address_by_id_and_identity_id_query(_, _), do: nil
+
+ def delete(tag_id, identity_id) when not is_nil(tag_id) and not is_nil(identity_id) do
+ tag_id
+ |> tag_address_by_id_and_identity_id_query(identity_id)
+ |> Repo.account_repo().delete_all()
+ end
+
+ def delete(_, _), do: nil
+
+ def update(%{id: tag_id, identity_id: identity_id} = attrs) do
+ with tag <- get_tag_address_by_id_and_identity_id_query(tag_id, identity_id),
+ false <- is_nil(tag) do
+ tag |> changeset(attrs) |> Repo.account_repo().update()
+ else
+ true ->
+ {:error, %{reason: :item_not_found}}
+ end
+ end
+
+ def get_max_tags_count, do: @max_tag_address_per_account
+end
diff --git a/apps/explorer/lib/explorer/account/tag_transaction.ex b/apps/explorer/lib/explorer/account/tag_transaction.ex
new file mode 100644
index 0000000000..495dd1779a
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/tag_transaction.ex
@@ -0,0 +1,155 @@
+defmodule Explorer.Account.TagTransaction do
+ @moduledoc """
+ This is a personal tag for transaction
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+
+ alias Ecto.Changeset
+ alias Explorer.Account.Identity
+ alias Explorer.{Chain, Repo}
+ import Explorer.Chain, only: [hash_to_lower_case_string: 1]
+
+ @max_tag_transaction_per_account 15
+
+ schema "account_tag_transactions" do
+ field(:tx_hash_hash, Cloak.Ecto.SHA256)
+ field(:name, Explorer.Encrypted.Binary)
+ field(:tx_hash, Explorer.Encrypted.TransactionHash, null: false)
+
+ belongs_to(:identity, Identity)
+
+ timestamps()
+ end
+
+ @attrs ~w(name identity_id tx_hash)a
+
+ def changeset do
+ %__MODULE__{}
+ |> cast(%{}, @attrs)
+ end
+
+ @doc false
+ def changeset(tag, attrs) do
+ tag
+ |> cast(attrs, @attrs)
+ |> validate_required(@attrs, message: "Required")
+ |> validate_length(:name, min: 1, max: 35)
+ |> put_hashed_fields()
+ |> unique_constraint([:identity_id, :tx_hash_hash], message: "Transaction tag already exists")
+ |> tag_transaction_count_constraint()
+ |> check_transaction_existance()
+ end
+
+ def create(attrs) do
+ %__MODULE__{}
+ |> changeset(attrs)
+ |> Repo.account_repo().insert()
+ end
+
+ defp put_hashed_fields(changeset) do
+ changeset
+ |> put_change(:tx_hash_hash, hash_to_lower_case_string(get_field(changeset, :tx_hash)))
+ end
+
+ defp check_transaction_existance(%Changeset{changes: %{tx_hash: tx_hash}} = changeset) do
+ check_transaction_existance_inner(changeset, tx_hash)
+ end
+
+ defp check_transaction_existance(changeset), do: changeset
+
+ defp check_transaction_existance_inner(changeset, tx_hash) do
+ if match?({:ok, _}, Chain.hash_to_transaction(tx_hash)) do
+ changeset
+ else
+ add_error(changeset, :tx_hash, "Transaction does not exist")
+ end
+ end
+
+ def tag_transaction_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = tag_transaction) do
+ if identity_id
+ |> tags_transaction_by_identity_id_query()
+ |> limit(@max_tag_transaction_per_account)
+ |> Repo.account_repo().aggregate(:count, :id) >= @max_tag_transaction_per_account do
+ tag_transaction
+ |> add_error(:name, "Max #{@max_tag_transaction_per_account} tags per account")
+ else
+ tag_transaction
+ end
+ end
+
+ def tag_transaction_count_constraint(changeset), do: changeset
+
+ def tags_transaction_by_identity_id_query(id) when not is_nil(id) do
+ __MODULE__
+ |> where([tag], tag.identity_id == ^id)
+ |> order_by([tag], desc: tag.id)
+ end
+
+ def tags_transaction_by_identity_id_query(_), do: nil
+
+ def get_tags_transaction_by_identity_id(id) when not is_nil(id) do
+ id
+ |> tags_transaction_by_identity_id_query()
+ |> Repo.account_repo().all()
+ end
+
+ def get_tags_transaction_by_identity_id(_), do: nil
+
+ def tag_transaction_by_transaction_hash_and_identity_id_query(tx_hash, identity_id)
+ when not is_nil(tx_hash) and not is_nil(identity_id) do
+ __MODULE__
+ |> where([tag], tag.identity_id == ^identity_id and tag.tx_hash == ^tx_hash)
+ end
+
+ def tag_transaction_by_transaction_hash_and_identity_id_query(_, _), do: nil
+
+ def get_tag_transaction_by_transaction_hash_and_identity_id(tx_hash, identity_id)
+ when not is_nil(tx_hash) and not is_nil(identity_id) do
+ tx_hash
+ |> hash_to_lower_case_string()
+ |> tag_transaction_by_transaction_hash_and_identity_id_query(identity_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_tag_transaction_by_transaction_hash_and_identity_id(_, _), do: nil
+
+ def tag_transaction_by_id_and_identity_id_query(tag_id, identity_id)
+ when not is_nil(tag_id) and not is_nil(identity_id) do
+ __MODULE__
+ |> where([tag], tag.identity_id == ^identity_id and tag.id == ^tag_id)
+ end
+
+ def tag_transaction_by_id_and_identity_id_query(_, _), do: nil
+
+ def get_tag_transaction_by_id_and_identity_id_query(tag_id, identity_id)
+ when not is_nil(tag_id) and not is_nil(identity_id) do
+ tag_id
+ |> tag_transaction_by_id_and_identity_id_query(identity_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_tag_transaction_by_id_and_identity_id_query(_, _), do: nil
+
+ def delete(tag_id, identity_id) when not is_nil(tag_id) and not is_nil(identity_id) do
+ tag_id
+ |> tag_transaction_by_id_and_identity_id_query(identity_id)
+ |> Repo.account_repo().delete_all()
+ end
+
+ def delete(_, _), do: nil
+
+ def update(%{id: tag_id, identity_id: identity_id} = attrs) do
+ with tag <- get_tag_transaction_by_id_and_identity_id_query(tag_id, identity_id),
+ false <- is_nil(tag) do
+ tag |> changeset(attrs) |> Repo.account_repo().update()
+ else
+ true ->
+ {:error, %{reason: :item_not_found}}
+ end
+ end
+
+ def get_max_tags_count, do: @max_tag_transaction_per_account
+end
diff --git a/apps/explorer/lib/explorer/account/watchlist.ex b/apps/explorer/lib/explorer/account/watchlist.ex
new file mode 100644
index 0000000000..cd6998b83f
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/watchlist.ex
@@ -0,0 +1,27 @@
+defmodule Explorer.Account.Watchlist do
+ @moduledoc """
+ Watchlist is root entity for WatchlistAddresses
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+
+ alias Explorer.Account.{Identity, WatchlistAddress}
+
+ @derive {Jason.Encoder, only: [:name, :watchlist_addresses]}
+ schema "account_watchlists" do
+ field(:name, :string)
+ belongs_to(:identity, Identity)
+ has_many(:watchlist_addresses, WatchlistAddress)
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(watchlist, attrs) do
+ watchlist
+ |> cast(attrs, [:name])
+ |> validate_required([:name])
+ end
+end
diff --git a/apps/explorer/lib/explorer/account/watchlist_address.ex b/apps/explorer/lib/explorer/account/watchlist_address.ex
new file mode 100644
index 0000000000..cce6c7935e
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/watchlist_address.ex
@@ -0,0 +1,176 @@
+defmodule Explorer.Account.WatchlistAddress do
+ @moduledoc """
+ WatchlistAddress entity
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+
+ alias Ecto.Changeset
+ alias Explorer.Account.Notifier.ForbiddenAddress
+ alias Explorer.Account.Watchlist
+ alias Explorer.{Chain, Repo}
+ alias Explorer.Chain.{Address, Wei}
+
+ import Explorer.Chain, only: [hash_to_lower_case_string: 1]
+
+ @max_watchlist_addresses_per_account 10
+
+ schema "account_watchlist_addresses" do
+ field(:address_hash_hash, Cloak.Ecto.SHA256)
+ field(:name, Explorer.Encrypted.Binary)
+ field(:address_hash, Explorer.Encrypted.AddressHash, null: false)
+
+ belongs_to(:watchlist, Watchlist)
+
+ field(:watch_coin_input, :boolean, default: true)
+ field(:watch_coin_output, :boolean, default: true)
+ field(:watch_erc_20_input, :boolean, default: true)
+ field(:watch_erc_20_output, :boolean, default: true)
+ field(:watch_erc_721_input, :boolean, default: true)
+ field(:watch_erc_721_output, :boolean, default: true)
+ field(:watch_erc_1155_input, :boolean, default: true)
+ field(:watch_erc_1155_output, :boolean, default: true)
+ field(:notify_email, :boolean, default: true)
+ field(:notify_epns, :boolean)
+ field(:notify_feed, :boolean)
+ field(:notify_inapp, :boolean)
+
+ field(:fetched_coin_balance, Wei, virtual: true)
+
+ timestamps()
+ end
+
+ @attrs ~w(name address_hash watch_coin_input watch_coin_output watch_erc_20_input watch_erc_20_output watch_erc_721_input watch_erc_721_output watch_erc_1155_input watch_erc_1155_output notify_email notify_epns notify_feed notify_inapp watchlist_id)a
+
+ def changeset do
+ %__MODULE__{}
+ |> cast(%{}, @attrs)
+ end
+
+ @doc false
+ def changeset(watchlist_address, attrs \\ %{}) do
+ watchlist_address
+ |> cast(attrs, @attrs)
+ |> validate_length(:name, min: 1, max: 35)
+ |> validate_required([:name, :address_hash, :watchlist_id], message: "Required")
+ |> put_hashed_fields()
+ |> unique_constraint([:watchlist_id, :address_hash_hash],
+ name: "unique_watchlist_id_address_hash_hash_index",
+ message: "Address already added to the watch list"
+ )
+ |> check_address()
+ |> watchlist_address_count_constraint()
+ end
+
+ defp put_hashed_fields(changeset) do
+ changeset
+ |> put_change(:address_hash_hash, hash_to_lower_case_string(get_field(changeset, :address_hash)))
+ end
+
+ def create(attrs) do
+ %__MODULE__{}
+ |> changeset(attrs)
+ |> Repo.account_repo().insert()
+ end
+
+ def watchlist_address_count_constraint(%Changeset{changes: %{watchlist_id: watchlist_id}} = watchlist_address) do
+ if watchlist_id
+ |> watchlist_addresses_by_watchlist_id_query()
+ |> limit(@max_watchlist_addresses_per_account)
+ |> Repo.account_repo().aggregate(:count, :id) >= @max_watchlist_addresses_per_account do
+ watchlist_address
+ |> add_error(:name, "Max #{@max_watchlist_addresses_per_account} watch list addresses per account")
+ else
+ watchlist_address
+ end
+ end
+
+ def watchlist_address_count_constraint(changeset), do: changeset
+
+ defp check_address(%Changeset{changes: %{address_hash: address_hash}, valid?: true} = changeset) do
+ check_address_inner(changeset, address_hash)
+ end
+
+ defp check_address(%Changeset{data: %{address_hash: address_hash}, valid?: true} = changeset) do
+ check_address_inner(changeset, address_hash)
+ end
+
+ defp check_address(changeset), do: changeset
+
+ defp check_address_inner(changeset, address_hash) do
+ with {:ok, address_hash} <- ForbiddenAddress.check(address_hash),
+ {:ok, %Address{}} <- Chain.find_or_insert_address_from_hash(address_hash, []) do
+ changeset
+ else
+ {:error, reason} ->
+ add_error(changeset, :address_hash, reason)
+
+ _ ->
+ add_error(changeset, :address_hash, "Address error")
+ end
+ end
+
+ def watchlist_addresses_by_watchlist_id_query(watchlist_id) when not is_nil(watchlist_id) do
+ __MODULE__
+ |> where([wl_address], wl_address.watchlist_id == ^watchlist_id)
+ end
+
+ def watchlist_addresses_by_watchlist_id_query(_), do: nil
+
+ def watchlist_address_by_id_and_watchlist_id_query(watchlist_address_id, watchlist_id)
+ when not is_nil(watchlist_address_id) and not is_nil(watchlist_id) do
+ __MODULE__
+ |> where([wl_address], wl_address.watchlist_id == ^watchlist_id and wl_address.id == ^watchlist_address_id)
+ end
+
+ def watchlist_address_by_id_and_watchlist_id_query(_, _), do: nil
+
+ def get_watchlist_address_by_id_and_watchlist_id(watchlist_address_id, watchlist_id)
+ when not is_nil(watchlist_address_id) and not is_nil(watchlist_id) do
+ watchlist_address_id
+ |> watchlist_address_by_id_and_watchlist_id_query(watchlist_id)
+ |> Repo.account_repo().one()
+ end
+
+ def get_watchlist_address_by_id_and_watchlist_id(_, _), do: nil
+
+ def delete(watchlist_address_id, watchlist_id)
+ when not is_nil(watchlist_address_id) and not is_nil(watchlist_id) do
+ watchlist_address_id
+ |> watchlist_address_by_id_and_watchlist_id_query(watchlist_id)
+ |> Repo.account_repo().delete_all()
+ end
+
+ def delete(_, _), do: nil
+
+ def update(%{id: id, watchlist_id: watchlist_id} = attrs) do
+ with watchlist_address <- get_watchlist_address_by_id_and_watchlist_id(id, watchlist_id),
+ false <- is_nil(watchlist_address) do
+ watchlist_address
+ |> changeset(attrs)
+ |> Repo.account_repo().update()
+ else
+ true ->
+ {:error, %{reason: :item_not_found}}
+ end
+ end
+
+ def get_max_watchlist_addresses_count, do: @max_watchlist_addresses_per_account
+
+ def preload_address_fetched_coin_balance(%Watchlist{watchlist_addresses: watchlist_addresses} = watchlist) do
+ w_addresses =
+ Enum.map(watchlist_addresses, fn wa ->
+ preload_address_fetched_coin_balance(wa)
+ end)
+
+ %Watchlist{watchlist | watchlist_addresses: w_addresses}
+ end
+
+ def preload_address_fetched_coin_balance(%__MODULE__{address_hash: address_hash} = watchlist_address) do
+ %__MODULE__{watchlist_address | fetched_coin_balance: address_hash |> Address.fetched_coin_balance() |> Repo.one()}
+ end
+
+ def preload_address_fetched_coin_balance(watchlist), do: watchlist
+end
diff --git a/apps/explorer/lib/explorer/account/watchlist_notification.ex b/apps/explorer/lib/explorer/account/watchlist_notification.ex
new file mode 100644
index 0000000000..6c909be3ed
--- /dev/null
+++ b/apps/explorer/lib/explorer/account/watchlist_notification.ex
@@ -0,0 +1,65 @@
+defmodule Explorer.Account.WatchlistNotification do
+ @moduledoc """
+ Strored notification about event
+ related to WatchlistAddress
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+ import Explorer.Chain, only: [hash_to_lower_case_string: 1]
+
+ alias Explorer.Account.WatchlistAddress
+
+ schema "account_watchlist_notifications" do
+ field(:amount, :decimal)
+ field(:block_number, :integer)
+ field(:direction, :string)
+ field(:method, :string)
+ field(:tx_fee, :decimal)
+ field(:type, :string)
+ field(:viewed_at, :integer)
+ field(:name, Explorer.Encrypted.Binary)
+ field(:subject, Explorer.Encrypted.Binary)
+ field(:subject_hash, Cloak.Ecto.SHA256)
+
+ belongs_to(:watchlist_address, WatchlistAddress)
+
+ field(:from_address_hash, Explorer.Encrypted.AddressHash)
+ field(:to_address_hash, Explorer.Encrypted.AddressHash)
+ field(:transaction_hash, Explorer.Encrypted.TransactionHash)
+
+ field(:from_address_hash_hash, Cloak.Ecto.SHA256)
+ field(:to_address_hash_hash, Cloak.Ecto.SHA256)
+ field(:transaction_hash_hash, Cloak.Ecto.SHA256)
+
+ timestamps()
+ end
+
+ @doc false
+ def changeset(watchlist_notifications, attrs) do
+ watchlist_notifications
+ |> cast(attrs, [:amount, :direction, :name, :type, :method, :block_number, :tx_fee, :value, :decimals, :viewed_at])
+ |> validate_required([
+ :amount,
+ :direction,
+ :name,
+ :type,
+ :method,
+ :block_number,
+ :tx_fee,
+ :value,
+ :decimals,
+ :viewed_at
+ ])
+ |> put_hashed_fields()
+ end
+
+ defp put_hashed_fields(changeset) do
+ changeset
+ |> put_change(:from_address_hash_hash, hash_to_lower_case_string(get_field(changeset, :from_address_hash)))
+ |> put_change(:to_address_hash_hash, hash_to_lower_case_string(get_field(changeset, :to_address_hash)))
+ |> put_change(:transaction_hash_hash, hash_to_lower_case_string(get_field(changeset, :transaction_hash)))
+ |> put_change(:subject_hash, get_field(changeset, :subject))
+ end
+end
diff --git a/apps/explorer/lib/explorer/application.ex b/apps/explorer/lib/explorer/application.ex
index c42370c0e5..1fd3a6cd2e 100644
--- a/apps/explorer/lib/explorer/application.ex
+++ b/apps/explorer/lib/explorer/application.ex
@@ -43,6 +43,8 @@ defmodule Explorer.Application do
base_children = [
Explorer.Repo,
Explorer.Repo.Replica1,
+ Explorer.Repo.Account,
+ Explorer.Vault,
Supervisor.child_spec({SpandexDatadog.ApiServer, datadog_opts()}, id: SpandexDatadog.ApiServer),
Supervisor.child_spec({Task.Supervisor, name: Explorer.HistoryTaskSupervisor}, id: Explorer.HistoryTaskSupervisor),
Supervisor.child_spec({Task.Supervisor, name: Explorer.MarketTaskSupervisor}, id: Explorer.MarketTaskSupervisor),
@@ -65,7 +67,8 @@ defmodule Explorer.Application do
con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
Transactions,
Accounts,
- Uncles
+ Uncles,
+ {Redix, redix_opts()}
]
children = base_children ++ configurable_children()
@@ -174,4 +177,8 @@ defmodule Explorer.Application do
id: {ConCache, name}
)
end
+
+ defp redix_opts do
+ {System.get_env("ACCOUNT_REDIS_URL") || "redis://127.0.0.1:6379", [name: :redix]}
+ end
end
diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex
index 7b0c65c263..e80f66a4ba 100644
--- a/apps/explorer/lib/explorer/chain.ex
+++ b/apps/explorer/lib/explorer/chain.ex
@@ -6183,4 +6183,22 @@ defmodule Explorer.Chain do
query
|> Repo.one()
end
+
+ def is_address_hash_is_smart_contract?(nil), do: false
+
+ def is_address_hash_is_smart_contract?(address_hash) do
+ with %Address{contract_code: bytecode} <- Repo.get_by(Address, hash: address_hash),
+ false <- is_nil(bytecode) do
+ true
+ else
+ _ ->
+ false
+ end
+ end
+
+ def hash_to_lower_case_string(hash) do
+ hash
+ |> to_string()
+ |> String.downcase()
+ end
end
diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex
index 1a71d3e224..163dd66f8d 100644
--- a/apps/explorer/lib/explorer/chain/address.ex
+++ b/apps/explorer/lib/explorer/chain/address.ex
@@ -259,6 +259,12 @@ defmodule Explorer.Chain.Address do
)
end
+ def fetched_coin_balance(address_hash) when not is_nil(address_hash) do
+ Address
+ |> where([address], address.hash == ^address_hash)
+ |> select([address], address.fetched_coin_balance)
+ end
+
defimpl String.Chars do
@doc """
Uses `hash` as string representation, formatting it according to the eip-55 specification
diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex
index b3aa19ac42..8640d47e3a 100644
--- a/apps/explorer/lib/explorer/chain/import.ex
+++ b/apps/explorer/lib/explorer/chain/import.ex
@@ -4,10 +4,13 @@ defmodule Explorer.Chain.Import do
"""
alias Ecto.Changeset
+ alias Explorer.Account.Notify
alias Explorer.Chain.Events.Publisher
alias Explorer.Chain.Import
alias Explorer.Repo
+ require Logger
+
@stages [
Import.Stage.Addresses,
Import.Stage.AddressReferencing,
@@ -126,6 +129,7 @@ defmodule Explorer.Chain.Import do
{:ok, valid_runner_option_pairs} <- validate_runner_options_pairs(runner_options_pairs),
{:ok, runner_to_changes_list} <- runner_to_changes_list(valid_runner_option_pairs),
{:ok, data} <- insert_runner_to_changes_list(runner_to_changes_list, options) do
+ Notify.async(data[:transactions])
Publisher.broadcast(data, Map.get(options, :broadcast, false))
{:ok, data}
end
diff --git a/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex b/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex
index 18b8eb089c..014bef4a04 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex
@@ -57,7 +57,7 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations do
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout
}) ::
- {:ok, %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t(), index: non_neg_integer()}}
+ {:ok, nil | %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t(), index: non_neg_integer()}}
| {:error, [Changeset.t()]}
defp insert(repo, changes_list, %{timeout: timeout} = options) when is_atom(repo) and is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
diff --git a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex
index 3935fb53b5..2b39a32fd7 100644
--- a/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex
+++ b/apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex
@@ -57,7 +57,7 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do
# Enforce TokenTransfer ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.block_hash, &1.log_index})
- {:ok, _} =
+ {:ok, inserted} =
Import.insert_changes_list(
repo,
ordered_changes_list,
@@ -68,6 +68,8 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do
timeout: timeout,
timestamps: timestamps
)
+
+ {:ok, inserted}
end
defp default_on_conflict do
diff --git a/apps/explorer/lib/explorer/chain/wei.ex b/apps/explorer/lib/explorer/chain/wei.ex
index b77f71f90f..8fa3a69b22 100644
--- a/apps/explorer/lib/explorer/chain/wei.ex
+++ b/apps/explorer/lib/explorer/chain/wei.ex
@@ -266,3 +266,9 @@ defimpl Inspect, for: Explorer.Chain.Wei do
"#Explorer.Chain.Wei<#{Decimal.to_string(wei.value)}>"
end
end
+
+defimpl Jason.Encoder, for: Explorer.Chain.Wei do
+ def encode(wei, _) do
+ Decimal.to_string(wei.value)
+ end
+end
diff --git a/apps/explorer/lib/explorer/encrypted/address_hash.ex b/apps/explorer/lib/explorer/encrypted/address_hash.ex
new file mode 100644
index 0000000000..4518951298
--- /dev/null
+++ b/apps/explorer/lib/explorer/encrypted/address_hash.ex
@@ -0,0 +1,5 @@
+defmodule Explorer.Encrypted.AddressHash do
+ @moduledoc false
+
+ use Explorer.Encrypted.Types.AddressHash, vault: Explorer.Vault
+end
diff --git a/apps/explorer/lib/explorer/encrypted/binary.ex b/apps/explorer/lib/explorer/encrypted/binary.ex
new file mode 100644
index 0000000000..6de296ded6
--- /dev/null
+++ b/apps/explorer/lib/explorer/encrypted/binary.ex
@@ -0,0 +1,5 @@
+defmodule Explorer.Encrypted.Binary do
+ @moduledoc false
+
+ use Cloak.Ecto.Binary, vault: Explorer.Vault
+end
diff --git a/apps/explorer/lib/explorer/encrypted/transaction_hash.ex b/apps/explorer/lib/explorer/encrypted/transaction_hash.ex
new file mode 100644
index 0000000000..a783cb899b
--- /dev/null
+++ b/apps/explorer/lib/explorer/encrypted/transaction_hash.ex
@@ -0,0 +1,5 @@
+defmodule Explorer.Encrypted.TransactionHash do
+ @moduledoc false
+
+ use Explorer.Encrypted.Types.TransactionHash, vault: Explorer.Vault
+end
diff --git a/apps/explorer/lib/explorer/encrypted/types/address_hash.ex b/apps/explorer/lib/explorer/encrypted/types/address_hash.ex
new file mode 100644
index 0000000000..f9ca332c53
--- /dev/null
+++ b/apps/explorer/lib/explorer/encrypted/types/address_hash.ex
@@ -0,0 +1,27 @@
+defmodule Explorer.Encrypted.Types.AddressHash do
+ @moduledoc """
+ An `Ecto.Type` to encrypt address_hash fields.
+ """
+
+ @doc false
+ defmacro __using__(opts) do
+ opts = Keyword.merge(opts, vault: Keyword.fetch!(opts, :vault))
+
+ quote do
+ use Cloak.Ecto.Type, unquote(opts)
+
+ def cast(value) do
+ Explorer.Chain.Hash.Address.cast(value)
+ end
+
+ def after_decrypt(nil), do: nil
+ def after_decrypt(""), do: nil
+ def after_decrypt(:error), do: nil
+
+ def after_decrypt(value) do
+ {:ok, address_hash} = Explorer.Chain.Hash.Address.cast(value)
+ address_hash
+ end
+ end
+ end
+end
diff --git a/apps/explorer/lib/explorer/encrypted/types/transaction_hash.ex b/apps/explorer/lib/explorer/encrypted/types/transaction_hash.ex
new file mode 100644
index 0000000000..7c39a9aeca
--- /dev/null
+++ b/apps/explorer/lib/explorer/encrypted/types/transaction_hash.ex
@@ -0,0 +1,27 @@
+defmodule Explorer.Encrypted.Types.TransactionHash do
+ @moduledoc """
+ An `Ecto.Type` to encrypt transaction_hash fields.
+ """
+
+ @doc false
+ defmacro __using__(opts) do
+ opts = Keyword.merge(opts, vault: Keyword.fetch!(opts, :vault))
+
+ quote do
+ use Cloak.Ecto.Type, unquote(opts)
+
+ def cast(value) do
+ Explorer.Chain.Hash.Full.cast(value)
+ end
+
+ def after_decrypt(nil), do: nil
+ def after_decrypt(""), do: nil
+ def after_decrypt(:error), do: nil
+
+ def after_decrypt(value) do
+ {:ok, transaction_hash} = Explorer.Chain.Hash.Full.cast(value)
+ transaction_hash
+ end
+ end
+ end
+end
diff --git a/apps/explorer/lib/explorer/env_var_translator.ex b/apps/explorer/lib/explorer/env_var_translator.ex
new file mode 100644
index 0000000000..62f3da9003
--- /dev/null
+++ b/apps/explorer/lib/explorer/env_var_translator.ex
@@ -0,0 +1,24 @@
+defmodule Explorer.EnvVarTranslator do
+ @moduledoc """
+ The module for transaformation of environment variables
+ """
+
+ alias Poison.Parser
+
+ @spec map_array_env_var_to_list(atom()) :: list()
+ def map_array_env_var_to_list(config_name) do
+ env_var = Application.get_env(:block_scout_web, config_name)
+
+ if env_var do
+ try do
+ env_var
+ |> Parser.parse!(%{keys: :atoms!})
+ rescue
+ _ ->
+ []
+ end
+ else
+ []
+ end
+ end
+end
diff --git a/apps/explorer/lib/explorer/exchange_rates/token.ex b/apps/explorer/lib/explorer/exchange_rates/token.ex
index 6521181c12..8aa6d3b721 100644
--- a/apps/explorer/lib/explorer/exchange_rates/token.ex
+++ b/apps/explorer/lib/explorer/exchange_rates/token.ex
@@ -30,6 +30,7 @@ defmodule Explorer.ExchangeRates.Token do
volume_24h_usd: Decimal.t()
}
+ @derive Jason.Encoder
@enforce_keys ~w(available_supply total_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a
defstruct ~w(available_supply total_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a
diff --git a/apps/explorer/lib/explorer/mailer.ex b/apps/explorer/lib/explorer/mailer.ex
new file mode 100644
index 0000000000..ba5c095714
--- /dev/null
+++ b/apps/explorer/lib/explorer/mailer.ex
@@ -0,0 +1,12 @@
+defmodule Explorer.Mailer do
+ @moduledoc """
+ Base module for mail sending
+
+ add in your module:
+ alias Explorer.Mailer
+
+ and call
+ Mailer.deliver_now!(email)
+ """
+ use Bamboo.Mailer, otp_app: :explorer
+end
diff --git a/apps/explorer/lib/explorer/repo.ex b/apps/explorer/lib/explorer/repo.ex
index 14a48d9d33..c64db8b97d 100644
--- a/apps/explorer/lib/explorer/repo.ex
+++ b/apps/explorer/lib/explorer/repo.ex
@@ -128,10 +128,54 @@ defmodule Explorer.Repo do
def replica, do: Explorer.Repo.Replica1
end
+ def account_repo, do: Explorer.Repo.Account
+
defmodule Replica1 do
use Ecto.Repo,
otp_app: :explorer,
adapter: Ecto.Adapters.Postgres,
read_only: true
+
+ def init(_, opts) do
+ db_url = Application.get_env(:explorer, Explorer.Repo.Replica1)[:url]
+ repo_conf = Application.get_env(:explorer, Explorer.Repo.Replica1)
+
+ merged =
+ %{url: db_url}
+ |> ConfigHelper.get_db_config()
+ |> Keyword.merge(repo_conf, fn
+ _key, v1, nil -> v1
+ _key, nil, v2 -> v2
+ _, _, v2 -> v2
+ end)
+
+ Application.put_env(:explorer, Explorer.Repo.Replica1, merged)
+
+ {:ok, Keyword.put(opts, :url, db_url)}
+ end
+ end
+
+ defmodule Account do
+ use Ecto.Repo,
+ otp_app: :explorer,
+ adapter: Ecto.Adapters.Postgres
+
+ def init(_, opts) do
+ db_url = Application.get_env(:explorer, Explorer.Repo.Account)[:url]
+ repo_conf = Application.get_env(:explorer, Explorer.Repo.Account)
+
+ merged =
+ %{url: db_url}
+ |> ConfigHelper.get_db_config()
+ |> Keyword.merge(repo_conf, fn
+ _key, v1, nil -> v1
+ _key, nil, v2 -> v2
+ _, _, v2 -> v2
+ end)
+
+ Application.put_env(:explorer, Explorer.Repo.Account, merged)
+
+ {:ok, Keyword.put(opts, :url, db_url)}
+ end
end
end
diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex
index b9f4410a1b..d83cdc2c4a 100644
--- a/apps/explorer/lib/explorer/smart_contract/reader.ex
+++ b/apps/explorer/lib/explorer/smart_contract/reader.ex
@@ -228,11 +228,7 @@ defmodule Explorer.SmartContract.Reader do
[]
_ ->
- abi_with_method_id = get_abi_with_method_id(abi)
-
- abi_with_method_id
- |> Enum.filter(&Helper.queriable_method?(&1))
- |> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false))
+ read_only_functions_from_abi(abi, contract_address_hash)
end
end
@@ -244,13 +240,7 @@ defmodule Explorer.SmartContract.Reader do
[]
_ ->
- implementation_abi_with_method_id = get_abi_with_method_id(implementation_abi)
-
- implementation_abi_with_method_id
- |> Enum.filter(&Helper.queriable_method?(&1))
- |> Enum.map(
- &fetch_current_value_from_blockchain(&1, implementation_abi_with_method_id, contract_address_hash, false)
- )
+ read_only_functions_from_abi(implementation_abi, contract_address_hash)
end
end
@@ -266,10 +256,7 @@ defmodule Explorer.SmartContract.Reader do
[]
_ ->
- implementation_abi_with_method_id = get_abi_with_method_id(implementation_abi)
-
- implementation_abi_with_method_id
- |> Enum.filter(&Helper.read_with_wallet_method?(&1))
+ read_functions_required_wallet_from_abi(implementation_abi)
end
end
@@ -288,13 +275,29 @@ defmodule Explorer.SmartContract.Reader do
[]
_ ->
- abi_with_method_id = get_abi_with_method_id(abi)
-
- abi_with_method_id
- |> Enum.filter(&Helper.read_with_wallet_method?(&1))
+ read_functions_required_wallet_from_abi(abi)
end
end
+ def read_only_functions_from_abi([_ | _] = abi, contract_address_hash) do
+ abi_with_method_id = get_abi_with_method_id(abi)
+
+ abi_with_method_id
+ |> Enum.filter(&Helper.queriable_method?(&1))
+ |> Enum.map(&fetch_current_value_from_blockchain(&1, abi_with_method_id, contract_address_hash, false))
+ end
+
+ def read_only_functions_from_abi(_, _), do: []
+
+ def read_functions_required_wallet_from_abi([_ | _] = abi) do
+ abi_with_method_id = get_abi_with_method_id(abi)
+
+ abi_with_method_id
+ |> Enum.filter(&Helper.read_with_wallet_method?(&1))
+ end
+
+ def read_functions_required_wallet_from_abi(_), do: []
+
def get_abi_with_method_id(abi) do
abi
|> Enum.map(fn method ->
@@ -365,14 +368,16 @@ defmodule Explorer.SmartContract.Reader do
@doc """
Method performs query of read functions of a smart contract.
`type` could be :proxy or :reqular
- if ethereumJSONRPC will return some errors it will represented as map
+ `from` is a address of a function caller
"""
- @spec query_function_with_names(Hash.t(), %{method_id: String.t(), args: [term()] | nil}, atom()) :: %{
- :names => [any()],
- :output => [%{}]
- }
- def query_function_with_names(contract_address_hash, %{method_id: method_id, args: args}, type) do
- outputs = query_function(contract_address_hash, %{method_id: method_id, args: args}, type, true)
+ @spec query_function_with_names(
+ Hash.t(),
+ %{method_id: String.t(), args: [term()] | nil},
+ atom(),
+ String.t()
+ ) :: %{:names => [any()], :output => [%{}]}
+ def query_function_with_names(contract_address_hash, %{method_id: method_id, args: args}, type, from) do
+ outputs = query_function(contract_address_hash, %{method_id: method_id, args: args}, type, from, true)
names = parse_names_from_abi(get_abi(contract_address_hash, type), method_id)
%{output: outputs, names: names}
end
@@ -382,15 +387,17 @@ defmodule Explorer.SmartContract.Reader do
`type` could be :proxy or :reqular
`from` is a address of a function caller
"""
- @spec query_function_with_names(
+ @spec query_function_with_names_custom_abi(
Hash.t(),
%{method_id: String.t(), args: [term()] | nil},
- atom(),
- String.t()
+ String.t(),
+ [%{}]
) :: %{:names => [any()], :output => [%{}]}
- def query_function_with_names(contract_address_hash, %{method_id: method_id, args: args}, type, from) do
- outputs = query_function(contract_address_hash, %{method_id: method_id, args: args}, type, from, true)
- names = parse_names_from_abi(get_abi(contract_address_hash, type), method_id)
+ def query_function_with_names_custom_abi(contract_address_hash, %{method_id: method_id, args: args}, from, custom_abi) do
+ outputs =
+ query_function_with_custom_abi(contract_address_hash, %{method_id: method_id, args: args}, from, true, custom_abi)
+
+ names = parse_names_from_abi(custom_abi, method_id)
%{output: outputs, names: names}
end
@@ -432,7 +439,77 @@ defmodule Explorer.SmartContract.Reader do
query_contract_and_link_outputs(contract_address_hash, args, from, abi, outputs, method_id, leave_error_as_map)
end
- defp proccess_abi(nil, _method_id), do: nil
+ @spec query_function_with_custom_abi(
+ String.t(),
+ %{method_id: String.t(), args: nil},
+ String.t() | nil,
+ true | false,
+ [%{}]
+ ) :: [%{}]
+ def query_function_with_custom_abi(
+ contract_address_hash,
+ %{method_id: method_id, args: nil},
+ from,
+ leave_error_as_map,
+ custom_abi
+ ) do
+ query_function_with_custom_abi(
+ contract_address_hash,
+ %{method_id: method_id, args: []},
+ from,
+ leave_error_as_map,
+ custom_abi
+ )
+ end
+
+ @spec query_function_with_custom_abi(
+ Hash.t(),
+ %{method_id: String.t(), args: [term()]},
+ String.t() | nil,
+ true | false,
+ [%{}]
+ ) :: [
+ %{}
+ ]
+ def query_function_with_custom_abi(
+ contract_address_hash,
+ %{method_id: method_id, args: args},
+ from,
+ leave_error_as_map,
+ custom_abi
+ ) do
+ query_function_with_custom_abi_inner(contract_address_hash, method_id, args, from, leave_error_as_map, custom_abi)
+ end
+
+ @spec query_function_with_custom_abi_inner(Hash.t(), String.t(), [term()], String.t() | nil, true | false, [%{}]) :: [
+ %{}
+ ]
+ defp query_function_with_custom_abi_inner(
+ contract_address_hash,
+ method_id,
+ args,
+ from,
+ leave_error_as_map,
+ custom_abi
+ ) do
+ parsed_abi =
+ custom_abi
+ |> ABI.parse_specification()
+
+ %{outputs: outputs, method_id: method_id} = proccess_abi(parsed_abi, method_id)
+
+ query_contract_and_link_outputs(
+ contract_address_hash,
+ args,
+ from,
+ custom_abi,
+ outputs,
+ method_id,
+ leave_error_as_map
+ )
+ end
+
+ defp proccess_abi([], _method_id), do: nil
defp proccess_abi(abi, method_id) do
function_object = find_function_by_method(abi, method_id)
diff --git a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex
index 25ff8cf129..bf8e9e6780 100644
--- a/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex
+++ b/apps/explorer/lib/explorer/smart_contract/solidity/verifier.ex
@@ -525,9 +525,11 @@ defmodule Explorer.SmartContract.Solidity.Verifier do
Enum.any?(abi, fn el -> el["type"] == "constructor" && el["inputs"] != [] end)
end
- defp parse_boolean("true"), do: true
- defp parse_boolean("false"), do: false
+ def parse_boolean("true"), do: true
+ def parse_boolean("false"), do: false
- defp parse_boolean(true), do: true
- defp parse_boolean(false), do: false
+ def parse_boolean(true), do: true
+ def parse_boolean(false), do: false
+
+ def parse_boolean(_), do: false
end
diff --git a/apps/explorer/lib/explorer/smart_contract/writer.ex b/apps/explorer/lib/explorer/smart_contract/writer.ex
index ba231a6551..2d6c7484be 100644
--- a/apps/explorer/lib/explorer/smart_contract/writer.ex
+++ b/apps/explorer/lib/explorer/smart_contract/writer.ex
@@ -43,8 +43,10 @@ defmodule Explorer.SmartContract.Writer do
(Helper.payable?(function) || Helper.nonpayable?(function))
end
- defp filter_write_functions(abi) do
+ def filter_write_functions(abi) when is_list(abi) do
abi
|> Enum.filter(&write_function?(&1))
end
+
+ def filter_write_functions(_), do: []
end
diff --git a/apps/explorer/lib/explorer/tags/address_tag.ex b/apps/explorer/lib/explorer/tags/address_tag.ex
new file mode 100644
index 0000000000..6174235608
--- /dev/null
+++ b/apps/explorer/lib/explorer/tags/address_tag.ex
@@ -0,0 +1,98 @@
+defmodule Explorer.Tags.AddressTag do
+ @moduledoc """
+ Represents a Tag object.
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+
+ import Ecto.Query,
+ only: [
+ from: 2
+ ]
+
+ alias Explorer.Chain.Address
+ alias Explorer.Repo
+ alias Explorer.Tags.{AddressTag, AddressToTag}
+
+ @typedoc """
+ * `:id` - id of Tag
+ * `:label` - Tag's label
+ * `:label` - Label's display name
+ """
+ @type t :: %AddressTag{
+ label: String.t()
+ }
+
+ schema "address_tags" do
+ field(:label, :string)
+ field(:display_name, :string)
+ has_many(:addresses, Address, foreign_key: :hash)
+ has_many(:tag_id, AddressToTag, foreign_key: :id)
+
+ timestamps()
+ end
+
+ @required_attrs ~w(label display_name)a
+
+ @doc false
+ def changeset(struct, params \\ %{}) do
+ struct
+ |> cast(params, @required_attrs)
+ |> validate_required(@required_attrs)
+ |> unique_constraint(:label, name: :address_tags_label_index)
+ end
+
+ def set_tag(name, display_name) do
+ tag = get_tag(name)
+
+ if tag do
+ tag
+ |> AddressTag.changeset(%{display_name: display_name})
+ |> Repo.update()
+ else
+ %AddressTag{}
+ |> AddressTag.changeset(%{label: name, display_name: display_name})
+ |> Repo.insert()
+ end
+ end
+
+ def get_tag_id(nil), do: nil
+
+ def get_tag_id(label) do
+ query =
+ from(
+ tag in AddressTag,
+ where: tag.label == ^label,
+ select: tag.id
+ )
+
+ query
+ |> Repo.one()
+ end
+
+ def get_tag(nil), do: nil
+
+ def get_tag(label) do
+ query =
+ from(
+ tag in AddressTag,
+ where: tag.label == ^label
+ )
+
+ query
+ |> Repo.one()
+ end
+
+ def get_all_tags do
+ query =
+ from(
+ tag in AddressTag,
+ select: tag
+ )
+
+ query
+ |> Repo.all()
+ end
+end
diff --git a/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex
new file mode 100644
index 0000000000..3256f3f3c2
--- /dev/null
+++ b/apps/explorer/lib/explorer/tags/address_tag_cataloger.ex
@@ -0,0 +1,198 @@
+defmodule Explorer.Tags.AddressTag.Cataloger do
+ @moduledoc """
+ Actualizes address tags.
+ """
+
+ use GenServer
+
+ alias Explorer.EnvVarTranslator
+ alias Explorer.Tags.{AddressTag, AddressToTag}
+ alias Explorer.Validator.MetadataRetriever
+ alias Poison.Parser
+
+ def start_link(_) do
+ GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
+ end
+
+ @impl GenServer
+ def init(args) do
+ send(self(), :fetch_tags)
+
+ {:ok, args}
+ end
+
+ @impl GenServer
+ def handle_info(:fetch_tags, state) do
+ # set tag for every chainlink oracle
+ create_chainlink_oracle_tag()
+
+ create_new_tags()
+
+ send(self(), :bind_addresses)
+
+ {:noreply, state}
+ end
+
+ def handle_info(:bind_addresses, state) do
+ # set validator tag
+ set_validator_tag()
+
+ # set amb bridge mediators tag
+ set_amb_mediators_tag()
+
+ # set omni bridge tag
+ set_omni_tag()
+
+ # set L2 tag
+ set_l2_tag()
+
+ all_tags = AddressTag.get_all_tags()
+
+ all_tags
+ |> Enum.each(fn %{label: tag_name} ->
+ if tag_name !== "validator" && tag_name !== "amb bridge mediators" && tag_name !== "omni bridge" &&
+ tag_name !== "l2" && !String.contains?(tag_name, "chainlink") do
+ env_var_name = "CUSTOM_CONTRACT_ADDRESSES_#{tag_name_to_env_var_part(tag_name)}"
+ set_tag_for_env_var_multiple_addresses(env_var_name, tag_name)
+ end
+ end)
+
+ {:noreply, state}
+ end
+
+ defp tag_name_to_env_var_part(tag_name) do
+ tag_name
+ |> String.upcase()
+ |> String.replace(" ", "_")
+ |> String.replace(".", "_")
+ end
+
+ def create_chainlink_oracle_tag do
+ chainlink_oracles_config = Application.get_env(:block_scout_web, :chainlink_oracles)
+
+ if chainlink_oracles_config do
+ chainlink_oracles_config
+ |> Parser.parse!(%{keys: :atoms!})
+ |> Enum.each(fn %{:name => name, :address => address} ->
+ chainlink_tag_name = "chainlink oracle #{String.downcase(name)}"
+ AddressTag.set_tag(chainlink_tag_name, chainlink_tag_name)
+ tag_id = AddressTag.get_tag_id(chainlink_tag_name)
+ AddressToTag.set_tag_to_addresses(tag_id, [address])
+ end)
+ end
+ end
+
+ defp set_tag_for_multiple_env_var_addresses(env_vars, tag) do
+ addresses =
+ env_vars
+ |> Enum.map(fn env_var ->
+ env_var
+ |> System.get_env("")
+ |> String.downcase()
+ end)
+
+ tag_id = AddressTag.get_tag_id(tag)
+ AddressToTag.set_tag_to_addresses(tag_id, addresses)
+ end
+
+ defp set_tag_for_multiple_env_var_array_addresses(env_vars, tag) do
+ addresses =
+ env_vars
+ |> Enum.reduce([], fn env_var, acc ->
+ env_var
+ |> System.get_env("")
+ |> String.split(",")
+ |> Enum.reduce(acc, fn env_var, acc_inner ->
+ addr =
+ env_var
+ |> String.downcase()
+
+ [addr | acc_inner]
+ end)
+ end)
+
+ tag_id = AddressTag.get_tag_id(tag)
+ AddressToTag.set_tag_to_addresses(tag_id, addresses)
+ end
+
+ def create_new_tags do
+ tags = EnvVarTranslator.map_array_env_var_to_list(:new_tags)
+
+ tags
+ |> Enum.each(fn %{tag: tag_name, title: tag_display_name} ->
+ AddressTag.set_tag(tag_name, tag_display_name)
+ end)
+ end
+
+ defp set_tag_for_env_var_multiple_addresses(env_var, tag) do
+ addresses = env_var_string_array_to_list(env_var)
+
+ tag_id = AddressTag.get_tag_id(tag)
+ AddressToTag.set_tag_to_addresses(tag_id, addresses)
+ end
+
+ defp env_var_string_array_to_list(env_var_array_string) do
+ env_var =
+ env_var_array_string
+ |> System.get_env(nil)
+
+ if env_var do
+ env_var
+ |> String.split(",")
+ |> Enum.map(fn env_var_array_string_item ->
+ env_var_array_string_item
+ |> String.downcase()
+ end)
+ else
+ []
+ end
+ end
+
+ defp set_validator_tag do
+ validators = MetadataRetriever.fetch_validators_list()
+ tag_id = AddressTag.get_tag_id("validator")
+ AddressToTag.set_tag_to_addresses(tag_id, validators)
+ end
+
+ defp set_amb_mediators_tag do
+ set_tag_for_multiple_env_var_array_addresses(
+ ["AMB_BRIDGE_MEDIATORS", "CUSTOM_CONTRACT_ADDRESSES_AMB_BRIDGE_MEDIATORS"],
+ "amb bridge mediators"
+ )
+ end
+
+ defp set_omni_tag do
+ set_tag_for_multiple_env_var_addresses(
+ ["ETH_OMNI_BRIDGE_MEDIATOR", "BSC_OMNI_BRIDGE_MEDIATOR", "POA_OMNI_BRIDGE_MEDIATOR"],
+ "omni bridge"
+ )
+ end
+
+ defp set_l2_tag do
+ set_tag_for_multiple_env_var_addresses(["CUSTOM_CONTRACT_ADDRESSES_AOX"], "l2")
+ end
+
+ def set_chainlink_oracle_tag do
+ chainlink_oracles = chainlink_oracles_list()
+
+ tag_id = AddressTag.get_tag_id("chainlink oracle")
+ AddressToTag.set_tag_to_addresses(tag_id, chainlink_oracles)
+ end
+
+ defp chainlink_oracles_list do
+ chainlink_oracles_config = Application.get_env(:block_scout_web, :chainlink_oracles)
+
+ if chainlink_oracles_config do
+ try do
+ chainlink_oracles_config
+ |> Parser.parse!(%{keys: :atoms!})
+ |> Enum.map(fn %{:name => _name, :address => address} -> address end)
+ rescue
+ _ ->
+ []
+ end
+ else
+ []
+ end
+ end
+end
diff --git a/apps/explorer/lib/explorer/tags/address_to_tag.ex b/apps/explorer/lib/explorer/tags/address_to_tag.ex
new file mode 100644
index 0000000000..26468cf29a
--- /dev/null
+++ b/apps/explorer/lib/explorer/tags/address_to_tag.ex
@@ -0,0 +1,145 @@
+defmodule Explorer.Tags.AddressToTag do
+ @moduledoc """
+ Represents ann Address to Tag relation.
+ """
+
+ use Explorer.Schema
+
+ import Ecto.Changeset
+
+ alias Explorer.{Chain, Repo}
+ alias Explorer.Chain.{Address, Hash}
+ alias Explorer.Tags.{AddressTag, AddressToTag}
+
+ # Notation.import_types(BlockScoutWeb.Schema.Types)
+
+ @typedoc """
+ * `:tag_id` - id of Tag
+ * `:address_hash` - hash of Address
+ """
+ @type t :: %AddressToTag{
+ tag_id: Decimal.t(),
+ address_hash: Hash.Address.t()
+ }
+
+ schema "address_to_tags" do
+ belongs_to(
+ :tag,
+ AddressTag,
+ foreign_key: :tag_id,
+ references: :id,
+ type: :integer
+ )
+
+ belongs_to(
+ :address,
+ Address,
+ foreign_key: :address_hash,
+ references: :hash,
+ type: Hash.Address
+ )
+
+ timestamps()
+ end
+
+ @required_attrs ~w(address_hash tag_id)a
+
+ @doc false
+ def changeset(struct, params \\ %{}) do
+ struct
+ |> cast(params, @required_attrs)
+ |> unique_constraint([:address_hash, :tag_id], name: :address_to_tags_address_hash_tag_id_index)
+ end
+
+ defp get_address_hashes_mapped_to_tag(nil), do: nil
+
+ defp get_address_hashes_mapped_to_tag(tag_id) do
+ query =
+ from(
+ att in AddressToTag,
+ where: att.tag_id == ^tag_id,
+ select: att.address_hash
+ )
+
+ query
+ |> Repo.all()
+ end
+
+ def set_tag_to_addresses(tag_id, address_hash_string_list) do
+ current_address_hashes = get_address_hashes_mapped_to_tag(tag_id)
+
+ if current_address_hashes do
+ current_address_hashes_strings =
+ current_address_hashes
+ |> Enum.map(fn address_hash ->
+ "0x" <> Base.encode16(address_hash.bytes, case: :lower)
+ end)
+
+ current_address_hashes_strings_tuples = MapSet.new(current_address_hashes_strings)
+ new_address_hashes_strings_tuples = MapSet.new(address_hash_string_list)
+
+ all_tuples = MapSet.union(current_address_hashes_strings_tuples, new_address_hashes_strings_tuples)
+
+ addresses_to_delete =
+ all_tuples
+ |> MapSet.difference(new_address_hashes_strings_tuples)
+ |> MapSet.to_list()
+
+ addresses_to_add =
+ all_tuples
+ |> MapSet.difference(current_address_hashes_strings_tuples)
+ |> MapSet.to_list()
+
+ changeset_to_add_list =
+ addresses_to_add
+ |> Enum.map(fn address_hash_string ->
+ with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
+ :ok <- Chain.check_address_exists(address_hash) do
+ %{
+ tag_id: tag_id,
+ address_hash: address_hash,
+ inserted_at: DateTime.utc_now(),
+ updated_at: DateTime.utc_now()
+ }
+ else
+ _ ->
+ nil
+ end
+ end)
+ |> Enum.filter(&(!is_nil(&1)))
+
+ if Enum.count(addresses_to_delete) > 0 do
+ delete_query_base =
+ from(
+ att in AddressToTag,
+ where: att.tag_id == ^tag_id
+ )
+
+ delete_query =
+ delete_query_base
+ |> where_addresses(addresses_to_delete)
+
+ Repo.delete_all(delete_query)
+ end
+
+ Repo.insert_all(AddressToTag, changeset_to_add_list,
+ on_conflict: :nothing,
+ conflict_target: [:address_hash, :tag_id]
+ )
+ end
+ end
+
+ defp where_addresses(query, addresses_to_delete) do
+ addresses_to_delete
+ |> Enum.reduce(query, fn address_hash_string, acc ->
+ case Chain.string_to_address_hash(address_hash_string) do
+ {:ok, address_hash} ->
+ acc
+ |> where(address_hash: ^address_hash)
+
+ _ ->
+ acc
+ end
+ end)
+ end
+end
diff --git a/apps/explorer/lib/explorer/third_party_integrations/airtable.ex b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex
new file mode 100644
index 0000000000..a2aba9c664
--- /dev/null
+++ b/apps/explorer/lib/explorer/third_party_integrations/airtable.ex
@@ -0,0 +1,56 @@
+defmodule Explorer.ThirdPartyIntegrations.AirTable do
+ @moduledoc """
+ Module is responsible for submitting requests for public tags to AirTable
+ """
+ require Logger
+
+ alias Ecto.Changeset
+ alias Explorer.Account.PublicTagsRequest
+ alias Explorer.Repo
+ alias HTTPoison.Response
+
+ def submit({:ok, %PublicTagsRequest{} = new_request} = input) do
+ if Mix.env() == :test do
+ new_request
+ |> PublicTagsRequest.changeset(%{request_id: "123"})
+ |> Repo.account_repo().update()
+
+ input
+ else
+ api_key = Application.get_env(:explorer, __MODULE__)[:api_key]
+ headers = [{"Authorization", "Bearer #{api_key}"}, {"Content-Type", "application/json"}]
+ url = Application.get_env(:explorer, __MODULE__)[:table_url]
+
+ body = %{
+ "typecast" => true,
+ "records" => [%{"fields" => PublicTagsRequest.to_map(new_request)}]
+ }
+
+ request = HTTPoison.post(url, Jason.encode!(body), headers, [])
+
+ case request do
+ {:ok, %Response{body: body, status_code: 200}} ->
+ request_id = Enum.at(Jason.decode!(body)["records"], 0)["fields"]["request_id"]
+
+ new_request
+ |> PublicTagsRequest.changeset(%{request_id: request_id})
+ |> Repo.account_repo().update()
+
+ input
+
+ error ->
+ Logger.error(fn -> ["Error while submitting AirTable entry", inspect(error)] end)
+
+ {:error,
+ %{
+ (%PublicTagsRequest{}
+ |> PublicTagsRequest.changeset_without_constraints(PublicTagsRequest.to_map(new_request))
+ |> Changeset.add_error(:full_name, "AirTable error. Please try again later"))
+ | action: :insert
+ }}
+ end
+ end
+ end
+
+ def submit(error), do: error
+end
diff --git a/apps/explorer/lib/explorer/validator/metadata_retriever.ex b/apps/explorer/lib/explorer/validator/metadata_retriever.ex
index 99fb83db79..3a5d13eefa 100644
--- a/apps/explorer/lib/explorer/validator/metadata_retriever.ex
+++ b/apps/explorer/lib/explorer/validator/metadata_retriever.ex
@@ -15,7 +15,7 @@ defmodule Explorer.Validator.MetadataRetriever do
end)
end
- defp fetch_validators_list do
+ def fetch_validators_list do
# b7ab4db5 = keccak256(getValidators())
case Reader.query_contract(
config(:validators_contract_address),
diff --git a/apps/explorer/lib/explorer/vault.ex b/apps/explorer/lib/explorer/vault.ex
new file mode 100644
index 0000000000..70fc1ed17d
--- /dev/null
+++ b/apps/explorer/lib/explorer/vault.ex
@@ -0,0 +1,22 @@
+defmodule Explorer.Vault do
+ @moduledoc """
+ Module responsible for encrypt/decrypt GenServer initialization
+ """
+ use Cloak.Vault, otp_app: :explorer
+
+ @impl GenServer
+ def init(config) do
+ config =
+ Keyword.put(config, :ciphers,
+ default: {Cloak.Ciphers.AES.GCM, tag: "AES.GCM.V1", key: decode_env!("ACCOUNT_CLOAK_KEY")}
+ )
+
+ {:ok, config}
+ end
+
+ defp decode_env!(var) do
+ env = if Mix.env() == :test, do: "+fh7IElJfA61+vMMw8rW9SBJFHmhVL1DLpKE22qUJgw=", else: System.get_env(var)
+
+ Base.decode64!(env || "")
+ end
+end
diff --git a/apps/explorer/lib/release_tasks.ex b/apps/explorer/lib/release_tasks.ex
index 8d5a1b486c..1fcdc7fca8 100644
--- a/apps/explorer/lib/release_tasks.ex
+++ b/apps/explorer/lib/release_tasks.ex
@@ -14,7 +14,7 @@ defmodule Explorer.ReleaseTasks do
:ecto_sql
]
- @repos Application.compile_env(:blockscout, :ecto_repos, [Explorer.Repo])
+ @repos Application.compile_env(:blockscout, :ecto_repos, [Explorer.Repo, Explorer.Repo.Account])
def create_and_migrate do
start_services()
diff --git a/apps/explorer/mix.exs b/apps/explorer/mix.exs
index 8f27ecc6b7..b9217ddd0a 100644
--- a/apps/explorer/mix.exs
+++ b/apps/explorer/mix.exs
@@ -24,7 +24,8 @@ defmodule Explorer.Mixfile do
dialyzer: :test
],
start_permanent: Mix.env() == :prod,
- version: "4.1.8"
+ version: "4.1.8",
+ xref: [exclude: [BlockScoutWeb.WebRouter.Helpers]]
]
end
@@ -56,6 +57,8 @@ defmodule Explorer.Mixfile do
# Type `mix help deps` for examples and options.
defp deps do
[
+ {:bamboo, "~> 2.2.0"},
+ {:mime, "~> 1.4"},
{:bcrypt_elixir, "~> 3.0"},
# benchmark optimizations
{:benchee, "~> 1.1.0", only: :test},
@@ -87,7 +90,7 @@ defmodule Explorer.Mixfile do
{:mock, "~> 0.3.0", only: [:test], runtime: false},
{:mox, "~> 1.0", only: [:test]},
{:phoenix_html, "== 3.0.4"},
- {:poison, "~> 5.0.0"},
+ {:poison, "~> 4.0.1"},
{:nimble_csv, "~> 1.1"},
{:postgrex, ">= 0.0.0"},
# For compatibility with `prometheus_process_collector`, which hasn't been updated yet
@@ -110,7 +113,9 @@ defmodule Explorer.Mixfile do
{:timex, "~> 3.7.1"},
{:con_cache, "~> 1.0"},
{:tesla, "~> 1.4.4"},
- {:cbor, "~> 1.0"}
+ {:cbor, "~> 1.0"},
+ {:cloak_ecto, "~> 1.2.0"},
+ {:redix, "~> 1.1"}
]
end
diff --git a/apps/explorer/priv/account/migrations/20211031164954_create_account_identities.exs b/apps/explorer/priv/account/migrations/20211031164954_create_account_identities.exs
new file mode 100644
index 0000000000..fb571247f0
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20211031164954_create_account_identities.exs
@@ -0,0 +1,13 @@
+defmodule Explorer.Repo.Account.Migrations.CreateAccountIdentities do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_identities) do
+ add(:uid, :string)
+
+ timestamps()
+ end
+
+ create(unique_index(:account_identities, [:uid]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20211105114502_create_account_watchlists.exs b/apps/explorer/priv/account/migrations/20211105114502_create_account_watchlists.exs
new file mode 100644
index 0000000000..2e7c93f3cc
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20211105114502_create_account_watchlists.exs
@@ -0,0 +1,14 @@
+defmodule Explorer.Repo.Account.Migrations.CreateAccountWatchlists do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_watchlists) do
+ add(:name, :string, default: "default")
+ add(:identity_id, references(:account_identities, on_delete: :delete_all))
+
+ timestamps()
+ end
+
+ create(index(:account_watchlists, [:identity_id]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20211105130907_create_account_watchlist_addresses.exs b/apps/explorer/priv/account/migrations/20211105130907_create_account_watchlist_addresses.exs
new file mode 100644
index 0000000000..ef51c1850e
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20211105130907_create_account_watchlist_addresses.exs
@@ -0,0 +1,28 @@
+defmodule Explorer.Repo.Account.Migrations.CreateAccountWatchlistAddresses do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_watchlist_addresses) do
+ add(:name, :string)
+ add(:address_hash, :bytea, null: false)
+ add(:watchlist_id, references(:account_watchlists, on_delete: :delete_all))
+ add(:watch_coin_input, :boolean, default: true)
+ add(:watch_coin_output, :boolean, default: true)
+ add(:watch_erc_20_input, :boolean, default: true)
+ add(:watch_erc_20_output, :boolean, default: true)
+ add(:watch_erc_721_input, :boolean, default: true)
+ add(:watch_erc_721_output, :boolean, default: true)
+ add(:watch_erc_1155_input, :boolean, default: true)
+ add(:watch_erc_1155_output, :boolean, default: true)
+ add(:notify_email, :boolean, default: true)
+ add(:notify_epns, :boolean, default: false)
+ add(:notify_feed, :boolean, default: true)
+ add(:notify_inapp, :boolean, default: false)
+
+ timestamps()
+ end
+
+ create(index(:account_watchlist_addresses, [:watchlist_id]))
+ create(index(:account_watchlist_addresses, [:address_hash]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20211127212336_create_account_watchlist_notifications.exs b/apps/explorer/priv/account/migrations/20211127212336_create_account_watchlist_notifications.exs
new file mode 100644
index 0000000000..7d4af566fe
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20211127212336_create_account_watchlist_notifications.exs
@@ -0,0 +1,31 @@
+defmodule Explorer.Repo.Account.Migrations.CreateAccountWatchlistNotifications do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_watchlist_notifications) do
+ add(:watchlist_address_id, references(:account_watchlist_addresses, on_delete: :delete_all))
+
+ add(:transaction_hash, :bytea)
+
+ add(:from_address_hash, :bytea)
+
+ add(:to_address_hash, :bytea)
+
+ add(:direction, :string)
+ add(:name, :string)
+ add(:type, :string)
+ add(:method, :string)
+ add(:block_number, :integer)
+ add(:amount, :decimal)
+ add(:tx_fee, :decimal)
+ add(:viewed_at, :utc_datetime_usec)
+
+ timestamps(null: false, type: :utc_datetime_usec)
+ end
+
+ create(index(:account_watchlist_notifications, [:watchlist_address_id]))
+ create(index(:account_watchlist_notifications, [:transaction_hash]))
+ create(index(:account_watchlist_notifications, [:from_address_hash]))
+ create(index(:account_watchlist_notifications, [:to_address_hash]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20211205220414_add_email_and_name_to_account_identity.exs b/apps/explorer/priv/account/migrations/20211205220414_add_email_and_name_to_account_identity.exs
new file mode 100644
index 0000000000..0633e0dbae
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20211205220414_add_email_and_name_to_account_identity.exs
@@ -0,0 +1,10 @@
+defmodule Explorer.Repo.Account.Migrations.AddEmailToAccountIdentity do
+ use Ecto.Migration
+
+ def change do
+ alter table(:account_identities) do
+ add(:email, :string)
+ add(:name, :string)
+ end
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220212222222_create_account_tag_addresses.exs b/apps/explorer/priv/account/migrations/20220212222222_create_account_tag_addresses.exs
new file mode 100644
index 0000000000..df0074041e
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220212222222_create_account_tag_addresses.exs
@@ -0,0 +1,17 @@
+defmodule Explorer.Repo.Account.Migrations.CreateAccountTagAddresses do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_tag_addresses) do
+ add(:name, :string)
+ add(:identity_id, references(:account_identities, on_delete: :delete_all))
+
+ add(:address_hash, :bytea, null: false)
+
+ timestamps()
+ end
+
+ create(index(:account_tag_addresses, [:identity_id]))
+ create(index(:account_tag_addresses, [:address_hash]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220313133333_create_account_tag_transactions.exs b/apps/explorer/priv/account/migrations/20220313133333_create_account_tag_transactions.exs
new file mode 100644
index 0000000000..ba6fa338b9
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220313133333_create_account_tag_transactions.exs
@@ -0,0 +1,17 @@
+defmodule Explorer.Repo.Account.Migrations.CreateAccountTagTransactions do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_tag_transactions) do
+ add(:name, :string)
+ add(:identity_id, references(:account_identities, on_delete: :delete_all))
+
+ add(:tx_hash, :bytea, null: false)
+
+ timestamps()
+ end
+
+ create(index(:account_tag_transactions, [:identity_id]))
+ create(index(:account_tag_transactions, [:tx_hash]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220324213333_add_subject_to_watchlist_notifications.exs b/apps/explorer/priv/account/migrations/20220324213333_add_subject_to_watchlist_notifications.exs
new file mode 100644
index 0000000000..6ff8a359ba
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220324213333_add_subject_to_watchlist_notifications.exs
@@ -0,0 +1,9 @@
+defmodule Explorer.Repo.Account.Migrations.AddSubjectToWatchlistNotifications do
+ use Ecto.Migration
+
+ def change do
+ alter table(:account_watchlist_notifications) do
+ add(:subject, :string, null: true)
+ end
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220407134152_add_api_keys_and_plans_tables.exs b/apps/explorer/priv/account/migrations/20220407134152_add_api_keys_and_plans_tables.exs
new file mode 100644
index 0000000000..11f23ffd61
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220407134152_add_api_keys_and_plans_tables.exs
@@ -0,0 +1,33 @@
+defmodule Explorer.Repo.Account.Migrations.AddApiKeysAndPlansTables do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_api_plans, primary_key: false) do
+ add(:id, :serial, null: false, primary_key: true)
+ add(:max_req_per_second, :smallint)
+ add(:name, :string, null: false)
+
+ timestamps()
+ end
+
+ create(unique_index(:account_api_plans, [:id, :max_req_per_second, :name]))
+
+ execute(
+ "INSERT INTO account_api_plans (id, max_req_per_second, name, inserted_at, updated_at) VALUES (1, 10, 'Free Plan', NOW(), NOW());"
+ )
+
+ create table(:account_api_keys, primary_key: false) do
+ add(:identity_id, references(:account_identities, column: :id, on_delete: :delete_all), null: false)
+ add(:name, :string, null: false)
+ add(:value, :uuid, null: false, primary_key: true)
+
+ timestamps()
+ end
+
+ alter table(:account_identities) do
+ add(:plan_id, references(:account_api_plans, column: :id), default: 1)
+ end
+
+ create(index(:account_api_keys, [:identity_id]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220510094118_add_custom_abis_table.exs b/apps/explorer/priv/account/migrations/20220510094118_add_custom_abis_table.exs
new file mode 100644
index 0000000000..a248c79458
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220510094118_add_custom_abis_table.exs
@@ -0,0 +1,18 @@
+defmodule Explorer.Repo.Account.Migrations.AddCustomAbisTable do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_custom_abis, primary_key: false) do
+ add(:id, :serial, null: false, primary_key: true)
+ add(:identity_id, references(:account_identities, column: :id, on_delete: :delete_all), null: false)
+ add(:name, :string, null: false)
+ add(:address_hash, :bytea, null: false)
+ add(:abi, :jsonb, null: false)
+
+ timestamps()
+ end
+
+ create(unique_index(:account_custom_abis, [:identity_id, :address_hash]))
+ create(index(:account_custom_abis, [:identity_id]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220606194836_add_account_public_tags_requests.exs b/apps/explorer/priv/account/migrations/20220606194836_add_account_public_tags_requests.exs
new file mode 100644
index 0000000000..dd0a9896b0
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220606194836_add_account_public_tags_requests.exs
@@ -0,0 +1,23 @@
+defmodule Explorer.Repo.Account.Migrations.AddAccountPublicTagsRequests do
+ use Ecto.Migration
+
+ def change do
+ create table(:account_public_tags_requests) do
+ add(:identity_id, references(:account_identities))
+ add(:full_name, :string)
+ add(:email, :string)
+ add(:company, :string)
+ add(:website, :string)
+ add(:tags, :string)
+ add(:addresses, :text)
+ add(:description, :text)
+ add(:additional_comment, :string)
+ add(:request_type, :string)
+ add(:is_owner, :boolean)
+ add(:remove_reason, :text)
+ add(:request_id, :string)
+
+ timestamps()
+ end
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220620182600_add_account_identity_fields.exs b/apps/explorer/priv/account/migrations/20220620182600_add_account_identity_fields.exs
new file mode 100644
index 0000000000..1c7b13535f
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220620182600_add_account_identity_fields.exs
@@ -0,0 +1,10 @@
+defmodule Explorer.Repo.Account.Migrations.AddAccountIdentityFields do
+ use Ecto.Migration
+
+ def change do
+ alter table("account_identities") do
+ add(:nickname, :string, null: true)
+ add(:avatar, :text, null: true)
+ end
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220624142547_add_unique_constraints.exs b/apps/explorer/priv/account/migrations/20220624142547_add_unique_constraints.exs
new file mode 100644
index 0000000000..53293983d4
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220624142547_add_unique_constraints.exs
@@ -0,0 +1,9 @@
+defmodule Explorer.Repo.Account.Migrations.AddUniqueConstraints do
+ use Ecto.Migration
+
+ def change do
+ create(unique_index(:account_tag_addresses, [:identity_id, :address_hash]))
+ create(unique_index(:account_tag_transactions, [:identity_id, :tx_hash]))
+ create(unique_index(:account_watchlist_addresses, [:watchlist_id, :address_hash]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220705195240_migrate_public_tags_addresses_to_array.exs b/apps/explorer/priv/account/migrations/20220705195240_migrate_public_tags_addresses_to_array.exs
new file mode 100644
index 0000000000..1282e2166c
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220705195240_migrate_public_tags_addresses_to_array.exs
@@ -0,0 +1,34 @@
+defmodule Explorer.Repo.Account.Migrations.MigratePublicTagsAddressesToArray do
+ use Ecto.Migration
+
+ def change do
+ alter table(:account_public_tags_requests) do
+ add(:addresses_duplicate, {:array, :bytea})
+ end
+
+ execute("""
+ CREATE OR REPLACE FUNCTION convert(text[]) RETURNS bytea[] AS $$
+ DECLARE
+ s bytea[] := ARRAY[]::bytea[];
+ x text;
+ BEGIN
+ FOREACH x IN ARRAY $1
+ LOOP
+ s := array_append(s, decode(replace(x, '0x', ''), 'hex'));
+ END LOOP;
+ RETURN s;
+ END;
+ $$ LANGUAGE plpgsql;
+ """)
+
+ execute("""
+ UPDATE account_public_tags_requests set addresses_duplicate = convert(string_to_array(addresses, ';'))
+ """)
+
+ alter table(:account_public_tags_requests) do
+ remove(:addresses)
+ end
+
+ rename(table(:account_public_tags_requests), :addresses_duplicate, to: :addresses)
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220706114430_encrypt_account_data.exs b/apps/explorer/priv/account/migrations/20220706114430_encrypt_account_data.exs
new file mode 100644
index 0000000000..0328025982
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220706114430_encrypt_account_data.exs
@@ -0,0 +1,60 @@
+defmodule Explorer.Repo.Account.Migrations.EncryptAccountData do
+ use Ecto.Migration
+
+ def change do
+ alter table(:account_identities) do
+ add(:encrypted_uid, :binary)
+ add(:uid_hash, :binary)
+ add(:encrypted_email, :binary)
+ add(:encrypted_name, :binary)
+ add(:encrypted_nickname, :binary, null: true)
+ add(:encrypted_avatar, :binary, null: true)
+ end
+
+ # unused because we dont have personal watchlists, only autogenerated `default` for each identity
+ # alter table(:account_watchlists) do
+ # add(:encrypted_name, :binary)
+ # end
+
+ alter table(:account_custom_abis) do
+ add(:address_hash_hash, :binary)
+ add(:encrypted_address_hash, :binary)
+ add(:encrypted_name, :binary)
+ end
+
+ alter table(:account_tag_addresses) do
+ add(:address_hash_hash, :binary)
+ add(:encrypted_name, :binary)
+ add(:encrypted_address_hash, :binary)
+ end
+
+ alter table(:account_tag_transactions) do
+ add(:tx_hash_hash, :binary)
+ add(:encrypted_name, :binary)
+ add(:encrypted_tx_hash, :binary)
+ end
+
+ alter table(:account_watchlist_addresses) do
+ add(:address_hash_hash, :binary)
+ add(:encrypted_name, :binary)
+ add(:encrypted_address_hash, :binary)
+ end
+
+ alter table(:account_watchlist_notifications) do
+ add(:encrypted_name, :binary)
+ add(:encrypted_subject, :binary, null: true)
+ add(:encrypted_from_address_hash, :binary)
+ add(:encrypted_to_address_hash, :binary)
+ add(:encrypted_transaction_hash, :binary)
+ add(:subject_hash, :binary, null: true)
+ add(:from_address_hash_hash, :binary, null: true)
+ add(:to_address_hash_hash, :binary, null: true)
+ add(:transaction_hash_hash, :binary, null: true)
+ end
+
+ alter table(:account_public_tags_requests) do
+ add(:encrypted_email, :binary)
+ add(:encrypted_full_name, :binary)
+ end
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220706153506_remove_unencrypted_fields.exs b/apps/explorer/priv/account/migrations/20220706153506_remove_unencrypted_fields.exs
new file mode 100644
index 0000000000..b887ac9f79
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220706153506_remove_unencrypted_fields.exs
@@ -0,0 +1,79 @@
+defmodule Explorer.Repo.Account.Migrations.RemoveUnencryptedFields do
+ use Ecto.Migration
+
+ def change do
+ alter table(:account_identities) do
+ remove(:uid)
+ remove(:email)
+ remove(:name)
+ remove(:nickname)
+ remove(:avatar)
+ end
+
+ rename(table(:account_identities), :encrypted_uid, to: :uid)
+ rename(table(:account_identities), :encrypted_email, to: :email)
+ rename(table(:account_identities), :encrypted_name, to: :name)
+ rename(table(:account_identities), :encrypted_nickname, to: :nickname)
+ rename(table(:account_identities), :encrypted_avatar, to: :avatar)
+
+ # unused because we dont have personal watchlists, only autogenerated `default` for each identity
+ # alter table(:account_watchlists) do
+ # remove(:name)
+ # end
+ # rename(table(:account_watchlists), :encrypted_name, to: :name)
+
+ alter table(:account_custom_abis) do
+ remove(:address_hash)
+ remove(:name)
+ end
+
+ rename(table(:account_custom_abis), :encrypted_address_hash, to: :address_hash)
+ rename(table(:account_custom_abis), :encrypted_name, to: :name)
+
+ alter table(:account_tag_addresses) do
+ remove(:address_hash)
+ remove(:name)
+ end
+
+ rename(table(:account_tag_addresses), :encrypted_address_hash, to: :address_hash)
+ rename(table(:account_tag_addresses), :encrypted_name, to: :name)
+
+ alter table(:account_tag_transactions) do
+ remove(:tx_hash)
+ remove(:name)
+ end
+
+ rename(table(:account_tag_transactions), :encrypted_tx_hash, to: :tx_hash)
+ rename(table(:account_tag_transactions), :encrypted_name, to: :name)
+
+ alter table(:account_watchlist_addresses) do
+ remove(:address_hash)
+ remove(:name)
+ end
+
+ rename(table(:account_watchlist_addresses), :encrypted_address_hash, to: :address_hash)
+ rename(table(:account_watchlist_addresses), :encrypted_name, to: :name)
+
+ alter table(:account_watchlist_notifications) do
+ remove(:to_address_hash)
+ remove(:from_address_hash)
+ remove(:transaction_hash)
+ remove(:subject)
+ remove(:name)
+ end
+
+ rename(table(:account_watchlist_notifications), :encrypted_name, to: :name)
+ rename(table(:account_watchlist_notifications), :encrypted_subject, to: :subject)
+ rename(table(:account_watchlist_notifications), :encrypted_from_address_hash, to: :from_address_hash)
+ rename(table(:account_watchlist_notifications), :encrypted_to_address_hash, to: :to_address_hash)
+ rename(table(:account_watchlist_notifications), :encrypted_transaction_hash, to: :transaction_hash)
+
+ alter table(:account_public_tags_requests) do
+ remove(:full_name)
+ remove(:email)
+ end
+
+ rename(table(:account_public_tags_requests), :encrypted_full_name, to: :full_name)
+ rename(table(:account_public_tags_requests), :encrypted_email, to: :email)
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220706211444_set_new_indexes.exs b/apps/explorer/priv/account/migrations/20220706211444_set_new_indexes.exs
new file mode 100644
index 0000000000..26abb92abe
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220706211444_set_new_indexes.exs
@@ -0,0 +1,43 @@
+defmodule Explorer.Repo.Account.Migrations.SetNewIndexes do
+ use Ecto.Migration
+
+ def change do
+ drop_if_exists(unique_index(:account_tag_addresses, [:identity_id, :address_hash]))
+ drop_if_exists(unique_index(:account_tag_transactions, [:identity_id, :tx_hash]))
+ drop_if_exists(unique_index(:account_watchlist_addresses, [:watchlist_id, :address_hash]))
+ drop_if_exists(unique_index(:account_custom_abis, [:identity_id, :address_hash]))
+
+ drop_if_exists(index(:account_watchlist_notifications, [:transaction_hash]))
+ drop_if_exists(index(:account_watchlist_notifications, [:from_address_hash]))
+ drop_if_exists(index(:account_watchlist_notifications, [:to_address_hash]))
+
+ drop_if_exists(unique_index(:account_identities, [:uid]))
+
+ drop_if_exists(index(:account_tag_addresses, [:address_hash]))
+ drop_if_exists(index(:account_tag_transactions, [:tx_hash]))
+
+ drop_if_exists(index(:account_watchlist_addresses, [:address_hash]))
+
+ create(unique_index(:account_tag_addresses, [:identity_id, :address_hash_hash]))
+ create(unique_index(:account_tag_transactions, [:identity_id, :tx_hash_hash]))
+
+ create(
+ unique_index(:account_watchlist_addresses, [:watchlist_id, :address_hash_hash],
+ name: "unique_watchlist_id_address_hash_hash_index"
+ )
+ )
+
+ create(unique_index(:account_custom_abis, [:identity_id, :address_hash_hash]))
+
+ create(index(:account_watchlist_notifications, [:transaction_hash_hash]))
+ create(index(:account_watchlist_notifications, [:from_address_hash_hash]))
+ create(index(:account_watchlist_notifications, [:to_address_hash_hash]))
+
+ create(unique_index(:account_identities, [:uid_hash]))
+
+ create(index(:account_tag_addresses, [:address_hash_hash]))
+ create(index(:account_tag_transactions, [:tx_hash_hash]))
+
+ create(index(:account_watchlist_addresses, [:address_hash_hash]))
+ end
+end
diff --git a/apps/explorer/priv/account/migrations/20220905195203_remove_guardian_tokens.exs b/apps/explorer/priv/account/migrations/20220905195203_remove_guardian_tokens.exs
new file mode 100644
index 0000000000..2c76df8099
--- /dev/null
+++ b/apps/explorer/priv/account/migrations/20220905195203_remove_guardian_tokens.exs
@@ -0,0 +1,7 @@
+defmodule Explorer.Repo.Account.Migrations.RemoveGuardianTokens do
+ use Ecto.Migration
+
+ def change do
+ drop_if_exists(table("guardian_tokens"))
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20210219080523_add_tags.exs b/apps/explorer/priv/repo/migrations/20210219080523_add_tags.exs
new file mode 100644
index 0000000000..79e83f808c
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20210219080523_add_tags.exs
@@ -0,0 +1,24 @@
+defmodule Explorer.Repo.Migrations.AddTags do
+ use Ecto.Migration
+
+ def change do
+ create table(:address_tags, primary_key: false) do
+ add(:id, :serial, null: false)
+ add(:label, :string, null: false)
+
+ timestamps()
+ end
+
+ create(unique_index(:address_tags, [:id]))
+ create(unique_index(:address_tags, [:label]))
+
+ create table(:address_to_tags) do
+ add(:address_hash, references(:addresses, column: :hash, type: :bytea), null: false)
+ add(:tag_id, references(:address_tags, column: :id, type: :serial), null: false)
+
+ timestamps()
+ end
+
+ create(unique_index(:address_to_tags, [:address_hash, :tag_id]))
+ end
+end
diff --git a/apps/explorer/priv/repo/migrations/20211210184136_add_display_name_to_address_tag.exs b/apps/explorer/priv/repo/migrations/20211210184136_add_display_name_to_address_tag.exs
new file mode 100644
index 0000000000..f07af29e10
--- /dev/null
+++ b/apps/explorer/priv/repo/migrations/20211210184136_add_display_name_to_address_tag.exs
@@ -0,0 +1,15 @@
+defmodule Explorer.Repo.Migrations.AddDisplayNameToAddressTag do
+ use Ecto.Migration
+
+ def up do
+ alter table(:address_tags) do
+ add(:display_name, :string, null: true)
+ end
+ end
+
+ def down do
+ alter table(:address_tags) do
+ remove(:display_name)
+ end
+ end
+end
diff --git a/apps/explorer/test/explorer/account/notify/email_test.exs b/apps/explorer/test/explorer/account/notify/email_test.exs
new file mode 100644
index 0000000000..34e5a57ad6
--- /dev/null
+++ b/apps/explorer/test/explorer/account/notify/email_test.exs
@@ -0,0 +1,124 @@
+defmodule Explorer.Account.Notify.EmailTest do
+ use ExUnit.Case
+
+ alias Explorer.Chain.Address
+ alias Explorer.Chain.Transaction
+
+ alias Explorer.Account.{
+ Identity,
+ Watchlist,
+ WatchlistAddress,
+ WatchlistNotification
+ }
+
+ import Explorer.Chain,
+ only: [
+ string_to_address_hash: 1,
+ string_to_transaction_hash: 1
+ ]
+
+ import Explorer.Account.Notifier.Email,
+ only: [compose: 2]
+
+ setup do
+ host = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:host]
+ path = Application.get_env(:block_scout_web, BlockScoutWeb.Endpoint)[:url][:path]
+
+ Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, url: [host: "localhost", path: "/"])
+
+ Application.put_env(:explorer, Explorer.Account,
+ sendgrid: [
+ sender: "noreply@blockscout.com",
+ template: "d-666"
+ ]
+ )
+
+ :ok
+
+ on_exit(fn ->
+ Application.put_env(:block_scout_web, BlockScoutWeb.Endpoint, url: [host: host, path: path])
+ end)
+ end
+
+ describe "composing email" do
+ test "compose_email" do
+ {:ok, tx_hash} = string_to_transaction_hash("0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d")
+
+ {:ok, from_hash} = string_to_address_hash("0x092D537737E767Dae48c28aE509f34094496f030")
+
+ {:ok, to_hash} = string_to_address_hash("0xE1F4dd38f00B0D8D4d2b4B5010bE53F2A0b934E5")
+ to_address = %Address{hash: to_hash}
+
+ identity = %Identity{
+ uid: "foo|bar",
+ name: "John Snow",
+ email: "john@blockscout.com"
+ }
+
+ watchlist = %Watchlist{identity: identity}
+
+ watchlist_address = %WatchlistAddress{
+ name: "wallet",
+ watchlist: watchlist,
+ address_hash: to_hash,
+ watch_coin_input: true,
+ watch_coin_output: true,
+ notify_email: true
+ }
+
+ watchlist_notification = %WatchlistNotification{
+ watchlist_address: watchlist_address,
+ transaction_hash: tx_hash,
+ from_address_hash: from_hash,
+ to_address_hash: to_hash,
+ direction: "incoming",
+ method: "transfer",
+ block_number: 24_121_177,
+ amount: Decimal.new(1),
+ tx_fee: Decimal.new(210_000),
+ name: "wallet",
+ type: "COIN"
+ }
+
+ assert compose(watchlist_notification, watchlist_address) ==
+ %Bamboo.Email{
+ assigns: %{},
+ attachments: [],
+ bcc: nil,
+ blocked: false,
+ cc: nil,
+ from: "noreply@blockscout.com",
+ headers: %{},
+ html_body: nil,
+ private: %{
+ send_grid_template: %{
+ dynamic_template_data: %{
+ "address_hash" => "0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5",
+ "address_name" => "wallet",
+ "address_url" => "https://localhost//address/0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5",
+ "amount" => Decimal.new(1),
+ "block_number" => 24_121_177,
+ "block_url" => "https://localhost/block/24121177",
+ "direction" => "received at",
+ "from_address_hash" => "0x092d537737e767dae48c28ae509f34094496f030",
+ "from_url" => "https://localhost//address/0x092d537737e767dae48c28ae509f34094496f030",
+ "method" => "transfer",
+ "name" => "wallet",
+ "to_address_hash" => "0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5",
+ "to_url" => "https://localhost//address/0xe1f4dd38f00b0d8d4d2b4b5010be53f2a0b934e5",
+ "transaction_hash" => "0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d",
+ "transaction_url" =>
+ "https://localhost//tx/0x5d5ff210261f1b2d6e4af22ea494f428f9997d4ab614a629d4f1390004b3e80d",
+ "tx_fee" => Decimal.new(210_000),
+ "username" => "John Snow"
+ },
+ template_id: "d-666"
+ }
+ },
+ subject: nil,
+ text_body: nil,
+ to: "john@blockscout.com"
+ }
+ end
+ end
+end
diff --git a/apps/explorer/test/explorer/account/notify/notify_test.exs b/apps/explorer/test/explorer/account/notify/notify_test.exs
new file mode 100644
index 0000000000..4d169f04d6
--- /dev/null
+++ b/apps/explorer/test/explorer/account/notify/notify_test.exs
@@ -0,0 +1,91 @@
+defmodule Explorer.Account.Notify.NotifyTest do
+ # use ExUnit.Case
+ use Explorer.DataCase
+
+ import Explorer.Factory
+
+ alias Explorer.Account.Notifier.Notify
+ alias Explorer.Account.{WatchlistAddress, WatchlistNotification}
+ alias Explorer.Chain
+ alias Explorer.Chain.{Transaction, Wei}
+ alias Explorer.Repo
+
+ setup do
+ Application.put_env(:explorer, Explorer.Account,
+ sendgrid: [
+ sender: "noreply@blockscout.com",
+ template: "d-666"
+ ]
+ )
+
+ Application.put_env(:explorer, Explorer.Mailer,
+ adapter: Bamboo.SendGridAdapter,
+ api_key: "SENDGRID_API_KEY"
+ )
+
+ Application.put_env(
+ :ueberauth,
+ Ueberauth,
+ providers: [
+ auth0: {
+ Ueberauth.Strategy.Auth0,
+ [callback_url: "callback.url"]
+ }
+ ],
+ logout_url: "logout.url",
+ logout_return_to_url: "return.url"
+ )
+ end
+
+ describe "notify" do
+ test "when address not in any watchlist" do
+ tx = with_block(insert(:transaction))
+
+ notify = Notify.call([tx])
+
+ wn =
+ WatchlistNotification
+ |> first
+ |> Repo.account_repo().one()
+
+ assert notify == [[:ok]]
+
+ assert wn == nil
+ end
+
+ test "when address apears in watchlist" do
+ wa =
+ %WatchlistAddress{address_hash: address_hash} =
+ build(:account_watchlist_address)
+ |> Repo.account_repo().insert!()
+
+ _watchlist_address = Repo.preload(wa, watchlist: :identity)
+
+ tx =
+ %Transaction{
+ from_address: _from_address,
+ to_address: _to_address,
+ block_number: _block_number,
+ hash: _tx_hash
+ } = with_block(insert(:transaction, to_address: %Chain.Address{hash: address_hash}))
+
+ {_, fee} = Chain.fee(tx, :gwei)
+ amount = Wei.to(tx.value, :ether)
+ notify = Notify.call([tx])
+
+ wn =
+ WatchlistNotification
+ |> first
+ |> Repo.account_repo().one()
+
+ assert notify == [[:ok]]
+
+ assert wn.amount == amount
+ assert wn.direction == "incoming"
+ assert wn.method == "transfer"
+ assert wn.subject == "Coin transaction"
+ assert wn.tx_fee == fee
+ assert wn.type == "COIN"
+ end
+ end
+end
diff --git a/apps/explorer/test/explorer/account/notify/summary_test.exs b/apps/explorer/test/explorer/account/notify/summary_test.exs
new file mode 100644
index 0000000000..611316f193
--- /dev/null
+++ b/apps/explorer/test/explorer/account/notify/summary_test.exs
@@ -0,0 +1,273 @@
+defmodule Explorer.Account.Notify.SummaryTest do
+ use Explorer.DataCase
+
+ import Explorer.Factory
+
+ alias Explorer.Account.Notifier.Summary
+ alias Explorer.Chain
+ alias Explorer.Chain.{TokenTransfer, Transaction, Wei}
+ alias Explorer.Repo
+
+ describe "call" do
+ test "Coin transaction" do
+ tx =
+ %Transaction{
+ from_address: from_address,
+ to_address: to_address,
+ block_number: block_number,
+ hash: tx_hash
+ } = with_block(insert(:transaction))
+
+ {_, fee} = Chain.fee(tx, :gwei)
+ amount = Wei.to(tx.value, :ether)
+
+ assert Summary.process(tx) == [
+ %Summary{
+ amount: amount,
+ block_number: block_number,
+ from_address_hash: from_address.hash,
+ method: "transfer",
+ name: "POA",
+ subject: "Coin transaction",
+ to_address_hash: to_address.hash,
+ transaction_hash: tx_hash,
+ tx_fee: fee,
+ type: "COIN"
+ }
+ ]
+ end
+
+ test "Pending Coin transaction (w/o block)" do
+ tx =
+ %Transaction{
+ from_address: _from_address,
+ to_address: _to_address,
+ hash: _tx_hash
+ } = insert(:transaction)
+
+ assert Summary.process(tx) == []
+ end
+
+ test "Contract creation transaction" do
+ address = insert(:address)
+ contract_address = insert(:contract_address)
+
+ block = insert(:block)
+
+ tx =
+ %Transaction{
+ from_address: _from_address,
+ block_number: _block_number,
+ hash: tx_hash
+ } =
+ :transaction
+ |> insert(from_address: address, to_address: nil)
+ |> with_contract_creation(contract_address)
+ |> with_block(block)
+
+ {_, fee} = Chain.fee(tx, :gwei)
+ amount = Wei.to(tx.value, :ether)
+
+ assert Summary.process(tx) == [
+ %Summary{
+ amount: amount,
+ block_number: block.number,
+ from_address_hash: address.hash,
+ method: "contract_creation",
+ name: "POA",
+ subject: "Contract creation",
+ to_address_hash: contract_address.hash,
+ transaction_hash: tx_hash,
+ tx_fee: fee,
+ type: "COIN"
+ }
+ ]
+ end
+
+ test "ERC-20 Token transfer" do
+ tx =
+ %Transaction{
+ from_address: _from_address,
+ to_address: _to_address,
+ block_number: _block_number,
+ hash: _tx_hash
+ } = with_block(insert(:transaction))
+
+ transfer =
+ %TokenTransfer{
+ amount: _amount,
+ block_number: block_number,
+ from_address: from_address,
+ to_address: to_address,
+ token: token
+ } =
+ :token_transfer
+ |> insert(transaction: tx)
+ |> Repo.preload([
+ :token
+ ])
+
+ {_, fee} = Chain.fee(tx, :gwei)
+
+ token_decimals = Decimal.to_integer(token.decimals)
+
+ decimals = Decimal.new(Integer.pow(10, token_decimals))
+
+ amount = Decimal.div(transfer.amount, decimals)
+
+ assert Summary.process(transfer) == [
+ %Summary{
+ amount: amount,
+ block_number: block_number,
+ from_address_hash: from_address.hash,
+ method: "transfer",
+ name: "Infinite Token",
+ subject: "ERC-20",
+ to_address_hash: to_address.hash,
+ transaction_hash: tx.hash,
+ tx_fee: fee,
+ type: "ERC-20"
+ }
+ ]
+ end
+
+ test "ERC-721 Token transfer" do
+ token = insert(:token, type: "ERC-721")
+
+ tx =
+ %Transaction{
+ from_address: _from_address,
+ to_address: _to_address,
+ block_number: _block_number,
+ hash: _tx_hash
+ } = with_block(insert(:transaction))
+
+ transfer =
+ %TokenTransfer{
+ amount: _amount,
+ block_number: block_number,
+ from_address: from_address,
+ to_address: to_address
+ } =
+ :token_transfer
+ |> insert(
+ transaction: tx,
+ token_id: 42,
+ token_contract_address: token.contract_address
+ )
+ |> Repo.preload([
+ :token
+ ])
+
+ {_, fee} = Chain.fee(tx, :gwei)
+
+ assert Summary.process(transfer) == [
+ %Summary{
+ amount: 0,
+ block_number: block_number,
+ from_address_hash: from_address.hash,
+ method: "transfer",
+ name: "Infinite Token",
+ subject: "42",
+ to_address_hash: to_address.hash,
+ transaction_hash: tx.hash,
+ tx_fee: fee,
+ type: "ERC-721"
+ }
+ ]
+ end
+
+ test "ERC-1155 single Token transfer" do
+ token = insert(:token, type: "ERC-1155")
+
+ tx =
+ %Transaction{
+ from_address: _from_address,
+ to_address: _to_address,
+ block_number: _block_number,
+ hash: _tx_hash
+ } = with_block(insert(:transaction))
+
+ transfer =
+ %TokenTransfer{
+ amount: _amount,
+ block_number: block_number,
+ from_address: from_address,
+ to_address: to_address
+ } =
+ :token_transfer
+ |> insert(
+ transaction: tx,
+ token_id: 42,
+ token_contract_address: token.contract_address
+ )
+ |> Repo.preload([
+ :token
+ ])
+
+ {_, fee} = Chain.fee(tx, :gwei)
+
+ assert Summary.process(transfer) == [
+ %Summary{
+ amount: 0,
+ block_number: block_number,
+ from_address_hash: from_address.hash,
+ method: "transfer",
+ name: "Infinite Token",
+ subject: "42",
+ to_address_hash: to_address.hash,
+ transaction_hash: tx.hash,
+ tx_fee: fee,
+ type: "ERC-1155"
+ }
+ ]
+ end
+
+ test "ERC-1155 multiple Token transfer" do
+ token = insert(:token, type: "ERC-1155")
+
+ tx =
+ %Transaction{
+ from_address: _from_address,
+ to_address: _to_address,
+ block_number: _block_number,
+ hash: _tx_hash
+ } = with_block(insert(:transaction))
+
+ transfer =
+ %TokenTransfer{
+ amount: _amount,
+ block_number: block_number,
+ from_address: from_address,
+ to_address: to_address
+ } =
+ :token_transfer
+ |> insert(
+ transaction: tx,
+ token_id: nil,
+ token_ids: [23, 42],
+ token_contract_address: token.contract_address
+ )
+ |> Repo.preload([
+ :token
+ ])
+
+ {_, fee} = Chain.fee(tx, :gwei)
+
+ assert Summary.process(transfer) == [
+ %Summary{
+ amount: 0,
+ block_number: block_number,
+ from_address_hash: from_address.hash,
+ method: "transfer",
+ name: "Infinite Token",
+ subject: "23, 42",
+ to_address_hash: to_address.hash,
+ transaction_hash: tx.hash,
+ tx_fee: fee,
+ type: "ERC-1155"
+ }
+ ]
+ end
+ end
+end
diff --git a/apps/explorer/test/support/data_case.ex b/apps/explorer/test/support/data_case.ex
index e12dd24a82..da18760983 100644
--- a/apps/explorer/test/support/data_case.ex
+++ b/apps/explorer/test/support/data_case.ex
@@ -34,9 +34,11 @@ defmodule Explorer.DataCase do
ExVCR.Config.cassette_library_dir("test/support/fixture/vcr_cassettes")
:ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo)
+ :ok = Ecto.Adapters.SQL.Sandbox.checkout(Explorer.Repo.Account)
unless tags[:async] do
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, {:shared, self()})
+ Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, {:shared, self()})
end
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.BlockNumber.child_id())
diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex
index 34f4692240..7967d83b8d 100644
--- a/apps/explorer/test/support/factory.ex
+++ b/apps/explorer/test/support/factory.ex
@@ -4,10 +4,20 @@ defmodule Explorer.Factory do
require Ecto.Query
import Ecto.Query
+ import Explorer.Chain, only: [hash_to_lower_case_string: 1]
import Kernel, except: [+: 2]
- alias Bcrypt
- alias Explorer.Accounts.{User, UserContact}
+ alias Explorer.Account.{
+ Identity,
+ Watchlist,
+ WatchlistAddress
+ }
+
+ alias Explorer.Accounts.{
+ User,
+ UserContact
+ }
+
alias Explorer.Admin.Administrator
alias Explorer.Chain.Block.{EmissionReward, Range, Reward}
@@ -37,6 +47,116 @@ defmodule Explorer.Factory do
alias Explorer.Market.MarketHistory
alias Explorer.Repo
+ alias Ueberauth.Strategy.Auth0
+ alias Ueberauth.Auth.Info
+ alias Ueberauth.Auth
+
+ def account_identity_factory do
+ %Identity{
+ uid: sequence("github|"),
+ email: sequence(:email, &"me-#{&1}@blockscout.com"),
+ name: sequence("John")
+ }
+ end
+
+ def auth_factory do
+ %Auth{
+ info: %Info{
+ birthday: nil,
+ description: nil,
+ email: sequence(:email, &"test_user-#{&1}@blockscout.com"),
+ first_name: nil,
+ image: sequence("https://example.com/avatar/test_user"),
+ last_name: nil,
+ location: nil,
+ name: sequence("User Test"),
+ nickname: sequence("test_user"),
+ phone: nil,
+ urls: %{profile: nil, website: nil}
+ },
+ provider: :auth0,
+ strategy: Auth0,
+ uid: sequence("blockscout|000")
+ }
+ end
+
+ def watchlist_address_factory do
+ %{
+ "address_hash" => to_string(build(:address).hash),
+ "name" => sequence("test"),
+ "notification_settings" => %{
+ "native" => %{
+ "incoming" => random_bool(),
+ "outcoming" => random_bool()
+ },
+ "ERC-20" => %{
+ "incoming" => random_bool(),
+ "outcoming" => random_bool()
+ },
+ "ERC-721" => %{
+ "incoming" => random_bool(),
+ "outcoming" => random_bool()
+ }
+ },
+ "notification_methods" => %{
+ "email" => random_bool()
+ }
+ }
+ end
+
+ def custom_abi_factory do
+ contract_address_hash = to_string(insert(:contract_address).hash)
+
+ %{"contract_address_hash" => contract_address_hash, "name" => sequence("test"), "abi" => contract_code_info().abi}
+ end
+
+ def public_tags_request_factory do
+ %{
+ "full_name" => sequence("full name"),
+ "email" => sequence(:email, &"test_user-#{&1}@blockscout.com"),
+ "tags" => Enum.join(Enum.map(1..Enum.random(1..2), fn _ -> sequence("Tag") end), ";"),
+ "website" => sequence("website"),
+ "additional_comment" => sequence("additional_comment"),
+ "addresses" => Enum.map(1..Enum.random(1..10), fn _ -> to_string(build(:address).hash) end),
+ "company" => sequence("company"),
+ "is_owner" => random_bool()
+ }
+ end
+
+ def account_watchlist_factory do
+ %Watchlist{
+ identity: build(:account_identity)
+ }
+ end
+
+ def tag_address_factory do
+ %{"name" => sequence("name"), "address_hash" => to_string(build(:address).hash)}
+ end
+
+ def tag_transaction_factory do
+ %{"name" => sequence("name"), "transaction_hash" => to_string(insert(:transaction).hash)}
+ end
+
+ def account_watchlist_address_factory do
+ hash = build(:address).hash
+
+ %WatchlistAddress{
+ name: "wallet",
+ watchlist: build(:account_watchlist),
+ address_hash: hash,
+ address_hash_hash: hash_to_lower_case_string(hash),
+ watch_coin_input: true,
+ watch_coin_output: true,
+ watch_erc_20_input: true,
+ watch_erc_20_output: true,
+ watch_erc_721_input: true,
+ watch_erc_721_output: true,
+ watch_erc_1155_input: true,
+ watch_erc_1155_output: true,
+ notify_email: true
+ }
+ end
+
def address_factory do
%Address{
hash: address_hash()
@@ -780,4 +900,6 @@ defmodule Explorer.Factory do
user: build(:user)
}
end
+
+ def random_bool, do: Enum.random([true, false])
end
diff --git a/apps/explorer/test/test_helper.exs b/apps/explorer/test/test_helper.exs
index 50847130f8..418ea17ea0 100644
--- a/apps/explorer/test/test_helper.exs
+++ b/apps/explorer/test/test_helper.exs
@@ -12,6 +12,7 @@ ExUnit.start()
{:ok, _} = Application.ensure_all_started(:ex_machina)
Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo, :auto)
+Ecto.Adapters.SQL.Sandbox.mode(Explorer.Repo.Account, :auto)
Mox.defmock(Explorer.ExchangeRates.Source.TestSource, for: Explorer.ExchangeRates.Source)
Mox.defmock(Explorer.KnownTokens.Source.TestSource, for: Explorer.KnownTokens.Source)
diff --git a/apps/indexer/config/runtime/test.exs b/apps/indexer/config/runtime/test.exs
index b360f5f9b9..476dedd943 100644
--- a/apps/indexer/config/runtime/test.exs
+++ b/apps/indexer/config/runtime/test.exs
@@ -1,3 +1,5 @@
+import Config
+
variant =
if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do
"parity"
diff --git a/config/config.exs b/config/config.exs
index ffdbc80032..8ec1d34312 100644
--- a/config/config.exs
+++ b/config/config.exs
@@ -35,7 +35,8 @@ config :logger,
{LoggerFileBackend, :pending_transactions_to_refetch},
{LoggerFileBackend, :empty_blocks_to_refetch},
{LoggerFileBackend, :api},
- {LoggerFileBackend, :block_import_timings}
+ {LoggerFileBackend, :block_import_timings},
+ {LoggerFileBackend, :account}
]
config :logger, :console,
diff --git a/config/dev.exs b/config/dev.exs
index 21e6c9fd40..323212e107 100644
--- a/config/dev.exs
+++ b/config/dev.exs
@@ -8,3 +8,8 @@ config :logger, :ecto,
path: Path.absname("logs/dev/ecto.log")
config :logger, :error, path: Path.absname("logs/dev/error.log")
+
+config :logger, :account,
+ level: :debug,
+ path: Path.absname("logs/dev/account.log"),
+ metadata_filter: [fetcher: :account]
diff --git a/config/prod.exs b/config/prod.exs
index 2175810d04..1d9be54e3f 100644
--- a/config/prod.exs
+++ b/config/prod.exs
@@ -12,3 +12,9 @@ config :logger, :ecto,
config :logger, :error,
path: Path.absname("logs/prod/error.log"),
rotate: %{max_bytes: 52_428_800, keep: 19}
+
+config :logger, :account,
+ level: :info,
+ path: Path.absname("logs/prod/account.log"),
+ rotate: %{max_bytes: 52_428_800, keep: 19},
+ metadata_filter: [fetcher: :account]
diff --git a/config/runtime.exs b/config/runtime.exs
index 59c3e150dc..f661efbe40 100644
--- a/config/runtime.exs
+++ b/config/runtime.exs
@@ -35,6 +35,17 @@ config :block_scout_web, :footer,
### BlockScout Web ###
######################
+# 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")
+
+# 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")
+
config :block_scout_web,
version: System.get_env("BLOCKSCOUT_VERSION"),
release_link: System.get_env("RELEASE_LINK"),
@@ -308,6 +319,21 @@ config :explorer, Explorer.SmartContract.RustVerifierInterface,
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")
+
+config :explorer, Explorer.Mailer,
+ 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")
+ ]
+
###############
### Indexer ###
###############
diff --git a/config/runtime/dev.exs b/config/runtime/dev.exs
index e7348f1e5d..8de612decb 100644
--- a/config/runtime/dev.exs
+++ b/config/runtime/dev.exs
@@ -48,8 +48,8 @@ database_api_url =
pool_size =
if System.get_env("DATABASE_READ_ONLY_API_URL"),
- do: String.to_integer(System.get_env("POOL_SIZE", "40")),
- else: String.to_integer(System.get_env("POOL_SIZE", "50"))
+ do: String.to_integer(System.get_env("POOL_SIZE", "30")),
+ else: String.to_integer(System.get_env("POOL_SIZE", "40"))
# Configure your database
config :explorer, Explorer.Repo,
@@ -61,10 +61,7 @@ config :explorer, Explorer.Repo,
database_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: database
hostname_api = if System.get_env("DATABASE_READ_ONLY_API_URL"), do: nil, else: hostname
-pool_size_api =
- if System.get_env("DATABASE_READ_ONLY_API_URL"),
- do: String.to_integer(System.get_env("POOL_SIZE_API", "50")),
- else: String.to_integer(System.get_env("POOL_SIZE_API", "10"))
+pool_size_api = String.to_integer(System.get_env("POOL_SIZE_API", "10"))
# Configure API database
config :explorer, Explorer.Repo.Replica1,
@@ -73,6 +70,20 @@ config :explorer, Explorer.Repo.Replica1,
url: database_api_url,
pool_size: pool_size_api
+database_account_url = System.get_env("ACCOUNT_DATABASE_URL") || System.get_env("DATABASE_URL")
+
+pool_size_account = String.to_integer(System.get_env("ACCOUNT_POOL_SIZE", "10"))
+
+database_account = if System.get_env("ACCOUNT_DATABASE_URL"), do: nil, else: database
+hostname_account = if System.get_env("ACCOUNT_DATABASE_URL"), do: nil, else: hostname
+
+# Configure Account database
+config :explorer, Explorer.Repo.Account,
+ database: database_account,
+ hostname: hostname_account,
+ url: database_account_url,
+ pool_size: pool_size_account
+
variant =
if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do
"ganache"
diff --git a/config/runtime/prod.exs b/config/runtime/prod.exs
index 8a5c8f24cd..0e4e58f81a 100644
--- a/config/runtime/prod.exs
+++ b/config/runtime/prod.exs
@@ -49,6 +49,19 @@ config :explorer, Explorer.Repo.Replica1,
pool_size: pool_size_api,
ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true")
+database_account_url =
+ if System.get_env("ACCOUNT_DATABASE_URL"),
+ do: System.get_env("ACCOUNT_DATABASE_URL"),
+ else: System.get_env("DATABASE_URL")
+
+pool_size_account = String.to_integer(System.get_env("ACCOUNT_POOL_SIZE", "50"))
+
+# Configures Account database
+config :explorer, Explorer.Repo.Account,
+ url: database_account_url,
+ pool_size: pool_size_account,
+ ssl: String.equivalent?(System.get_env("ECTO_USE_SSL") || "true", "true")
+
variant =
if is_nil(System.get_env("ETHEREUM_JSONRPC_VARIANT")) do
"parity"
diff --git a/config/runtime/test.exs b/config/runtime/test.exs
index 6f604ba05e..38cd15e0df 100644
--- a/config/runtime/test.exs
+++ b/config/runtime/test.exs
@@ -1,3 +1,5 @@
+import Config
+
######################
### BlockScout Web ###
######################
diff --git a/docker-compose/docker-compose-no-build-ganache.yml b/docker-compose/docker-compose-no-build-ganache.yml
index 619e8104b0..62a007a2b0 100644
--- a/docker-compose/docker-compose-no-build-ganache.yml
+++ b/docker-compose/docker-compose-no-build-ganache.yml
@@ -1,6 +1,10 @@
version: '3.8'
services:
+ redis_db:
+ image: 'redis:alpine'
+ command: redis-server
+
db:
image: postgres:14
restart: always
@@ -16,6 +20,7 @@ services:
depends_on:
- db
- smart-contract-verifier
+ - redis_db
image: blockscout/blockscout:${DOCKER_TAG:-latest}
restart: always
container_name: 'blockscout'
diff --git a/docker-compose/docker-compose-no-build-geth.yml b/docker-compose/docker-compose-no-build-geth.yml
index ec6e64d248..d4f59f9a4d 100644
--- a/docker-compose/docker-compose-no-build-geth.yml
+++ b/docker-compose/docker-compose-no-build-geth.yml
@@ -1,6 +1,10 @@
version: '3.8'
services:
+ redis_db:
+ image: 'redis:alpine'
+ command: redis-server
+
db:
image: postgres:14
restart: always
@@ -18,6 +22,7 @@ services:
depends_on:
- db
- smart-contract-verifier
+ - redis_db
image: blockscout/blockscout:${DOCKER_TAG:-latest}
restart: always
container_name: 'blockscout'
diff --git a/docker-compose/docker-compose-no-build-hardhat-network.yml b/docker-compose/docker-compose-no-build-hardhat-network.yml
index 9a74d44f10..aaab30fd1e 100644
--- a/docker-compose/docker-compose-no-build-hardhat-network.yml
+++ b/docker-compose/docker-compose-no-build-hardhat-network.yml
@@ -1,6 +1,10 @@
version: '3.8'
services:
+ redis_db:
+ image: 'redis:alpine'
+ command: redis-server
+
db:
image: postgres:14
restart: always
@@ -16,6 +20,7 @@ services:
depends_on:
- db
- smart-contract-verifier
+ - redis_db
image: blockscout/blockscout:${DOCKER_TAG:-latest}
restart: always
container_name: 'blockscout'
diff --git a/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml b/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml
index 9a272acb57..cde891f1ad 100644
--- a/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml
+++ b/docker-compose/docker-compose-no-build-open-ethereum-nethermind.yml
@@ -1,6 +1,10 @@
version: '3.8'
services:
+ redis_db:
+ image: 'redis:alpine'
+ command: redis-server
+
db:
image: postgres:14
restart: always
@@ -18,6 +22,7 @@ services:
depends_on:
- db
- smart-contract-verifier
+ - redis_db
image: blockscout/blockscout:${DOCKER_TAG:-latest}
restart: always
container_name: 'blockscout'
diff --git a/docker-compose/docker-compose-no-rust-verification.yml b/docker-compose/docker-compose-no-rust-verification.yml
index 74c3b77642..2f809bcfe2 100644
--- a/docker-compose/docker-compose-no-rust-verification.yml
+++ b/docker-compose/docker-compose-no-rust-verification.yml
@@ -1,6 +1,10 @@
version: '3.8'
services:
+ redis_db:
+ image: 'redis:alpine'
+ command: redis-server
+
db:
image: postgres:14
restart: always
@@ -15,6 +19,7 @@ services:
blockscout:
depends_on:
- db
+ - redis_db
image: blockscout/blockscout:${DOCKER_TAG:-latest}
build:
context: ..
diff --git a/docker-compose/docker-compose.yml b/docker-compose/docker-compose.yml
index 0a57ceb4ca..e9043545f3 100644
--- a/docker-compose/docker-compose.yml
+++ b/docker-compose/docker-compose.yml
@@ -1,6 +1,10 @@
version: '3.8'
services:
+ redis_db:
+ image: 'redis:alpine'
+ command: redis-server
+
db:
image: postgres:14
restart: always
@@ -16,6 +20,7 @@ services:
depends_on:
- db
- smart-contract-verifier
+ - redis_db
image: blockscout/blockscout:${DOCKER_TAG:-latest}
build:
context: ..
diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env
index cd2a3ffdb0..6b157a2011 100644
--- a/docker-compose/envs/common-blockscout.env
+++ b/docker-compose/envs/common-blockscout.env
@@ -19,8 +19,8 @@ BLOCKSCOUT_PROTOCOL=
# SECRET_KEY_BASE=
# CHECK_ORIGIN=
PORT=4000
-# COIN=
COIN_NAME=
+# COIN=
# METADATA_CONTRACT=
# VALIDATORS_CONTRACT=
# KEYS_MANAGER_CONTRACT=
@@ -124,3 +124,19 @@ 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/
+# DATABASE_READ_ONLY_API_URL=
+# ACCOUNT_DATABASE_URL=
+# ACCOUNT_POOL_SIZE=
+# ACCOUNT_AUTH0_DOMAIN=
+# ACCOUNT_AUTH0_CLIENT_ID=
+# ACCOUNT_AUTH0_CLIENT_SECRET=
+# ACCOUNT_AUTH0_LOGOUT_URL=
+# ACCOUNT_AUTH0_LOGOUT_RETURN_URL=
+# ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL=
+# ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY=
+# ACCOUNT_SENDGRID_API_KEY=
+# ACCOUNT_SENDGRID_SENDER=
+# ACCOUNT_SENDGRID_TEMPLATE=
+ACCOUNT_CLOAK_KEY=
+ACCOUNT_ENABLED=false
+ACCOUNT_REDIS_URL=redis://redis_db:6379
\ No newline at end of file
diff --git a/docker/Makefile b/docker/Makefile
index f16843d91e..2ef104427c 100644
--- a/docker/Makefile
+++ b/docker/Makefile
@@ -367,9 +367,6 @@ endif
ifdef API_RATE_LIMIT_WHITELISTED_IPS
BLOCKSCOUT_CONTAINER_PARAMS += -e 'API_RATE_LIMIT_WHITELISTED_IPS=$(API_RATE_LIMIT_WHITELISTED_IPS)'
endif
-ifdef COIN_NAME
- BLOCKSCOUT_CONTAINER_PARAMS += -e 'COIN_NAME=$(COIN_NAME)'
-endif
ifdef INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER
BLOCKSCOUT_CONTAINER_PARAMS += -e 'INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER=$(INDEXER_DISABLE_PENDING_TRANSACTIONS_FETCHER)'
endif
@@ -466,7 +463,54 @@ endif
ifdef RUST_VERIFICATION_SERVICE_URL
BLOCKSCOUT_CONTAINER_PARAMS += -e 'RUST_VERIFICATION_SERVICE_URL=$(RUST_VERIFICATION_SERVICE_URL)'
endif
-
+ifdef ACCOUNT_ENABLED
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_ENABLED=$(ACCOUNT_ENABLED)'
+endif
+ifdef ACCOUNT_REDIS_URL
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_REDIS_URL=$(ACCOUNT_REDIS_URL)'
+endif
+ifdef COIN_NAME
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'COIN_NAME=$(COIN_NAME)'
+endif
+ifdef ACCOUNT_AUTH0_DOMAIN
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_DOMAIN=$(ACCOUNT_AUTH0_DOMAIN)'
+endif
+ifdef ACCOUNT_AUTH0_CLIENT_ID
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_CLIENT_ID=$(ACCOUNT_AUTH0_CLIENT_ID)'
+endif
+ifdef ACCOUNT_AUTH0_CLIENT_SECRET
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_CLIENT_SECRET=$(ACCOUNT_AUTH0_CLIENT_SECRET)'
+endif
+ifdef ACCOUNT_AUTH0_LOGOUT_RETURN_URL
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_LOGOUT_RETURN_URL=$(ACCOUNT_AUTH0_LOGOUT_RETURN_URL)'
+endif
+ifdef ACCOUNT_AUTH0_LOGOUT_URL
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_AUTH0_LOGOUT_URL=$(ACCOUNT_AUTH0_LOGOUT_URL)'
+endif
+ifdef ACCOUNT_SENDGRID_API_KEY
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_SENDGRID_API_KEY=$(ACCOUNT_SENDGRID_API_KEY)'
+endif
+ifdef ACCOUNT_SENDGRID_SENDER
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_SENDGRID_SENDER=$(ACCOUNT_SENDGRID_SENDER)'
+endif
+ifdef ACCOUNT_SENDGRID_TEMPLATE
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_SENDGRID_TEMPLATE=$(ACCOUNT_SENDGRID_TEMPLATE)'
+endif
+ifdef ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL=$(ACCOUNT_PUBLIC_TAGS_AIRTABLE_URL)'
+endif
+ifdef ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY=$(ACCOUNT_PUBLIC_TAGS_AIRTABLE_API_KEY)'
+endif
+ifdef ACCOUNT_DATABASE_URL
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_DATABASE_URL=$(ACCOUNT_DATABASE_URL)'
+endif
+ifdef ACCOUNT_POOL_SIZE
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_POOL_SIZE=$(ACCOUNT_POOL_SIZE)'
+endif
+ifdef ACCOUNT_CLOAK_KEY
+ BLOCKSCOUT_CONTAINER_PARAMS += -e 'ACCOUNT_CLOAK_KEY=$(ACCOUNT_CLOAK_KEY)'
+endif
HAS_BLOCKSCOUT_IMAGE := $(shell docker images | grep -sw "${BS_CONTAINER_IMAGE} ")
build:
diff --git a/mix.lock b/mix.lock
index ebd7454167..0a53157e37 100644
--- a/mix.lock
+++ b/mix.lock
@@ -4,16 +4,20 @@
"absinthe_plug": {:git, "https://github.com/blockscout/absinthe_plug.git", "c435d43f316769e1beee1dbe500b623124c96785", [tag: "1.5.3"]},
"absinthe_relay": {:hex, :absinthe_relay, "1.5.2", "cfb8aed70f4e4c7718d3f1c212332d2ea728f17c7fc0f68f1e461f0f5f0c4b9a", [:mix], [{:absinthe, "~> 1.5.0 or ~> 1.6.0 or ~> 1.7.0", [hex: :absinthe, repo: "hexpm", optional: false]}, {:ecto, "~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "0587ee913afa31512e1457a5064ee88427f8fe7bcfbeeecd41c71d9cff0b62b6"},
"accept": {:hex, :accept, "0.3.5", "b33b127abca7cc948bbe6caa4c263369abf1347cfa9d8e699c6d214660f10cd1", [:rebar3], [], "hexpm", "11b18c220bcc2eab63b5470c038ef10eb6783bcb1fcdb11aa4137defa5ac1bb8"},
+ "bamboo": {:hex, :bamboo, "2.2.0", "f10a406d2b7f5123eb1f02edfa043c259db04b47ab956041f279eaac776ef5ce", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8c3b14ba7d2f40cb4be04128ed1e2aff06d91d9413d38bafb4afccffa3ade4fc"},
"bcrypt_elixir": {:hex, :bcrypt_elixir, "3.0.1", "9be815469e6bfefec40fa74658ecbbe6897acfb57614df1416eeccd4903f602c", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "486bb95efb645d1efc6794c1ddd776a186a9a713abf06f45708a6ce324fb96cf"},
"benchee": {:hex, :benchee, "1.1.0", "f3a43817209a92a1fade36ef36b86e1052627fd8934a8b937ac9ab3a76c43062", [:mix], [{:deep_merge, "~> 1.0", [hex: :deep_merge, repo: "hexpm", optional: false]}, {:statistex, "~> 1.0", [hex: :statistex, repo: "hexpm", optional: false]}], "hexpm", "7da57d545003165a012b587077f6ba90b89210fd88074ce3c60ce239eb5e6d93"},
"benchee_csv": {:hex, :benchee_csv, "1.0.0", "0b3b9223290bfcb8003552705bec9bcf1a89b4a83b70bd686e45295c264f3d16", [:mix], [{:benchee, ">= 0.99.0 and < 2.0.0", [hex: :benchee, repo: "hexpm", optional: false]}, {:csv, "~> 2.0", [hex: :csv, repo: "hexpm", optional: false]}], "hexpm", "cdefb804c021dcf7a99199492026584be9b5a21d6644ac0d01c81c5d97c520d5"},
"binary": {:hex, :binary, "0.0.5", "20d816f7274ea34f1b673b4cff2fdb9ebec9391a7a68c349070d515c66b1b2cf", [:mix], [], "hexpm", "ee1e9ebcab703a4e24db554957fbb540642fe9327eb9e295cb3f07dd7c11ddb2"},
"briefly": {:git, "https://github.com/CargoSense/briefly.git", "1dd66ee19ca84ed60f4eca47fee59227ba960fb7", []},
"bunt": {:hex, :bunt, "0.2.1", "e2d4792f7bc0ced7583ab54922808919518d0e57ee162901a16a1b6664ef3b14", [:mix], [], "hexpm", "a330bfb4245239787b15005e66ae6845c9cd524a288f0d141c148b02603777a5"},
+ "bureaucrat": {:hex, :bureaucrat, "0.2.9", "d98e4d2b9bdbf22e4a45c2113ce8b38b5b63278506c6ff918e3b943a4355d85b", [:mix], [{:inflex, ">= 1.10.0", [hex: :inflex, repo: "hexpm", optional: false]}, {:phoenix, ">= 1.2.0", [hex: :phoenix, repo: "hexpm", optional: true]}, {:plug, ">= 1.0.0", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 1.5 or ~> 2.0 or ~> 3.0 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "111c8dd84382a62e1026ae011d592ceee918553e5203fe8448d9ba6ccbdfff7d"},
"bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"},
"cbor": {:hex, :cbor, "1.0.0", "35d33a26f6420ce3d2d01c0b1463a748b34c537d5609fc40116daf3666700d36", [:mix], [], "hexpm", "cc5e21e0fa5a0330715a3806c67bc294f8b65d07160f751b5bd6058bed1962ac"},
"certifi": {:hex, :certifi, "2.9.0", "6f2a475689dd47f19fb74334859d460a2dc4e3252a3324bd2111b8f0429e7e21", [:rebar3], [], "hexpm", "266da46bdb06d6c6d35fde799bcb28d36d985d424ad7c08b5bb48f5b5cdd4641"},
"cldr_utils": {:hex, :cldr_utils, "2.19.1", "5a7bcd2f2fd432c548e494e850bba8a9e838f1b10202f682ea1d9809d74eff31", [:mix], [{:castore, "~> 0.1", [hex: :castore, repo: "hexpm", optional: true]}, {:certifi, "~> 2.5", [hex: :certifi, repo: "hexpm", optional: true]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "fbd10f79363e70f3d893ab21e195f444ca87c2c80120b5911761491da4489620"},
+ "cloak": {:hex, :cloak, "1.1.2", "7e0006c2b0b98d976d4f559080fabefd81f0e0a50a3c4b621f85ceeb563e80bb", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "940d5ac4fcd51b252930fd112e319ea5ae6ab540b722f3ca60a85666759b9585"},
+ "cloak_ecto": {:hex, :cloak_ecto, "1.2.0", "e86a3df3bf0dc8980f70406bcb0af2858bac247d55494d40bc58a152590bd402", [:mix], [{:cloak, "~> 1.1.1", [hex: :cloak, repo: "hexpm", optional: false]}, {:ecto, "~> 3.0", [hex: :ecto, repo: "hexpm", optional: false]}], "hexpm", "8bcc677185c813fe64b786618bd6689b1707b35cd95acaae0834557b15a0c62f"},
"coerce": {:hex, :coerce, "1.0.1", "211c27386315dc2894ac11bc1f413a0e38505d808153367bd5c6e75a4003d096", [:mix], [], "hexpm", "b44a691700f7a1a15b4b7e2ff1fa30bebd669929ac8aa43cffe9e2f8bf051cf1"},
"combine": {:hex, :combine, "0.10.0", "eff8224eeb56498a2af13011d142c5e7997a80c8f5b97c499f84c841032e429f", [:mix], [], "hexpm", "1b1dbc1790073076580d0d1d64e42eae2366583e7aecd455d1215b0d16f2451b"},
"comeonin": {:hex, :comeonin, "5.3.3", "2c564dac95a35650e9b6acfe6d2952083d8a08e4a89b93a481acb552b325892e", [:mix], [], "hexpm", "3e38c9c2cb080828116597ca8807bb482618a315bfafd98c90bc22a821cc84df"},
@@ -64,7 +68,9 @@
"html_entities": {:hex, :html_entities, "0.5.2", "9e47e70598da7de2a9ff6af8758399251db6dbb7eebe2b013f2bbd2515895c3c", [:mix], [], "hexpm", "c53ba390403485615623b9531e97696f076ed415e8d8058b1dbaa28181f4fdcc"},
"httpoison": {:hex, :httpoison, "1.8.2", "9eb9c63ae289296a544842ef816a85d881d4a31f518a0fec089aaa744beae290", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "2bb350d26972e30c96e2ca74a1aaf8293d61d0742ff17f01e0279fef11599921"},
"idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~>0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"},
+ "inflex": {:hex, :inflex, "2.1.0", "a365cf0821a9dacb65067abd95008ca1b0bb7dcdd85ae59965deef2aa062924c", [:mix], [], "hexpm", "14c17d05db4ee9b6d319b0bff1bdf22aa389a25398d1952c7a0b5f3d93162dd8"},
"jason": {:hex, :jason, "1.3.0", "fa6b82a934feb176263ad2df0dbd91bf633d4a46ebfdffea0c8ae82953714946", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "53fc1f51255390e0ec7e50f9cb41e751c260d065dcba2bf0d08dc51a4002c2ac"},
+ "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"},
"jsx": {:hex, :jsx, "2.8.3", "a05252d381885240744d955fbe3cf810504eb2567164824e19303ea59eef62cf", [:mix, :rebar3], [], "hexpm", "fc3499fed7a726995aa659143a248534adc754ebd16ccd437cd93b649a95091f"},
"junit_formatter": {:hex, :junit_formatter, "3.3.1", "c729befb848f1b9571f317d2fefa648e9d4869befc4b2980daca7c1edc468e40", [:mix], [], "hexpm", "761fc5be4b4c15d8ba91a6dafde0b2c2ae6db9da7b8832a55b5a1deb524da72b"},
"libsecp256k1": {:hex, :libsecp256k1, "0.1.10", "d27495e2b9851c7765129b76c53b60f5e275bd6ff68292c50536bf6b8d091a4d", [:make, :mix], [{:mix_erlang_tasks, "0.1.0", [hex: :mix_erlang_tasks, repo: "hexpm", optional: false]}], "hexpm", "09ea06239938571124f7f5a27bc9ac45dfb1cfc2df40d46ee9b59c3d51366652"},
@@ -76,7 +82,7 @@
"meck": {:hex, :meck, "0.9.2", "85ccbab053f1db86c7ca240e9fc718170ee5bda03810a6292b5306bf31bae5f5", [:rebar3], [], "hexpm", "81344f561357dc40a8344afa53767c32669153355b626ea9fcbc8da6b3045826"},
"memento": {:hex, :memento, "0.3.2", "38cfc8ff9bcb1adff7cbd0f3b78a762636b86dff764729d1c82d0464c539bdd0", [:mix], [], "hexpm", "25cf691a98a0cb70262f4a7543c04bab24648cb2041d937eb64154a8d6f8012b"},
"metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"},
- "mime": {:hex, :mime, "2.0.3", "3676436d3d1f7b81b5a2d2bd8405f412c677558c81b1c92be58c00562bb59095", [:mix], [], "hexpm", "27a30bf0db44d25eecba73755acf4068cbfe26a4372f9eb3e4ea3a45956bff6b"},
+ "mime": {:hex, :mime, "1.6.0", "dabde576a497cef4bbdd60aceee8160e02a6c89250d6c0b29e56c0dfb00db3d2", [:mix], [], "hexpm", "31a1a8613f8321143dde1dafc36006a17d28d02bdfecb9e95a880fa7aabd19a7"},
"mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"},
"mimetype_parser": {:hex, :mimetype_parser, "0.1.3", "628ac9fe56aa7edcedb534d68397dd66674ab82493c8ebe39acb9a19b666099d", [:mix], [], "hexpm", "7d8f80c567807ce78cd93c938e7f4b0a20b1aaaaab914bf286f68457d9f7a852"},
"mix_erlang_tasks": {:hex, :mix_erlang_tasks, "0.1.0", "36819fec60b80689eb1380938675af215565a89320a9e29c72c70d97512e4649", [:mix], [], "hexpm", "95d2839c422c482a70c08a8702da8242f86b773f8ab6e8602a4eb72da8da04ed"},
@@ -88,6 +94,7 @@
"nimble_parsec": {:hex, :nimble_parsec, "1.2.3", "244836e6e3f1200c7f30cb56733fd808744eca61fd182f731eac4af635cc6d0b", [:mix], [], "hexpm", "c8d789e39b9131acf7b99291e93dae60ab48ef14a7ee9d58c6964f59efb570b0"},
"number": {:hex, :number, "1.0.3", "932c8a2d478a181c624138958ca88a78070332191b8061717270d939778c9857", [:mix], [{:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}], "hexpm", "dd397bbc096b2ca965a6a430126cc9cf7b9ef7421130def69bcf572232ca0f18"},
"numbers": {:hex, :numbers, "5.2.4", "f123d5bb7f6acc366f8f445e10a32bd403c8469bdbce8ce049e1f0972b607080", [:mix], [{:coerce, "~> 1.0", [hex: :coerce, repo: "hexpm", optional: false]}, {:decimal, "~> 1.9 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "eeccf5c61d5f4922198395bf87a465b6f980b8b862dd22d28198c5e6fab38582"},
+ "oauth2": {:hex, :oauth2, "2.0.0", "338382079fe16c514420fa218b0903f8ad2d4bfc0ad0c9f988867dfa246731b0", [:mix], [{:hackney, "~> 1.13", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "881b8364ac7385f9fddc7949379cbe3f7081da37233a1aa7aab844670a91e7e7"},
"optimal": {:hex, :optimal, "0.3.6", "46bbf52fbbbd238cda81e02560caa84f93a53c75620f1fe19e81e4ae7b07d1dd", [:mix], [], "hexpm", "1a06ea6a653120226b35b283a1cd10039550f2c566edcdec22b29316d73640fd"},
"parallel_stream": {:hex, :parallel_stream, "1.0.6", "b967be2b23f0f6787fab7ed681b4c45a215a81481fb62b01a5b750fa8f30f76c", [:mix], [], "hexpm", "639b2e8749e11b87b9eb42f2ad325d161c170b39b288ac8d04c4f31f8f0823eb"},
"parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"},
@@ -99,7 +106,7 @@
"plug": {:hex, :plug, "1.13.6", "187beb6b67c6cec50503e940f0434ea4692b19384d47e5fdfd701e93cadb4cc2", [:mix], [{:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:plug_crypto, "~> 1.1.1 or ~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4.3 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "02b9c6b9955bce92c829f31d6284bf53c591ca63c4fb9ff81dfd0418667a34ff"},
"plug_cowboy": {:hex, :plug_cowboy, "2.5.2", "62894ccd601cf9597e2c23911ff12798a8a18d237e9739f58a6b04e4988899fe", [:mix], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:cowboy_telemetry, "~> 0.3", [hex: :cowboy_telemetry, repo: "hexpm", optional: false]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "ea6e87f774c8608d60c8d34022a7d073bd7680a0a013f049fc62bf35efea1044"},
"plug_crypto": {:hex, :plug_crypto, "1.2.2", "05654514ac717ff3a1843204b424477d9e60c143406aa94daf2274fdd280794d", [:mix], [], "hexpm", "87631c7ad914a5a445f0a3809f99b079113ae4ed4b867348dd9eec288cecb6db"},
- "poison": {:hex, :poison, "5.0.0", "d2b54589ab4157bbb82ec2050757779bfed724463a544b6e20d79855a9e43b24", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "11dc6117c501b80c62a7594f941d043982a1bd05a1184280c0d9166eb4d8d3fc"},
+ "poison": {:hex, :poison, "4.0.1", "bcb755a16fac91cad79bfe9fc3585bb07b9331e50cfe3420a24bcc2d735709ae", [:mix], [], "hexpm", "ba8836feea4b394bb718a161fc59a288fe0109b5006d6bdf97b6badfcf6f0f25"},
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
"postgrex": {:hex, :postgrex, "0.15.13", "7794e697481799aee8982688c261901de493eb64451feee6ea58207d7266d54a", [:mix], [{:connection, "~> 1.0", [hex: :connection, repo: "hexpm", optional: false]}, {:db_connection, "~> 2.1", [hex: :db_connection, repo: "hexpm", optional: false]}, {:decimal, "~> 1.5 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm", "3ffb76e1a97cfefe5c6a95632a27ffb67f28871c9741fb585f9d1c3cd2af70f1"},
"prometheus": {:hex, :prometheus, "4.9.1", "ecf9ccf0fdd0fefb13b19f5216aff8b4bdc852171f5c79133bd998ce8210cf65", [:mix, :rebar3], [{:quantile_estimator, "~> 0.2.1", [hex: :quantile_estimator, repo: "hexpm", optional: false]}], "hexpm", "d75e80d7b2c1be6bf296e211e806e939ae3d9e0428f45b4caad1817f028213d3"},
@@ -113,6 +120,7 @@
"que": {:hex, :que, "0.10.1", "788ed0ec92ed69bdf9cfb29bf41a94ca6355b8d44959bd0669cf706e557ac891", [:mix], [{:ex_utils, "~> 0.1.6", [hex: :ex_utils, repo: "hexpm", optional: false]}, {:memento, "~> 0.3.0", [hex: :memento, repo: "hexpm", optional: false]}], "hexpm", "a737b365253e75dbd24b2d51acc1d851049e87baae08cd0c94e2bc5cd65088d5"},
"ranch": {:hex, :ranch, "1.8.0", "8c7a100a139fd57f17327b6413e4167ac559fbc04ca7448e9be9057311597a1d", [:make, :rebar3], [], "hexpm", "49fbcfd3682fab1f5d109351b61257676da1a2fdbe295904176d5e521a2ddfe5"},
"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"},
+ "redix": {:hex, :redix, "1.1.5", "6fc460d66a5c2287e83e6d73dddc8d527ff59cb4d4f298b41e03a4db8c3b2bd5", [: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", "679afdd4c14502fe9c11387ff1cdcb33065a1cf511097da1eee407f17c7a418b"},
"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"},
"rustler": {:hex, :rustler, "0.24.0", "b8362a2fee1c9d2c7373b0bfdc98f75bbc02864efcec50df173fe6c4f72d4cc4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:toml, "~> 0.6", [hex: :toml, repo: "hexpm", optional: false]}], "hexpm", "2773167fca68a6525822ad977b41368ea3c2af876c42ebaa7c9d6bb69b67f1ce"},
"sobelow": {:hex, :sobelow, "0.11.1", "23438964486f8112b41e743bbfd402da3e5b296fdc9eacab29914b79c48916dd", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "9897363a7eff96f4809304a90aad819e2ad5e5d24db547af502885146746a53c"},
@@ -127,6 +135,8 @@
"timex": {:hex, :timex, "3.7.9", "790cdfc4acfce434e442f98c02ea6d84d0239073bfd668968f82ac63e9a6788d", [:mix], [{:combine, "~> 0.10", [hex: :combine, repo: "hexpm", optional: false]}, {:gettext, "~> 0.10", [hex: :gettext, repo: "hexpm", optional: false]}, {:tzdata, "~> 1.1", [hex: :tzdata, repo: "hexpm", optional: false]}], "hexpm", "64691582e5bb87130f721fc709acfb70f24405833998fabf35be968984860ce1"},
"toml": {:hex, :toml, "0.6.2", "38f445df384a17e5d382befe30e3489112a48d3ba4c459e543f748c2f25dd4d1", [:mix], [], "hexpm", "d013e45126d74c0c26a38d31f5e8e9b83ea19fc752470feb9a86071ca5a672fa"},
"tzdata": {:hex, :tzdata, "1.1.1", "20c8043476dfda8504952d00adac41c6eda23912278add38edc140ae0c5bcc46", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "a69cec8352eafcd2e198dea28a34113b60fdc6cb57eb5ad65c10292a6ba89787"},
+ "ueberauth": {:hex, :ueberauth, "0.10.1", "6706b410ee6bd9d67eac983ed9dc7fdc1f06b18677d7b8ba71d5725e07cc8826", [:mix], [{:plug, "~> 1.5", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "bb715b562395c4cc26b2d8e637c6bb0eb8c67d50c0ea543c0f78f06b7e8efdb1"},
+ "ueberauth_auth0": {:hex, :ueberauth_auth0, "2.0.0", "f3919834f1f473b39e423f2e90dfc1801929d319634e6649d9e198f4ccd46f3e", [:mix], [{:oauth2, "~> 2.0", [hex: :oauth2, repo: "hexpm", optional: false]}, {:ueberauth, "~> 0.7", [hex: :ueberauth, repo: "hexpm", optional: false]}], "hexpm", "7b7b8fd7f2f7314ab910e9327452e5c904296f0dbba227d02b445f25a334a012"},
"unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"},
"wallaby": {:hex, :wallaby, "0.30.1", "81342a34080867ab359aca23de4d1d8c6bbdeb35d8ce2a8c42e42b758d539963", [:mix], [{:ecto_sql, ">= 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}, {:httpoison, "~> 0.12 or ~> 1.0", [hex: :httpoison, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix_ecto, ">= 3.0.0", [hex: :phoenix_ecto, repo: "hexpm", optional: true]}, {:web_driver_client, "~> 0.2.0", [hex: :web_driver_client, repo: "hexpm", optional: false]}], "hexpm", "457251df6a94ff80816524136edbce6400cb1ee979586c90224ff634e9543d78"},
"web_driver_client": {:hex, :web_driver_client, "0.2.0", "63b76cd9eb3b0716ec5467a0f8bead73d3d9612e63f7560d21357f03ad86e31a", [:mix], [{:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:tesla, "~> 1.3", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "83cc6092bc3e74926d1c8455f0ce927d5d1d36707b74d9a65e38c084aab0350f"},
- <%=
- for status <- ["error", "warning", "success", "question"] do
- render BlockScoutWeb.CommonComponentsView, "_modal_status.html", status: status
- end
- %>
- <%= render BlockScoutWeb.SmartContractView, "_pending_contract_write.html" %>
<% function_abi =
case Jason.encode([function]) do
{:ok, abi_string} ->
@@ -66,7 +57,7 @@
@contract_abi
end
end %>
-
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex
index f9d6cc62e9..5ea6121e39 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex
@@ -4,6 +4,7 @@
"_details.html",
token: @token,
counters_path: @counters_path,
+ tags: @tags,
conn: @conn
) %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
index a8faa835bf..75b6e2fe20 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
@@ -1,5 +1,7 @@
<% status = transaction_status(@transaction) %>
<% error_in_internal_tx = @transaction.has_error_in_internal_txs %>
+<% current_user = AuthController.current_user(@conn) %>
+<% tx_tags = BlockScoutWeb.Models.GetTransactionTags.get_transaction_with_addresses_tags(@transaction, current_user) %>
@@ -31,6 +33,10 @@
<%= if method_name do %>
<%= render BlockScoutWeb.FormView, "_tag.html", text: method_name, additional_classes: ["method", "ml-1"] %>
<% end %>
+ <%= if tx_tags.personal_tx_tag && tx_tags.personal_tx_tag.name !== :error do %>
+ <%= render BlockScoutWeb.FormView, "_tag.html", text: tx_tags.personal_tx_tag.name, additional_classes: [tag_name_to_label(tx_tags.personal_tx_tag.name), "ml-1"] %>
+ <% end %>
+ <%= render BlockScoutWeb.AddressView, "_labels.html", tags: tx_tags %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex
index 24e9036698..d26557b9c3 100644
--- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex
+++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_total_transfers_from_to.html.eex
@@ -1,45 +1,48 @@
<%= with {:ok, from_address} <- Chain.hash_to_address(@transfer.from_address_hash),
- {:ok, to_address} <- Chain.hash_to_address(@transfer.to_address_hash) do %>
+{:ok, to_address} <- Chain.hash_to_address(@transfer.to_address_hash) do %>
+<% from_tags = BlockScoutWeb.Models.GetAddressTags.get_address_tags(@transfer.from_address_hash, @current_user) %>
+<% to_tags = BlockScoutWeb.Models.GetAddressTags.get_address_tags(@transfer.to_address_hash, @current_user) %>