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