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
pull/8696/head
nikitosing 1 year ago committed by GitHub
parent 15a48bed36
commit 0560fa8990
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 67
      apps/block_scout_web/lib/block_scout_web/api_router.ex
  3. 17
      apps/block_scout_web/lib/block_scout_web/chain.ex
  4. 91
      apps/block_scout_web/lib/block_scout_web/controllers/account/api/v1/user_controller.ex
  5. 19
      apps/block_scout_web/lib/block_scout_web/views/account/api/v1/user_view.ex
  6. 196
      apps/block_scout_web/test/block_scout_web/controllers/account/api/v1/user_controller_test.exs
  7. 37
      apps/explorer/lib/explorer/account/tag_address.ex
  8. 37
      apps/explorer/lib/explorer/account/tag_transaction.ex
  9. 41
      apps/explorer/lib/explorer/account/watchlist_address.ex
  10. 2
      apps/explorer/lib/explorer/chain.ex
  11. 48
      apps/explorer/test/support/factory.ex
  12. 4
      config/runtime.exs
  13. 2
      docker-compose/envs/common-blockscout.env

@ -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

@ -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)

@ -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

@ -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)},

@ -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

@ -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

@ -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

@ -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

@ -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 =

@ -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}

@ -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

@ -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),

@ -249,3 +249,5 @@ EIP_1559_ELASTICITY_MULTIPLIER=2
# IPFS_GATEWAY_URL=
API_V2_ENABLED=true
# ADDRESSES_TABS_COUNTERS_TTL=10m
# ACCOUNT_PRIVATE_TAGS_LIMIT=2000
# ACCOUNT_WATCHLIST_ADDRESSES_LIMIT=15

Loading…
Cancel
Save