From 12517dbde551f1a19eeba2ed45631c935037d6eb Mon Sep 17 00:00:00 2001 From: Victor Baranov Date: Fri, 27 Sep 2024 18:59:49 +0300 Subject: [PATCH] feat: Address scam badge flag (#10763) * Address badges * Pass badges preload in all controllers related to address * Process review comments: redesign routes * Changes to fit specified requirements * Hide scam addresses from search based on the flag at the backend * Refactoring based on review comments * Update apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> * Update apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> * Add addresses filtering * Hide scam tokens from the lists --------- Co-authored-by: nikitosing <32202610+nikitosing@users.noreply.github.com> --- apps/block_scout_web/.sobelow-conf | 3 +- .../channels/address_channel.ex | 18 +++- .../api/v2/address_badge_controller.ex | 90 +++++++++++++++++++ .../controllers/api/v2/address_controller.ex | 19 ++-- .../controllers/api/v2/block_controller.ex | 10 +-- .../controllers/api/v2/fallback_controller.ex | 7 ++ .../api/v2/main_page_controller.ex | 4 +- .../api/v2/transaction_controller.ex | 26 ++++-- .../transaction_interpretation.ex | 10 +-- .../models/transaction_state_helper.ex | 10 +-- .../lib/block_scout_web/notifier.ex | 8 +- .../routers/address_badges_v2_router.ex | 58 ++++++++++++ .../lib/block_scout_web/routers/api_router.ex | 11 ++- .../views/api/v2/address_badge_view.ex | 33 +++++++ .../block_scout_web/views/api/v2/helper.ex | 7 ++ apps/explorer/lib/explorer/chain/address.ex | 4 + .../chain/address/scam_badge_to_address.ex | 79 ++++++++++++++++ .../lib/explorer/chain/advanced_filter.ex | 12 +-- .../lib/explorer/chain/celo/epoch_reward.ex | 4 +- apps/explorer/lib/explorer/chain/search.ex | 12 ++- .../lib/explorer/chain/smart_contract.ex | 2 + apps/explorer/lib/explorer/chain/token.ex | 2 + .../lib/explorer/chain/token_transfer.ex | 4 +- apps/explorer/lib/explorer/helper.ex | 38 +++++++- ...240910095635_add_address_badges_tables.exs | 14 +++ config/runtime.exs | 1 + cspell.json | 1 + docker-compose/envs/common-blockscout.env | 5 +- 28 files changed, 431 insertions(+), 61 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex create mode 100644 apps/explorer/lib/explorer/chain/address/scam_badge_to_address.ex create mode 100644 apps/explorer/priv/repo/migrations/20240910095635_add_address_badges_tables.exs diff --git a/apps/block_scout_web/.sobelow-conf b/apps/block_scout_web/.sobelow-conf index 70a8e7b010..45fc3453b9 100644 --- a/apps/block_scout_web/.sobelow-conf +++ b/apps/block_scout_web/.sobelow-conf @@ -9,6 +9,7 @@ ignore_files: [ "apps/block_scout_web/lib/block_scout_web/routers/smart_contracts_api_v2_router.ex", "apps/block_scout_web/lib/block_scout_web/routers/tokens_api_v2_router.ex", - "apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex" + "apps/block_scout_web/lib/block_scout_web/routers/utils_api_v2_router.ex", + "apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex" ] ] diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex index 7fd6932a41..b22306431b 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex @@ -50,8 +50,18 @@ defmodule BlockScoutWeb.AddressChannel do @transaction_associations [ from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations], - created_contract_address: [:names, :smart_contract, :proxy_implementations] + to_address: [ + :scam_badge, + :names, + :smart_contract, + :proxy_implementations + ], + created_contract_address: [ + :scam_badge, + :names, + :smart_contract, + :proxy_implementations + ] ] ++ @chain_type_transaction_associations @@ -404,8 +414,8 @@ defmodule BlockScoutWeb.AddressChannel do token_transfers |> Repo.preload([ [ - from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations] + from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ] ]), conn: nil diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex new file mode 100644 index 0000000000..5a3d58d572 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_badge_controller.ex @@ -0,0 +1,90 @@ +defmodule BlockScoutWeb.API.V2.AddressBadgeController do + require Logger + use BlockScoutWeb, :controller + + alias Explorer.Chain + alias Explorer.Chain.Address.ScamBadgeToAddress + alias Plug.Conn + + @api_true [api?: true] + + action_fallback(BlockScoutWeb.API.V2.FallbackController) + + def assign_badge_to_address( + conn, + %{ + "address_hashes" => address_hashes + } = params + ) + when is_list(address_hashes) do + with :ok <- check_sensitive_endpoint_api_key(params["api_key"]), + valid_address_hashes = filter_address_hashes(address_hashes), + {_num_of_inserted, badge_to_address_list} <- ScamBadgeToAddress.add(valid_address_hashes) do + conn + |> put_status(200) + |> render(:badge_to_address, %{ + badge_to_address_list: badge_to_address_list, + status: if(Enum.empty?(badge_to_address_list), do: "update skipped", else: "added") + }) + end + end + + def assign_badge_to_address(_, _), do: {:error, :not_found} + + def unassign_badge_from_address( + conn, + %{ + "address_hashes" => address_hashes + } = params + ) + when is_list(address_hashes) do + with :ok <- check_sensitive_endpoint_api_key(params["api_key"]), + valid_address_hashes = filter_address_hashes(address_hashes), + {_num_of_deleted, badge_to_address_list} <- ScamBadgeToAddress.delete(valid_address_hashes) do + conn + |> put_status(200) + |> render(:badge_to_address, %{ + badge_to_address_list: badge_to_address_list, + status: if(Enum.empty?(badge_to_address_list), do: "update skipped", else: "removed") + }) + end + end + + def unassign_badge_from_address(_, _), do: {:error, :not_found} + + def show_badge_addresses(conn, _) do + with {:ok, body, _conn} <- Conn.read_body(conn, []), + {:ok, %{"api_key" => provided_api_key}} <- Jason.decode(body), + :ok <- check_sensitive_endpoint_api_key(provided_api_key) do + badge_to_address_list = ScamBadgeToAddress.get(@api_true) + + conn + |> put_status(200) + |> render(:badge_to_address, %{ + badge_to_address_list: badge_to_address_list + }) + else + _ -> + {:error, :not_found} + end + end + + defp check_sensitive_endpoint_api_key(provided_api_key) do + with {:sensitive_endpoints_api_key, api_key} when not is_nil(api_key) <- + {:sensitive_endpoints_api_key, Application.get_env(:block_scout_web, :sensitive_endpoints_api_key)}, + {:api_key, ^api_key} <- {:api_key, provided_api_key} do + :ok + end + end + + defp filter_address_hashes(address_hashes) do + address_hashes + |> Enum.uniq() + |> Enum.filter(fn potential_address_hash -> + case Chain.string_to_address_hash(potential_address_hash) do + {:ok, _address_hash} -> true + _ -> false + end + end) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex index 59afca890e..447d830313 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/address_controller.ex @@ -52,9 +52,9 @@ defmodule BlockScoutWeb.API.V2.AddressController do @transaction_necessity_by_association [ necessity_by_association: %{ - [created_contract_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, :block => :optional } |> Map.merge(@chain_type_transaction_necessity_by_association), @@ -63,8 +63,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do @token_transfer_necessity_by_association [ necessity_by_association: %{ - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, :block => :optional, :transaction => :optional, :token => :optional @@ -75,6 +75,7 @@ defmodule BlockScoutWeb.API.V2.AddressController do @address_options [ necessity_by_association: %{ :names => :optional, + :scam_badge => :optional, :token => :optional, :proxy_implementations => :optional }, @@ -212,8 +213,8 @@ defmodule BlockScoutWeb.API.V2.AddressController do options = [ necessity_by_association: %{ - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, :block => :optional, :token => :optional, :transaction => :optional @@ -284,9 +285,9 @@ defmodule BlockScoutWeb.API.V2.AddressController do full_options = [ necessity_by_association: %{ - [created_contract_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional } ] |> Keyword.merge(paging_options(params)) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex index ab2fed6b17..f440de46f8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/block_controller.ex @@ -83,9 +83,9 @@ defmodule BlockScoutWeb.API.V2.BlockController do @transaction_necessity_by_association [ necessity_by_association: %{ - [created_contract_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, :block => :optional } |> Map.merge(@chain_type_transaction_necessity_by_association) @@ -93,9 +93,9 @@ defmodule BlockScoutWeb.API.V2.BlockController do @internal_transaction_necessity_by_association [ necessity_by_association: %{ - [created_contract_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional } ] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex index 7f2544b453..f6c90431e3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/fallback_controller.ex @@ -133,6 +133,13 @@ defmodule BlockScoutWeb.API.V2.FallbackController do |> render(:changeset_errors, changeset: changeset) end + def call(conn, {:error, :badge_creation_failed}) do + conn + |> put_status(:unprocessable_entity) + |> put_view(UserView) + |> render(:message, %{message: "Badge creation failed"}) + end + def call(conn, {:restricted_access, true}) do Logger.error(fn -> ["#{@restricted_access}"] diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex index 79a89f8edd..ec99ba4422 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/main_page_controller.ex @@ -24,9 +24,9 @@ defmodule BlockScoutWeb.API.V2.MainPageController do necessity_by_association: %{ :block => :required, - [created_contract_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional } |> Map.merge(@chain_type_transaction_necessity_by_association), paging_options: %PagingOptions{page_size: 6}, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex index 43179c26c8..ce5e5bbed9 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/transaction_controller.ex @@ -70,6 +70,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do :block => :optional, [ created_contract_address: [ + :scam_badge, :names, :token, :smart_contract, @@ -78,26 +79,33 @@ defmodule BlockScoutWeb.API.V2.TransactionController do ] => :optional, [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [ + to_address: [ + :scam_badge, + :names, + :smart_contract, + :proxy_implementations + ] + ] => :optional } |> Map.merge(@chain_type_transaction_necessity_by_association) @token_transfers_necessity_by_association %{ - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional } @token_transfers_in_tx_necessity_by_association %{ - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional, + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, token: :required } @internal_transaction_necessity_by_association [ necessity_by_association: %{ - [created_contract_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional } ] @@ -515,7 +523,7 @@ defmodule BlockScoutWeb.API.V2.TransactionController do validate_transaction(transaction_hash_string, params, necessity_by_association: %{ [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional }, api?: true ) do diff --git a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex index 591598fd42..94a0eb27e6 100644 --- a/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex +++ b/apps/block_scout_web/lib/block_scout_web/microservice_interfaces/transaction_interpretation.ex @@ -19,9 +19,9 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do @items_limit 50 @internal_transaction_necessity_by_association [ necessity_by_association: %{ - [created_contract_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [from_address: [:names, :smart_contract, :proxy_implementations]] => :optional, - [to_address: [:names, :smart_contract, :proxy_implementations]] => :optional + [created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional, + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] => :optional } ] @@ -108,9 +108,9 @@ defmodule BlockScoutWeb.MicroserviceInterfaces.TransactionInterpretation do Chain.select_repo(@api_true).preload(transaction, [ :transaction_actions, :block, - to_address: [:names, :smart_contract], + to_address: [:scam_badge, :names, :smart_contract], from_address: [:names, :smart_contract], - created_contract_address: [:names, :token, :smart_contract] + created_contract_address: [:scam_badge, :names, :token, :smart_contract] ]) skip_sig_provider? = false diff --git a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex index 3d96d69ff1..7774d28759 100644 --- a/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/models/transaction_state_helper.ex @@ -69,16 +69,16 @@ defmodule BlockScoutWeb.Models.TransactionStateHelper do |> Enum.find(&(&1.hash == transaction.hash)) |> Repo.preload( token_transfers: [ - from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations] + from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ], internal_transactions: [ - from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations] + from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ], block: [miner: [:names, :smart_contract, :proxy_implementations]], from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations] + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ) previous_block_number = BlockNumberHelper.previous_block_number(transaction.block_number) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index a25f812120..089bc27de3 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -181,8 +181,8 @@ defmodule BlockScoutWeb.Notifier do DenormalizationHelper.extend_transaction_preload([ :token, :transaction, - from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations] + from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ]) ) @@ -205,9 +205,9 @@ defmodule BlockScoutWeb.Notifier do def handle_event({:chain_event, :transactions, :realtime, transactions}) do base_preloads = [ :block, - created_contract_address: [:names, :smart_contract, :proxy_implementations], + created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations] + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ] preloads = if API_V2.enabled?(), do: [:token_transfers | base_preloads], else: base_preloads diff --git a/apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex new file mode 100644 index 0000000000..36edce1b8c --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/routers/address_badges_v2_router.ex @@ -0,0 +1,58 @@ +# This file in ignore list of `sobelow`, be careful while adding new endpoints here +defmodule BlockScoutWeb.Routers.AddressBadgesApiV2Router do + @moduledoc """ + Router for /api/v2/scam-badge-addresses. This route has separate router in order to ignore sobelow's warning about missing CSRF protection + """ + use BlockScoutWeb, :router + alias BlockScoutWeb.API.V2 + alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} + + @max_query_string_length 5_000 + + pipeline :api_v2 do + plug( + Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + query_string_length: @max_query_string_length, + pass: ["*/*"], + json_decoder: Poison + ) + + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) + plug(:accepts, ["json"]) + plug(CheckApiV2) + plug(:fetch_session) + plug(:protect_from_forgery) + plug(RateLimit) + end + + pipeline :api_v2_no_forgery_protect do + plug( + Plug.Parsers, + parsers: [:urlencoded, :multipart, :json], + length: 20_000_000, + query_string_length: 5_000, + pass: ["*/*"], + json_decoder: Poison + ) + + plug(BlockScoutWeb.Plug.Logger, application: :api_v2) + plug(:accepts, ["json"]) + plug(CheckApiV2) + plug(RateLimit) + plug(:fetch_session) + end + + scope "/", as: :api_v2 do + pipe_through(:api_v2_no_forgery_protect) + + post("/", V2.AddressBadgeController, :assign_badge_to_address) + delete("/", V2.AddressBadgeController, :unassign_badge_from_address) + end + + scope "/", as: :api_v2 do + pipe_through(:api_v2) + + get("/", V2.AddressBadgeController, :show_badge_addresses) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex index 2a987813fc..772527d2ec 100644 --- a/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/routers/api_router.ex @@ -14,7 +14,15 @@ defmodule BlockScoutWeb.Routers.ApiRouter do """ use BlockScoutWeb, :router alias BlockScoutWeb.AddressTransactionController - alias BlockScoutWeb.Routers.{APIKeyV2Router, SmartContractsApiV2Router, TokensApiV2Router, UtilsApiV2Router} + + alias BlockScoutWeb.Routers.{ + AddressBadgesApiV2Router, + APIKeyV2Router, + SmartContractsApiV2Router, + TokensApiV2Router, + UtilsApiV2Router + } + alias BlockScoutWeb.Plug.{CheckApiV2, RateLimit} alias BlockScoutWeb.Routers.AccountRouter @@ -25,6 +33,7 @@ defmodule BlockScoutWeb.Routers.ApiRouter do forward("/v2/key", APIKeyV2Router) forward("/v2/utils", UtilsApiV2Router) + forward("/v2/scam-badge-addresses", AddressBadgesApiV2Router) pipeline :api do plug( diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex new file mode 100644 index 0000000000..d3eda4545f --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/address_badge_view.ex @@ -0,0 +1,33 @@ +defmodule BlockScoutWeb.API.V2.AddressBadgeView do + use BlockScoutWeb, :view + + def render("badge_to_address.json", %{badge_to_address_list: badge_to_address_list, status: status}) do + prepare_badge_to_address(badge_to_address_list, status) + end + + def render("badge_to_address.json", %{badge_to_address_list: badge_to_address_list}) do + prepare_badge_to_address(badge_to_address_list) + end + + defp prepare_badge_to_address(badge_to_address_list) do + %{ + badge_to_address_list: format_badge_to_address_list(badge_to_address_list) + } + end + + defp prepare_badge_to_address(badge_to_address_list, status) do + %{ + badge_to_address_list: format_badge_to_address_list(badge_to_address_list), + status: status + } + end + + defp format_badge_to_address_list(badge_to_address_list) do + badge_to_address_list + |> Enum.map(fn badge_to_address -> + %{ + address_hash: "0x" <> Base.encode16(badge_to_address.address_hash.bytes) + } + end) + end +end diff --git a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex index 8df0acb81b..d0ffdc9608 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/api/v2/helper.ex @@ -84,6 +84,7 @@ defmodule BlockScoutWeb.API.V2.Helper do "hash" => Address.checksum(address), "is_contract" => smart_contract?, "name" => address_name(address), + "is_scam" => address_marked_as_scam?(address), "proxy_type" => proxy_type, "implementations" => Proxy.proxy_object_info(implementation_address_hashes, implementation_names), "is_verified" => verified?(address) || verified_minimal_proxy?(proxy_implementations), @@ -158,6 +159,12 @@ defmodule BlockScoutWeb.API.V2.Helper do def address_name(_), do: nil + def address_marked_as_scam?(%Address{scam_badge: scam_badge}) when not is_nil(scam_badge) do + true + end + + def address_marked_as_scam?(_), do: false + def verified?(%Address{smart_contract: nil}), do: false def verified?(%Address{smart_contract: %{metadata_from_verified_bytecode_twin: true}}), do: false def verified?(%Address{smart_contract: %NotLoaded{}}), do: nil diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 52c1d662a9..700cbb126c 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -103,6 +103,7 @@ defmodule Explorer.Chain.Address.Schema do ) has_many(:names, Address.Name, foreign_key: :address_hash, references: :hash) + has_one(:scam_badge, Address.ScamBadgeToAddress, foreign_key: :address_hash, references: :hash) has_many(:decompiled_smart_contracts, DecompiledSmartContract, foreign_key: :address_hash, references: :hash) has_many(:withdrawals, Withdrawal, foreign_key: :address_hash, references: :hash) @@ -126,6 +127,7 @@ defmodule Explorer.Chain.Address do alias Ecto.Association.NotLoaded alias Ecto.Changeset + alias Explorer.Helper, as: ExplorerHelper alias Explorer.Chain.Cache.{Accounts, NetVersion} alias Explorer.Chain.SmartContract.Proxy alias Explorer.Chain.SmartContract.Proxy.Models.Implementation @@ -181,6 +183,7 @@ defmodule Explorer.Chain.Address do Solidity source code is in `smart_contract` `t:Explorer.Chain.SmartContract.t/0` `contract_source_code` *if* the contract has been verified * `names` - names known for the address + * `badges` - badges applied for the address * `inserted_at` - when this address was inserted * `updated_at` - when this address was last updated * `ens_domain_name` - virtual field for ENS domain name passing @@ -454,6 +457,7 @@ defmodule Explorer.Chain.Address do ) base_query + |> ExplorerHelper.maybe_hide_scam_addresses(:hash) |> page_addresses(paging_options) |> limit(^paging_options.page_size) |> Chain.select_repo(options).all() diff --git a/apps/explorer/lib/explorer/chain/address/scam_badge_to_address.ex b/apps/explorer/lib/explorer/chain/address/scam_badge_to_address.ex new file mode 100644 index 0000000000..085de60e44 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/address/scam_badge_to_address.ex @@ -0,0 +1,79 @@ +defmodule Explorer.Chain.Address.ScamBadgeToAddress do + @moduledoc """ + Defines Address.ScamBadgeToAddress.t() mapping with Address.t() + """ + + use Explorer.Schema + + import Ecto.Changeset + + alias Explorer.{Chain, Repo} + alias Explorer.Chain.{Address, Hash} + + import Ecto.Query, only: [from: 2] + + @typedoc """ + * `address` - the `t:Explorer.Chain.Address.t/0`. + * `address_hash` - foreign key for `address`. + """ + @primary_key false + typed_schema "scam_address_badge_mappings" do + belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address, null: false) + + timestamps() + end + + @required_fields ~w(address_hash)a + @allowed_fields @required_fields + + def changeset(%__MODULE__{} = struct, params \\ %{}) do + struct + |> cast(params, @allowed_fields) + |> validate_required(@required_fields) + |> foreign_key_constraint(:address_hash) + end + + @doc """ + Adds Address.ScamBadgeToAddress.t() by the list of Hash.Address.t() + """ + @spec add([Hash.Address.t()]) :: {non_neg_integer(), [__MODULE__.t()]} + def add(address_hashes) do + now = DateTime.utc_now() + + insert_params = + address_hashes + |> Enum.map(fn address_hash_string -> + case Chain.string_to_address_hash(address_hash_string) do + {:ok, address_hash} -> %{address_hash: address_hash, inserted_at: now, updated_at: now} + :error -> nil + end + end) + |> Enum.filter(&(!is_nil(&1))) + + Repo.insert_all(__MODULE__, insert_params, on_conflict: :nothing, returning: [:address_hash]) + end + + @doc """ + Deletes Address.ScamBadgeToAddress.t() by the list of Hash.Address.t() + """ + @spec delete([Hash.Address.t()]) :: {non_neg_integer(), [__MODULE__.t()]} + def delete(address_hashes) do + query = + from( + bta in __MODULE__, + where: bta.address_hash in ^address_hashes, + select: bta + ) + + Repo.delete_all(query) + end + + @doc """ + Gets the list of Address.ScamBadgeToAddress.t() + """ + @spec get([Chain.necessity_by_association_option() | Chain.api?()]) :: [__MODULE__.t()] + def get(options) do + __MODULE__ + |> Chain.select_repo(options).all() + end +end diff --git a/apps/explorer/lib/explorer/chain/advanced_filter.ex b/apps/explorer/lib/explorer/chain/advanced_filter.ex index 1161030be1..ec7b9305b8 100644 --- a/apps/explorer/lib/explorer/chain/advanced_filter.ex +++ b/apps/explorer/lib/explorer/chain/advanced_filter.ex @@ -252,8 +252,8 @@ defmodule Explorer.Chain.AdvancedFilter do preload: [ :block, from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations], - created_contract_address: [:names, :smart_contract, :proxy_implementations] + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], + created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ], order_by: [ desc: transaction.block_number, @@ -289,8 +289,8 @@ defmodule Explorer.Chain.AdvancedFilter do as: :transaction, preload: [ from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations], - created_contract_address: [:names, :smart_contract, :proxy_implementations], + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], + created_contract_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], transaction: transaction ], order_by: [ @@ -703,8 +703,8 @@ defmodule Explorer.Chain.AdvancedFilter do preload: [ :transaction, :token, - from_address: [:names, :smart_contract, :proxy_implementations], - to_address: [:names, :smart_contract, :proxy_implementations] + from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations], + to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations] ], select_merge: %{ token_ids: [token_transfer.token_id], diff --git a/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex b/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex index 8356456f25..f08d59f336 100644 --- a/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex +++ b/apps/explorer/lib/explorer/chain/celo/epoch_reward.ex @@ -101,8 +101,8 @@ defmodule Explorer.Chain.Celo.EpochReward do select: {tt.log_index, tt}, preload: [ :token, - [from_address: [:names, :smart_contract, :proxy_implementations]], - [to_address: [:names, :smart_contract, :proxy_implementations]] + [from_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]], + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] ] ) diff --git a/apps/explorer/lib/explorer/chain/search.ex b/apps/explorer/lib/explorer/chain/search.ex index 3405753499..5731f94cd8 100644 --- a/apps/explorer/lib/explorer/chain/search.ex +++ b/apps/explorer/lib/explorer/chain/search.ex @@ -83,10 +83,12 @@ defmodule Explorer.Chain.Search do end def base_joint_query(string, term) do - tokens_query = search_token_query(string, term) - contracts_query = search_contract_query(term) + tokens_query = + string |> search_token_query(term) |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash) + + contracts_query = term |> search_contract_query() |> ExplorerHelper.maybe_hide_scam_addresses(:address_hash) labels_query = search_label_query(term) - address_query = search_address_query(string) + address_query = string |> search_address_query() |> ExplorerHelper.maybe_hide_scam_addresses(:hash) block_query = search_block_query(string) basic_query = @@ -161,6 +163,7 @@ defmodule Explorer.Chain.Search do tokens_result = search_query |> search_token_query(term) + |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash) |> order_by([token], desc_nulls_last: token.circulating_market_cap, desc_nulls_last: token.fiat_value, @@ -175,6 +178,7 @@ defmodule Explorer.Chain.Search do contracts_result = term |> search_contract_query() + |> ExplorerHelper.maybe_hide_scam_addresses(:address_hash) |> order_by([items], asc: items.name, desc: items.inserted_at) |> limit(^paging_options.page_size) |> select_repo(options).all() @@ -216,6 +220,7 @@ defmodule Explorer.Chain.Search do address_result = if query = search_address_query(search_query) do query + |> ExplorerHelper.maybe_hide_scam_addresses(:hash) |> select_repo(options).all() else [] @@ -602,6 +607,7 @@ defmodule Explorer.Chain.Search do [ result[:address_hash] |> search_address_query() + |> ExplorerHelper.maybe_hide_scam_addresses(:hash) |> select_repo(options).all() |> merge_address_search_result_with_ens_info(result) ] diff --git a/apps/explorer/lib/explorer/chain/smart_contract.ex b/apps/explorer/lib/explorer/chain/smart_contract.ex index 3e80fbfdf2..c79d2fe278 100644 --- a/apps/explorer/lib/explorer/chain/smart_contract.ex +++ b/apps/explorer/lib/explorer/chain/smart_contract.ex @@ -113,6 +113,7 @@ defmodule Explorer.Chain.SmartContract do alias Explorer.Chain.SmartContract.Proxy alias Explorer.Chain.SmartContract.Proxy.Models.Implementation + alias Explorer.Helper, as: ExplorerHelper alias Explorer.SmartContract.Helper alias Explorer.SmartContract.Solidity.Verifier @@ -1273,6 +1274,7 @@ defmodule Explorer.Chain.SmartContract do query = from(contract in __MODULE__) query + |> ExplorerHelper.maybe_hide_scam_addresses(:address_hash) |> filter_contracts(filter) |> search_contracts(search_string) |> SortingHelper.apply_sorting(sorting_options, @default_sorting) diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index b756d2f301..f97e0f5113 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -81,6 +81,7 @@ defmodule Explorer.Chain.Token do alias Ecto.Changeset alias Explorer.{Chain, SortingHelper} alias Explorer.Chain.{BridgedToken, Hash, Search, Token} + alias Explorer.Helper, as: ExplorerHelper alias Explorer.Repo alias Explorer.SmartContract.Helper @@ -210,6 +211,7 @@ defmodule Explorer.Chain.Token do sorted_paginated_query = query + |> ExplorerHelper.maybe_hide_scam_addresses(:contract_address_hash) |> apply_filter(token_type) |> SortingHelper.apply_sorting(sorting, @default_sorting) |> SortingHelper.page_with_sorting(paging_options, sorting, @default_sorting) diff --git a/apps/explorer/lib/explorer/chain/token_transfer.ex b/apps/explorer/lib/explorer/chain/token_transfer.ex index 8bf41f8c88..77cace2455 100644 --- a/apps/explorer/lib/explorer/chain/token_transfer.ex +++ b/apps/explorer/lib/explorer/chain/token_transfer.ex @@ -240,7 +240,7 @@ defmodule Explorer.Chain.TokenTransfer do :transaction, :token, [from_address: [:names, :smart_contract, :proxy_implementations]], - [to_address: [:names, :smart_contract, :proxy_implementations]] + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] ]) only_consensus_transfers_query() @@ -267,7 +267,7 @@ defmodule Explorer.Chain.TokenTransfer do :transaction, :token, [from_address: [:names, :smart_contract, :proxy_implementations]], - [to_address: [:names, :smart_contract, :proxy_implementations]] + [to_address: [:scam_badge, :names, :smart_contract, :proxy_implementations]] ]) only_consensus_transfers_query() diff --git a/apps/explorer/lib/explorer/helper.ex b/apps/explorer/lib/explorer/helper.ex index e4d2bc6e46..f31081578e 100644 --- a/apps/explorer/lib/explorer/helper.ex +++ b/apps/explorer/lib/explorer/helper.ex @@ -7,7 +7,7 @@ defmodule Explorer.Helper do alias Explorer.Chain alias Explorer.Chain.Data - import Ecto.Query, only: [where: 3] + import Ecto.Query, only: [join: 5, where: 3] import Explorer.Chain.SmartContract, only: [burn_address_hash_string: 0] @max_safe_integer round(:math.pow(2, 63)) - 1 @@ -212,4 +212,40 @@ defmodule Explorer.Helper do true -> :eq end end + + @doc """ + Conditionally hides scam addresses in the given query. + + ## Parameters + + - query: The Ecto query to be modified. + - address_hash_key: The key used to identify address hash field in the query to join with base query table on. + + ## Returns + + The modified query with scam addresses hidden, if applicable. + """ + @spec maybe_hide_scam_addresses(nil | Ecto.Query.t(), atom()) :: Ecto.Query.t() + def maybe_hide_scam_addresses(nil, _address_hash_key), do: nil + + def maybe_hide_scam_addresses(query, address_hash_key) do + if Application.get_env(:block_scout_web, :hide_scam_addresses) do + query + |> join( + :inner, + [q], + q2 in fragment(""" + ( + SELECT hash + FROM addresses a + WHERE NOT EXISTS + (SELECT 1 FROM scam_address_badge_mappings sabm WHERE sabm.address_hash=a.hash) + ) + """), + on: field(q, ^address_hash_key) == q2.hash + ) + else + query + end + end end diff --git a/apps/explorer/priv/repo/migrations/20240910095635_add_address_badges_tables.exs b/apps/explorer/priv/repo/migrations/20240910095635_add_address_badges_tables.exs new file mode 100644 index 0000000000..ba7bbc7db2 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20240910095635_add_address_badges_tables.exs @@ -0,0 +1,14 @@ +defmodule Explorer.Repo.Migrations.AddAddressBadgesTables do + use Ecto.Migration + + def change do + create table(:scam_address_badge_mappings, primary_key: false) do + add(:address_hash, references(:addresses, column: :hash, type: :bytea, on_delete: :delete_all), + null: false, + primary_key: true + ) + + timestamps(null: false, type: :utc_datetime_usec) + end + end +end diff --git a/config/runtime.exs b/config/runtime.exs index 4ef037d05c..bcd6580bcc 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -33,6 +33,7 @@ config :block_scout_web, permanent_light_mode_enabled: ConfigHelper.parse_bool_env_var("PERMANENT_LIGHT_MODE_ENABLED"), display_token_icons: ConfigHelper.parse_bool_env_var("DISPLAY_TOKEN_ICONS"), hide_block_miner: ConfigHelper.parse_bool_env_var("HIDE_BLOCK_MINER"), + hide_scam_addresses: ConfigHelper.parse_bool_env_var("HIDE_SCAM_ADDRESSES"), show_tenderly_link: ConfigHelper.parse_bool_env_var("SHOW_TENDERLY_LINK"), sensitive_endpoints_api_key: System.get_env("API_SENSITIVE_ENDPOINTS_KEY"), disable_api?: disable_api? diff --git a/cspell.json b/cspell.json index 22872a9c56..3e2c5a3b11 100644 --- a/cspell.json +++ b/cspell.json @@ -461,6 +461,7 @@ "rollups", "RPC's", "RPCs", + "sabm", "safelow", "savechives", "Secon", diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index fd2f85e1ff..7c82e9e21c 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -65,12 +65,12 @@ EXCHANGE_RATES_COIN= # EXCHANGE_RATES_CRYPTORANK_COIN_ID= # EXCHANGE_RATES_CRYPTORANK_LIMIT= # TOKEN_EXCHANGE_RATES_SOURCE= -POOL_SIZE=80 # EXCHANGE_RATES_COINGECKO_PLATFORM_ID= # TOKEN_EXCHANGE_RATE_INTERVAL= # TOKEN_EXCHANGE_RATE_REFETCH_INTERVAL= # TOKEN_EXCHANGE_RATE_MAX_BATCH_SIZE= # DISABLE_TOKEN_EXCHANGE_RATE= +POOL_SIZE=80 POOL_SIZE_API=10 ECTO_USE_SSL=false # DATADOG_HOST= @@ -262,6 +262,7 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false # INDEXER_ARBITRUM_BRIDGE_MESSAGES_TRACKING_ENABLED= # INDEXER_ARBITRUM_TRACKING_MESSAGES_ON_L1_RECHECK_INTERVAL= # INDEXER_ARBITRUM_MISSED_MESSAGES_RECHECK_INTERVAL= +# INDEXER_ARBITRUM_MISSED_MESSAGES_BLOCKS_DEPTH= # CELO_CORE_CONTRACTS= # INDEXER_CELO_VALIDATOR_GROUP_VOTES_BATCH_SIZE=200000 # INDEXER_DISABLE_CELO_EPOCH_FETCHER=false @@ -273,7 +274,6 @@ INDEXER_DISABLE_INTERNAL_TRANSACTIONS_FETCHER=false # FILECOIN_PENDING_ADDRESS_OPERATIONS_MIGRATION_CONCURRENCY= # INDEXER_DISABLE_FILECOIN_ADDRESS_INFO_FETCHER=false # INDEXER_FILECOIN_ADDRESS_INFO_CONCURRENCY=1 -# INDEXER_ARBITRUM_MISSED_MESSAGES_BLOCKS_DEPTH= # INDEXER_REALTIME_FETCHER_MAX_GAP= # INDEXER_FETCHER_INIT_QUERY_LIMIT= # INDEXER_TOKEN_BALANCES_FETCHER_INIT_QUERY_LIMIT= @@ -342,6 +342,7 @@ MAINTENANCE_ALERT_MESSAGE= CHAIN_ID= MAX_SIZE_UNLESS_HIDE_ARRAY=50 HIDE_BLOCK_MINER=false +# HIDE_SCAM_ADDRESSES= DISPLAY_TOKEN_ICONS=false RE_CAPTCHA_SECRET_KEY= RE_CAPTCHA_CLIENT_KEY=