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>pull/10844/head
parent
e66d345b96
commit
12517dbde5
@ -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 |
@ -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 |
@ -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 |
@ -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 |
@ -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 |
Loading…
Reference in new issue