From 0560fa899090c6fbf8e751ddfcd4e9e48784a919 Mon Sep 17 00:00:00 2001 From: nikitosing <32202610+nikitosing@users.noreply.github.com> Date: Wed, 25 Oct 2023 13:43:26 +0300 Subject: [PATCH] Account: add pagination + envs for limits (#8528) * Account: Add pagination + increase limits * Add tests * Add specs and docs * Add TODO * Dupliacte all the account endpoints + new paginated to /api/account/v2 --- CHANGELOG.md | 1 + .../lib/block_scout_web/api_router.ex | 67 +++++- .../lib/block_scout_web/chain.ex | 17 +- .../account/api/v1/user_controller.ex | 91 +++++++- .../views/account/api/v1/user_view.ex | 19 ++ .../account/api/v1/user_controller_test.exs | 196 +++++++++++++++++- .../lib/explorer/account/tag_address.ex | 37 +++- .../lib/explorer/account/tag_transaction.ex | 37 +++- .../lib/explorer/account/watchlist_address.ex | 41 +++- apps/explorer/lib/explorer/chain.ex | 2 +- apps/explorer/test/support/factory.ex | 48 ++++- config/runtime.exs | 4 +- docker-compose/envs/common-blockscout.env | 4 +- 13 files changed, 514 insertions(+), 50 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b4c4b58a9..886337c4ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ ### Features - [#8673](https://github.com/blockscout/blockscout/pull/8673) - Add a window for balances fetching from non-archive node +- [#8528](https://github.com/blockscout/blockscout/pull/8528) - Account: add pagination + envs for limits - [#7584](https://github.com/blockscout/blockscout/pull/7584) - Add Polygon zkEVM batches fetcher ### Fixes diff --git a/apps/block_scout_web/lib/block_scout_web/api_router.ex b/apps/block_scout_web/lib/block_scout_web/api_router.ex index b1b1faa048..64ac1d99c7 100644 --- a/apps/block_scout_web/lib/block_scout_web/api_router.ex +++ b/apps/block_scout_web/lib/block_scout_web/api_router.ex @@ -49,6 +49,7 @@ defmodule BlockScoutWeb.ApiRouter do alias BlockScoutWeb.Account.Api.V1.{AuthenticateController, EmailController, TagsController, UserController} alias BlockScoutWeb.API.V2 + # TODO: Remove /account/v1 paths scope "/account/v1", as: :account_v1 do pipe_through(:api) pipe_through(:account_api) @@ -62,6 +63,70 @@ defmodule BlockScoutWeb.ApiRouter do get("/resend", EmailController, :resend_email) end + scope "/user" do + get("/info", UserController, :info) + + get("/watchlist", UserController, :watchlist_old) + delete("/watchlist/:id", UserController, :delete_watchlist) + post("/watchlist", UserController, :create_watchlist) + put("/watchlist/:id", UserController, :update_watchlist) + + get("/api_keys", UserController, :api_keys) + delete("/api_keys/:api_key", UserController, :delete_api_key) + post("/api_keys", UserController, :create_api_key) + put("/api_keys/:api_key", UserController, :update_api_key) + + get("/custom_abis", UserController, :custom_abis) + delete("/custom_abis/:id", UserController, :delete_custom_abi) + post("/custom_abis", UserController, :create_custom_abi) + put("/custom_abis/:id", UserController, :update_custom_abi) + + get("/public_tags", UserController, :public_tags_requests) + delete("/public_tags/:id", UserController, :delete_public_tags_request) + post("/public_tags", UserController, :create_public_tags_request) + put("/public_tags/:id", UserController, :update_public_tags_request) + + scope "/tags" do + get("/address/", UserController, :tags_address_old) + get("/address/:id", UserController, :tags_address) + delete("/address/:id", UserController, :delete_tag_address) + post("/address/", UserController, :create_tag_address) + put("/address/:id", UserController, :update_tag_address) + + get("/transaction/", UserController, :tags_transaction_old) + get("/transaction/:id", UserController, :tags_transaction) + delete("/transaction/:id", UserController, :delete_tag_transaction) + post("/transaction/", UserController, :create_tag_transaction) + put("/transaction/:id", UserController, :update_tag_transaction) + end + end + end + + # TODO: Remove /account/v1 paths + scope "/account/v1" do + pipe_through(:api) + pipe_through(:account_api) + + scope "/tags" do + get("/address/:address_hash", TagsController, :tags_address) + + get("/transaction/:transaction_hash", TagsController, :tags_transaction) + end + end + + scope "/account/v2", as: :account_v2 do + pipe_through(:api) + pipe_through(:account_api) + + get("/authenticate", AuthenticateController, :authenticate_get) + post("/authenticate", AuthenticateController, :authenticate_post) + + get("/get_csrf", UserController, :get_csrf) + + scope "/email" do + get("/resend", EmailController, :resend_email) + end + scope "/user" do get("/info", UserController, :info) @@ -101,7 +166,7 @@ defmodule BlockScoutWeb.ApiRouter do end end - scope "/account/v1" do + scope "/account/v2" do pipe_through(:api) pipe_through(:account_api) diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 97f291feae..dc3bcbe837 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -17,6 +17,7 @@ defmodule BlockScoutWeb.Chain do import Explorer.Helper, only: [parse_integer: 1] + alias Explorer.Account.{TagAddress, TagTransaction, WatchlistAddress} alias Explorer.Chain.Block.Reward alias Explorer.Chain.{ @@ -365,7 +366,7 @@ defmodule BlockScoutWeb.Chain do end end - # clause for Polygon Edge Deposits and Withdrawals + # clause for Polygon Edge Deposits and Withdrawals and for account's entities pagination def paging_options(%{"id" => id_string}) when is_binary(id_string) do case Integer.parse(id_string) do {id, ""} -> @@ -376,7 +377,7 @@ defmodule BlockScoutWeb.Chain do end end - # clause for Polygon Edge Deposits and Withdrawals + # clause for Polygon Edge Deposits and Withdrawals and for account's entities pagination def paging_options(%{"id" => id}) when is_integer(id) do [paging_options: %{@default_paging_options | key: {id}}] end @@ -484,6 +485,18 @@ defmodule BlockScoutWeb.Chain do } end + defp paging_params(%TagAddress{id: id}) do + %{"id" => id} + end + + defp paging_params(%TagTransaction{id: id}) do + %{"id" => id} + end + + defp paging_params(%WatchlistAddress{id: id}) do + %{"id" => id} + end + defp paging_params([%Token{} = token, _]) do paging_params(token) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex index 544e2a1c52..724378cc69 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex @@ -2,6 +2,16 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do use BlockScoutWeb, :controller import BlockScoutWeb.Account.AuthController, only: [current_user: 1] + + import BlockScoutWeb.Chain, + only: [ + next_page_params: 3, + paging_options: 1, + split_list_by_page: 1 + ] + + import BlockScoutWeb.PagingHelper, only: [delete_parameters_from_next_page_params: 1] + import Ecto.Query, only: [from: 2] alias BlockScoutWeb.Models.UserFromAuth @@ -25,7 +35,7 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do end end - def watchlist(conn, _params) do + def watchlist_old(conn, _params) do with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)}, {:watchlist, %{watchlists: [watchlist | _]}} <- @@ -63,6 +73,51 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do end end + def watchlist(conn, params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)}, + {:watchlist, %{watchlists: [watchlist | _]}} <- + {:watchlist, Repo.account_repo().preload(identity, :watchlists)} do + results_plus_one = WatchlistAddress.get_watchlist_addresses_by_watchlist_id(watchlist.id, paging_options(params)) + + {watchlist_addresses, next_page} = split_list_by_page(results_plus_one) + + next_page_params = + next_page |> next_page_params(watchlist_addresses, delete_parameters_from_next_page_params(params)) + + watchlist_addresses_prepared = + Enum.map(watchlist_addresses, fn wa -> + balances = + Chain.fetch_paginated_last_token_balances(wa.address_hash, + paging_options: %PagingOptions{page_size: @token_balances_amount + 1} + ) + + count = Enum.count(balances) + overflow? = count > @token_balances_amount + + fiat_sum = + balances + |> Enum.take(@token_balances_amount) + |> Enum.reduce(Decimal.new(0), fn tb, acc -> Decimal.add(acc, tb.fiat_value || 0) end) + + %WatchlistAddress{ + wa + | tokens_fiat_value: fiat_sum, + tokens_count: min(count, @token_balances_amount), + tokens_overflow: overflow? + } + end) + + conn + |> put_status(200) + |> render(:watchlist_addresses, %{ + exchange_rate: Market.get_coin_exchange_rate(), + watchlist_addresses: watchlist_addresses_prepared, + next_page_params: next_page_params + }) + end + end + def delete_watchlist(conn, %{"id" => watchlist_address_id}) do with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)}, @@ -188,7 +243,7 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do end end - def tags_address(conn, _params) do + def tags_address_old(conn, _params) do with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)}, address_tags <- TagAddress.get_tags_address_by_identity_id(identity.id) do @@ -198,6 +253,21 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do end end + def tags_address(conn, params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)} do + results_plus_one = TagAddress.get_tags_address_by_identity_id(identity.id, paging_options(params)) + + {tags, next_page} = split_list_by_page(results_plus_one) + + next_page_params = next_page |> next_page_params(tags, delete_parameters_from_next_page_params(params)) + + conn + |> put_status(200) + |> render(:address_tags, %{address_tags: tags, next_page_params: next_page_params}) + end + end + def delete_tag_address(conn, %{"id" => tag_id}) do with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)}, @@ -242,7 +312,7 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do end end - def tags_transaction(conn, _params) do + def tags_transaction_old(conn, _params) do with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)}, transaction_tags <- TagTransaction.get_tags_transaction_by_identity_id(identity.id) do @@ -252,6 +322,21 @@ defmodule BlockScoutWeb.Account.Api.V1.UserController do end end + def tags_transaction(conn, params) do + with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, + {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)} do + results_plus_one = TagTransaction.get_tags_transaction_by_identity_id(identity.id, paging_options(params)) + + {tags, next_page} = split_list_by_page(results_plus_one) + + next_page_params = next_page |> next_page_params(tags, delete_parameters_from_next_page_params(params)) + + conn + |> put_status(200) + |> render(:transaction_tags, %{transaction_tags: tags, next_page_params: next_page_params}) + end + end + def delete_tag_transaction(conn, %{"id" => tag_id}) do with {:auth, %{id: uid}} <- {:auth, current_user(conn)}, {:identity, %Identity{} = identity} <- {:identity, UserFromAuth.find_identity(uid)}, 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 index 1dd819166a..26c8d7c829 100644 --- 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 @@ -12,6 +12,17 @@ defmodule BlockScoutWeb.Account.Api.V1.UserView 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, + next_page_params: next_page_params + }) do + %{ + "items" => Enum.map(watchlist_addresses, &prepare_watchlist_address(&1, exchange_rate)), + "next_page_params" => next_page_params + } + 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 @@ -20,6 +31,10 @@ defmodule BlockScoutWeb.Account.Api.V1.UserView do prepare_watchlist_address(watchlist_address, exchange_rate) end + def render("address_tags.json", %{address_tags: address_tags, next_page_params: next_page_params}) do + %{"items" => Enum.map(address_tags, &prepare_address_tag/1), "next_page_params" => next_page_params} + end + def render("address_tags.json", %{address_tags: address_tags}) do Enum.map(address_tags, &prepare_address_tag/1) end @@ -28,6 +43,10 @@ defmodule BlockScoutWeb.Account.Api.V1.UserView do prepare_address_tag(address_tag) end + def render("transaction_tags.json", %{transaction_tags: transaction_tags, next_page_params: next_page_params}) do + %{"items" => Enum.map(transaction_tags, &prepare_transaction_tag/1), "next_page_params" => next_page_params} + end + def render("transaction_tags.json", %{transaction_tags: transaction_tags}) do Enum.map(transaction_tags, &prepare_transaction_tag/1) 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 index 810751d6ff..2a8639bfdc 100644 --- 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 @@ -1,8 +1,14 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do use BlockScoutWeb.ConnCase - alias Explorer.Repo + alias Explorer.Account.{ + TagAddress, + TagTransaction, + WatchlistAddress + } + alias Explorer.Chain.Address + alias Explorer.Repo alias BlockScoutWeb.Models.UserFromAuth setup %{conn: conn} do @@ -48,6 +54,50 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do assert tag_address_response["id"] end + test "can't insert private address tags more than limit", %{conn: conn, user: user} do + old_env = Application.get_env(:explorer, Explorer.Account) + + new_env = + old_env + |> Keyword.replace(:private_tags_limit, 5) + |> Keyword.replace(:watchlist_addresses_limit, 5) + + Application.put_env(:explorer, Explorer.Account, new_env) + + for _ <- 0..3 do + build(:tag_address_db, user: user) |> Repo.account_repo().insert() + end + + assert conn + |> post("/api/account/v1/user/tags/address", build(:tag_address)) + |> json_response(200) + + assert conn + |> post("/api/account/v1/user/tags/address", build(:tag_address)) + |> json_response(422) + + Application.put_env(:explorer, Explorer.Account, old_env) + end + + test "check address tags pagination", %{conn: conn, user: user} do + tags_address = + for _ <- 0..50 do + build(:tag_address_db, user: user) |> Repo.account_repo().insert!() + end + + assert response = + conn + |> get("/api/account/v2/user/tags/address") + |> json_response(200) + + response_1 = + conn + |> get("/api/account/v2/user/tags/address", response["next_page_params"]) + |> json_response(200) + + check_paginated_response(response, response_1, tags_address) + end + test "edit private address tag", %{conn: conn} do address_tag = build(:tag_address) @@ -236,6 +286,50 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do assert tag_transaction_response["id"] end + test "can't insert private transaction tags more than limit", %{conn: conn, user: user} do + old_env = Application.get_env(:explorer, Explorer.Account) + + new_env = + old_env + |> Keyword.replace(:private_tags_limit, 5) + |> Keyword.replace(:watchlist_addresses_limit, 5) + + Application.put_env(:explorer, Explorer.Account, new_env) + + for _ <- 0..3 do + build(:tag_transaction_db, user: user) |> Repo.account_repo().insert() + end + + assert conn + |> post("/api/account/v1/user/tags/transaction", build(:tag_transaction)) + |> json_response(200) + + assert conn + |> post("/api/account/v1/user/tags/transaction", build(:tag_transaction)) + |> json_response(422) + + Application.put_env(:explorer, Explorer.Account, old_env) + end + + test "check transaction tags pagination", %{conn: conn, user: user} do + tags_address = + for _ <- 0..50 do + build(:tag_transaction_db, user: user) |> Repo.account_repo().insert!() + end + + assert response = + conn + |> get("/api/account/v2/user/tags/transaction") + |> json_response(200) + + response_1 = + conn + |> get("/api/account/v2/user/tags/transaction", response["next_page_params"]) + |> json_response(200) + + check_paginated_response(response, response_1, tags_address) + end + test "edit private transaction tag", %{conn: conn} do tx_tag = build(:tag_transaction) @@ -422,6 +516,50 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do assert get_watchlist_address_response_1_1["id"] == post_watchlist_address_response_1["id"] end + test "can't insert watchlist addresses more than limit", %{conn: conn, user: user} do + old_env = Application.get_env(:explorer, Explorer.Account) + + new_env = + old_env + |> Keyword.replace(:private_tags_limit, 5) + |> Keyword.replace(:watchlist_addresses_limit, 5) + + Application.put_env(:explorer, Explorer.Account, new_env) + + for _ <- 0..3 do + build(:watchlist_address_db, wl_id: user.watchlist_id) |> Repo.account_repo().insert() + end + + assert conn + |> post("/api/account/v1/user/watchlist", build(:watchlist_address)) + |> json_response(200) + + assert conn + |> post("/api/account/v1/user/watchlist", build(:watchlist_address)) + |> json_response(422) + + Application.put_env(:explorer, Explorer.Account, old_env) + end + + test "check watchlist tags pagination", %{conn: conn, user: user} do + tags_address = + for _ <- 0..50 do + build(:watchlist_address_db, wl_id: user.watchlist_id) |> Repo.account_repo().insert!() + end + + assert response = + conn + |> get("/api/account/v2/user/watchlist") + |> json_response(200) + + response_1 = + conn + |> get("/api/account/v2/user/watchlist", response["next_page_params"] |> dbg()) + |> json_response(200) + + check_paginated_response(response, response_1, tags_address) + end + test "delete watchlist address", %{conn: conn} do watchlist_address_map = build(:watchlist_address) @@ -625,14 +763,14 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do [wa2, wa1] = conn |> get("/api/account/v1/user/watchlist") |> json_response(200) - assert wa1["tokens_fiat_value"] |> Decimal.new() |> Decimal.round(14) == - values |> Enum.reduce(Decimal.new(0), fn x, acc -> Decimal.add(x, acc) end) |> Decimal.round(14) + assert wa1["tokens_fiat_value"] |> Decimal.new() |> Decimal.round(13) == + values |> Enum.reduce(Decimal.new(0), fn x, acc -> Decimal.add(x, acc) end) |> Decimal.round(13) assert wa1["tokens_count"] == 150 assert wa1["tokens_overflow"] == false - assert wa2["tokens_fiat_value"] |> Decimal.new() |> Decimal.round(14) == - values_1 |> Enum.reduce(Decimal.new(0), fn x, acc -> Decimal.add(x, acc) end) |> Decimal.round(14) + assert wa2["tokens_fiat_value"] |> Decimal.new() |> Decimal.round(13) == + values_1 |> Enum.reduce(Decimal.new(0), fn x, acc -> Decimal.add(x, acc) end) |> Decimal.round(13) assert wa2["tokens_count"] == 150 assert wa2["tokens_overflow"] == true @@ -1051,4 +1189,52 @@ defmodule BlockScoutWeb.Account.Api.V1.UserControllerTest do {:ok, time, _} = DateTime.from_iso8601(request["submission_date"]) %{request | "submission_date" => Calendar.strftime(time, "%b %d, %Y")} end + + defp compare_item(%TagAddress{} = tag_address, json) do + assert json["address_hash"] == to_string(tag_address.address_hash) + assert json["name"] == tag_address.name + assert json["id"] == tag_address.id + assert json["address"]["hash"] == Address.checksum(tag_address.address_hash) + end + + defp compare_item(%TagTransaction{} = tag_transaction, json) do + assert json["transaction_hash"] == to_string(tag_transaction.tx_hash) + assert json["name"] == tag_transaction.name + assert json["id"] == tag_transaction.id + end + + defp compare_item(%WatchlistAddress{} = watchlist, json) do + 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 + } + } + + assert json["address_hash"] == to_string(watchlist.address_hash) + assert json["name"] == watchlist.name + assert json["id"] == watchlist.id + assert json["address"]["hash"] == Address.checksum(watchlist.address_hash) + assert json["notification_methods"]["email"] == watchlist.notify_email + assert json["notification_settings"] == notification_settings + end + + defp check_paginated_response(first_page_resp, second_page_resp, list) do + assert Enum.count(first_page_resp["items"]) == 50 + assert first_page_resp["next_page_params"] != nil + compare_item(Enum.at(list, 50), Enum.at(first_page_resp["items"], 0)) + compare_item(Enum.at(list, 1), Enum.at(first_page_resp["items"], 49)) + + assert Enum.count(second_page_resp["items"]) == 1 + assert second_page_resp["next_page_params"] == nil + compare_item(Enum.at(list, 0), Enum.at(second_page_resp["items"], 0)) + end end diff --git a/apps/explorer/lib/explorer/account/tag_address.ex b/apps/explorer/lib/explorer/account/tag_address.ex index 546632773e..5d38db529a 100644 --- a/apps/explorer/lib/explorer/account/tag_address.ex +++ b/apps/explorer/lib/explorer/account/tag_address.ex @@ -9,13 +9,11 @@ defmodule Explorer.Account.TagAddress do alias Ecto.Changeset alias Explorer.Account.Identity - alias Explorer.{Chain, Repo} + alias Explorer.{Chain, PagingOptions, 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) @@ -70,12 +68,14 @@ defmodule Explorer.Account.TagAddress do end def tag_address_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = tag_address) do + max_tags_count = get_max_tags_count() + 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 + |> limit(^max_tags_count) + |> Repo.account_repo().aggregate(:count, :id) >= max_tags_count do tag_address - |> add_error(:name, "Max #{@max_tag_address_per_account} tags per account") + |> add_error(:name, "Max #{max_tags_count} tags per account") else tag_address end @@ -86,18 +86,35 @@ defmodule Explorer.Account.TagAddress do 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 + @doc """ + Query paginated private address tags by identity id + """ + @spec get_tags_address_by_identity_id(integer(), [Chain.paging_options()]) :: [__MODULE__] + def get_tags_address_by_identity_id(id, options \\ []) + + def get_tags_address_by_identity_id(id, options) when not is_nil(id) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + id |> tags_address_by_identity_id_query() + |> order_by([tag], desc: tag.id) + |> page_address_tags(paging_options) + |> limit(^paging_options.page_size) |> Repo.account_repo().all() end - def get_tags_address_by_identity_id(_), do: nil + def get_tags_address_by_identity_id(_, _), do: [] + + defp page_address_tags(query, %PagingOptions{key: {id}}) do + query + |> where([tag], tag.id < ^id) + end + + defp page_address_tags(query, _), do: query 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 @@ -152,5 +169,5 @@ defmodule Explorer.Account.TagAddress do end end - def get_max_tags_count, do: @max_tag_address_per_account + def get_max_tags_count, do: Application.get_env(:explorer, Explorer.Account)[:private_tags_limit] end diff --git a/apps/explorer/lib/explorer/account/tag_transaction.ex b/apps/explorer/lib/explorer/account/tag_transaction.ex index 086488ab23..b821fffcf9 100644 --- a/apps/explorer/lib/explorer/account/tag_transaction.ex +++ b/apps/explorer/lib/explorer/account/tag_transaction.ex @@ -9,11 +9,9 @@ defmodule Explorer.Account.TagTransaction do alias Ecto.Changeset alias Explorer.Account.Identity - alias Explorer.{Chain, Repo} + alias Explorer.{Chain, PagingOptions, 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) @@ -69,12 +67,14 @@ defmodule Explorer.Account.TagTransaction do end def tag_transaction_count_constraint(%Changeset{changes: %{identity_id: identity_id}} = tag_transaction) do + max_tags_count = get_max_tags_count() + 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 + |> limit(^max_tags_count) + |> Repo.account_repo().aggregate(:count, :id) >= max_tags_count do tag_transaction - |> add_error(:name, "Max #{@max_tag_transaction_per_account} tags per account") + |> add_error(:name, "Max #{max_tags_count} tags per account") else tag_transaction end @@ -85,18 +85,35 @@ defmodule Explorer.Account.TagTransaction do 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 + @doc """ + Query paginated private transaction tags by identity id + """ + @spec get_tags_transaction_by_identity_id(integer(), [Chain.paging_options()]) :: [__MODULE__] + def get_tags_transaction_by_identity_id(id, options \\ []) + + def get_tags_transaction_by_identity_id(id, options) when not is_nil(id) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + id |> tags_transaction_by_identity_id_query() + |> order_by([tag], desc: tag.id) + |> page_transaction_tags(paging_options) + |> limit(^paging_options.page_size) |> Repo.account_repo().all() end - def get_tags_transaction_by_identity_id(_), do: nil + def get_tags_transaction_by_identity_id(_, _), do: [] + + defp page_transaction_tags(query, %PagingOptions{key: {id}}) do + query + |> where([tag], tag.id < ^id) + end + + defp page_transaction_tags(query, _), do: query 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 @@ -151,7 +168,7 @@ defmodule Explorer.Account.TagTransaction do end end - def get_max_tags_count, do: @max_tag_transaction_per_account + def get_max_tags_count, do: Application.get_env(:explorer, Explorer.Account)[:private_tags_limit] end defimpl Jason.Encoder, for: Explorer.Account.TagTransaction do diff --git a/apps/explorer/lib/explorer/account/watchlist_address.ex b/apps/explorer/lib/explorer/account/watchlist_address.ex index aab74b4579..e488ac758e 100644 --- a/apps/explorer/lib/explorer/account/watchlist_address.ex +++ b/apps/explorer/lib/explorer/account/watchlist_address.ex @@ -10,13 +10,11 @@ defmodule Explorer.Account.WatchlistAddress do alias Ecto.Changeset alias Explorer.Account.Notifier.ForbiddenAddress alias Explorer.Account.Watchlist - alias Explorer.{Chain, Repo} + alias Explorer.{Chain, PagingOptions, 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) @@ -79,12 +77,14 @@ defmodule Explorer.Account.WatchlistAddress do end def watchlist_address_count_constraint(%Changeset{changes: %{watchlist_id: watchlist_id}} = watchlist_address) do + max_watchlist_addresses_count = get_max_watchlist_addresses_count() + 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 + |> limit(^max_watchlist_addresses_count) + |> Repo.account_repo().aggregate(:count, :id) >= max_watchlist_addresses_count do watchlist_address - |> add_error(:name, "Max #{@max_watchlist_addresses_per_account} watch list addresses per account") + |> add_error(:name, "Max #{max_watchlist_addresses_count} watch list addresses per account") else watchlist_address end @@ -122,6 +122,32 @@ defmodule Explorer.Account.WatchlistAddress do def watchlist_addresses_by_watchlist_id_query(_), do: nil + @doc """ + Query paginated watchlist addresses by watchlist id + """ + @spec get_watchlist_addresses_by_watchlist_id(integer(), [Chain.paging_options()]) :: [__MODULE__] + def get_watchlist_addresses_by_watchlist_id(watchlist_id, options \\ []) + + def get_watchlist_addresses_by_watchlist_id(watchlist_id, options) when not is_nil(watchlist_id) do + paging_options = Keyword.get(options, :paging_options, Chain.default_paging_options()) + + watchlist_id + |> watchlist_addresses_by_watchlist_id_query() + |> order_by([wla], desc: wla.id) + |> page_watchlist_address(paging_options) + |> limit(^paging_options.page_size) + |> Repo.account_repo().all() + end + + def get_watchlist_addresses_by_watchlist_id(_, _), do: [] + + defp page_watchlist_address(query, %PagingOptions{key: {id}}) do + query + |> where([wla], wla.id < ^id) + end + + defp page_watchlist_address(query, _), do: query + 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__ @@ -160,7 +186,8 @@ defmodule Explorer.Account.WatchlistAddress do end end - def get_max_watchlist_addresses_count, do: @max_watchlist_addresses_per_account + def get_max_watchlist_addresses_count, + do: Application.get_env(:explorer, Explorer.Account)[:watchlist_addresses_limit] def preload_address_fetched_coin_balance(%Watchlist{watchlist_addresses: watchlist_addresses} = watchlist) do w_addresses = diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 422f1dabf1..fa2d966024 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -165,7 +165,7 @@ defmodule Explorer.Chain do @type necessity_by_association :: %{association => necessity} @typep necessity_by_association_option :: {:necessity_by_association, necessity_by_association} - @typep paging_options :: {:paging_options, PagingOptions.t()} + @type paging_options :: {:paging_options, PagingOptions.t()} @typep balance_by_day :: %{date: String.t(), value: Wei.t()} @type api? :: {:api?, true | false} diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index b85a0fff6d..33c9f63477 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -9,6 +9,8 @@ defmodule Explorer.Factory do alias Explorer.Account.{ Identity, + TagAddress, + TagTransaction, Watchlist, WatchlistAddress } @@ -107,6 +109,26 @@ defmodule Explorer.Factory do } end + def watchlist_address_db_factory(%{wl_id: id}) do + hash = build(:address).hash + + %WatchlistAddress{ + name: sequence("test"), + watchlist_id: id, + address_hash: hash, + address_hash_hash: hash_to_lower_case_string(hash), + watch_coin_input: random_bool(), + watch_coin_output: random_bool(), + watch_erc_20_input: random_bool(), + watch_erc_20_output: random_bool(), + watch_erc_721_input: random_bool(), + watch_erc_721_output: random_bool(), + watch_erc_1155_input: random_bool(), + watch_erc_1155_output: random_bool(), + notify_email: random_bool() + } + end + def custom_abi_factory do contract_address_hash = to_string(insert(:contract_address).hash) @@ -140,6 +162,14 @@ defmodule Explorer.Factory do %{"name" => sequence("name"), "transaction_hash" => to_string(insert(:transaction).hash)} end + def tag_address_db_factory(%{user: user}) do + %TagAddress{name: sequence("name"), address_hash: build(:address).hash, identity_id: user.id} + end + + def tag_transaction_db_factory(%{user: user}) do + %TagTransaction{name: sequence("name"), tx_hash: insert(:transaction).hash, identity_id: user.id} + end + def address_to_tag_factory do %AddressToTag{ tag: build(:address_tag), @@ -162,15 +192,15 @@ defmodule Explorer.Factory do 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 + watch_coin_input: random_bool(), + watch_coin_output: random_bool(), + watch_erc_20_input: random_bool(), + watch_erc_20_output: random_bool(), + watch_erc_721_input: random_bool(), + watch_erc_721_output: random_bool(), + watch_erc_1155_input: random_bool(), + watch_erc_1155_output: random_bool(), + notify_email: random_bool() } end diff --git a/config/runtime.exs b/config/runtime.exs index aafccbbf2d..4d0650988c 100644 --- a/config/runtime.exs +++ b/config/runtime.exs @@ -401,7 +401,9 @@ config :explorer, Explorer.Account, sender: System.get_env("ACCOUNT_SENDGRID_SENDER"), template: System.get_env("ACCOUNT_SENDGRID_TEMPLATE") ], - resend_interval: ConfigHelper.parse_time_env_var("ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL", "5m") + resend_interval: ConfigHelper.parse_time_env_var("ACCOUNT_VERIFICATION_EMAIL_RESEND_INTERVAL", "5m"), + private_tags_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_PRIVATE_TAGS_LIMIT", 2000), + watchlist_addresses_limit: ConfigHelper.parse_integer_env_var("ACCOUNT_WATCHLIST_ADDRESSES_LIMIT", 15) config :explorer, :token_id_migration, first_block: ConfigHelper.parse_integer_env_var("TOKEN_ID_MIGRATION_FIRST_BLOCK", 0), diff --git a/docker-compose/envs/common-blockscout.env b/docker-compose/envs/common-blockscout.env index 3a3724eec6..3b5ae9a7d2 100644 --- a/docker-compose/envs/common-blockscout.env +++ b/docker-compose/envs/common-blockscout.env @@ -248,4 +248,6 @@ EIP_1559_ELASTICITY_MULTIPLIER=2 # TOKEN_INSTANCE_OWNER_MIGRATION_BATCH_SIZE=50 # IPFS_GATEWAY_URL= API_V2_ENABLED=true -# ADDRESSES_TABS_COUNTERS_TTL=10m \ No newline at end of file +# ADDRESSES_TABS_COUNTERS_TTL=10m +# ACCOUNT_PRIVATE_TAGS_LIMIT=2000 +# ACCOUNT_WATCHLIST_ADDRESSES_LIMIT=15