API v2: Add sorting to tokens page

pull/7895/head
Maxim Filonov 1 year ago
parent ce4f28c982
commit d52bb7acc7
  1. 1
      CHANGELOG.md
  2. 38
      apps/block_scout_web/lib/block_scout_web/chain.ex
  3. 3
      apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex
  4. 1
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex
  5. 34
      apps/block_scout_web/lib/block_scout_web/paging_helper.ex
  6. 240
      apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs
  7. 89
      apps/explorer/lib/explorer/chain.ex
  8. 175
      apps/explorer/lib/explorer/chain/token.ex

@ -9,6 +9,7 @@
- [#7836](https://github.com/blockscout/blockscout/pull/7836) - Improve unverified email flow
- [#7784](https://github.com/blockscout/blockscout/pull/7784) - Search improvements: Add new fields, light refactoring
- [#7811](https://github.com/blockscout/blockscout/pull/7811) - Filter addresses before insertion
- [#7895](https://github.com/blockscout/blockscout/pull/7895) - API v2: Add sorting to tokens page
### Fixes

@ -150,16 +150,24 @@ defmodule BlockScoutWeb.Chain do
]
end
def paging_options(%{
"market_cap" => market_cap,
"holder_count" => holder_count_str,
"name" => name,
"contract_address_hash" => contract_address_hash_str,
"is_name_null" => is_name_null
}) do
def paging_options(
%{
"market_cap" => market_cap,
"holder_count" => holder_count_str,
"name" => name,
"contract_address_hash" => contract_address_hash_str,
"is_name_null" => is_name_null
} = params
) do
market_cap_decimal =
case Decimal.parse(market_cap) do
{decimal, ""} -> Decimal.round(decimal, 16)
{decimal, ""} -> decimal
_ -> nil
end
fiat_value_decimal =
case Decimal.parse(params["fiat_value"]) do
{decimal, ""} -> decimal
_ -> nil
end
@ -171,7 +179,13 @@ defmodule BlockScoutWeb.Chain do
[
paging_options: %{
@default_paging_options
| key: {market_cap_decimal, holder_count, token_name, contract_address_hash}
| key: %{
fiat_value: fiat_value_decimal,
circulating_market_cap: market_cap_decimal,
holder_count: holder_count,
name: token_name,
contract_address_hash: contract_address_hash
}
}
]
@ -403,14 +417,16 @@ defmodule BlockScoutWeb.Chain do
contract_address_hash: contract_address_hash,
circulating_market_cap: circulating_market_cap,
holder_count: holder_count,
name: token_name
name: token_name,
fiat_value: fiat_value
}) do
%{
"market_cap" => circulating_market_cap,
"holder_count" => holder_count,
"contract_address_hash" => contract_address_hash,
"name" => token_name,
"is_name_null" => is_nil(token_name)
"is_name_null" => is_nil(token_name),
"fiat_value" => fiat_value
}
end

@ -17,7 +17,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
]
import BlockScoutWeb.PagingHelper,
only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1]
only: [delete_parameters_from_next_page_params: 1, token_transfers_types_options: 1, tokens_sorting: 1]
action_fallback(BlockScoutWeb.API.V2.FallbackController)
@ -208,6 +208,7 @@ defmodule BlockScoutWeb.API.V2.TokenController do
params
|> paging_options()
|> Keyword.merge(token_transfers_types_options(params))
|> Keyword.merge(tokens_sorting(params))
|> Keyword.merge(@api_true)
{tokens, next_page} = filter |> Chain.list_top_tokens(options) |> split_list_by_page()

@ -2,7 +2,6 @@ defmodule BlockScoutWeb.TokensController do
use BlockScoutWeb, :controller
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
alias BlockScoutWeb.{Controller, TokensView}
alias Explorer.Chain
alias Phoenix.View

@ -127,14 +127,18 @@ defmodule BlockScoutWeb.PagingHelper do
def delete_parameters_from_next_page_params(params) when is_map(params) do
params
|> Map.delete("block_hash_or_number")
|> Map.delete("transaction_hash")
|> Map.delete("address_hash")
|> Map.delete("type")
|> Map.delete("method")
|> Map.delete("filter")
|> Map.delete("token_address_hash")
|> Map.delete("q")
|> Map.drop([
"block_hash_or_number",
"transaction_hash",
"address_hash",
"type",
"method",
"filter",
"token_address_hash",
"q",
"sort",
"order"
])
end
def delete_parameters_from_next_page_params(_), do: nil
@ -166,4 +170,18 @@ defmodule BlockScoutWeb.PagingHelper do
end
def search_query(_), do: []
def tokens_sorting(%{"sort" => sort_field, "order" => order}) do
[sorting: do_tokens_sorting(sort_field, order)]
end
def tokens_sorting(_), do: []
defp do_tokens_sorting("fiat_value", "asc"), do: [asc_nulls_first: :fiat_value]
defp do_tokens_sorting("fiat_value", "desc"), do: [desc_nulls_last: :fiat_value]
defp do_tokens_sorting("holder_count", "asc"), do: [asc_nulls_first: :holder_count]
defp do_tokens_sorting("holder_count", "desc"), do: [desc_nulls_last: :holder_count]
defp do_tokens_sorting("circulating_market_cap", "asc"), do: [asc_nulls_first: :circulating_market_cap]
defp do_tokens_sorting("circulating_market_cap", "desc"), do: [desc_nulls_last: :circulating_market_cap]
defp do_tokens_sorting(_, _), do: []
end

@ -393,12 +393,174 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
end
describe "/tokens" do
defp check_tokens_pagination(tokens, conn) do
request = get(conn, "/api/v2/tokens")
defp check_tokens_pagination(tokens, conn, additional_params \\ %{}) do
request = get(conn, "/api/v2/tokens", additional_params)
assert response = json_response(request, 200)
request_2nd_page = get(conn, "/api/v2/tokens", response["next_page_params"])
request_2nd_page = get(conn, "/api/v2/tokens", additional_params |> Map.merge(response["next_page_params"]))
assert response_2nd_page = json_response(request_2nd_page, 200)
check_paginated_response(response, response_2nd_page, tokens)
# by fiat_value
tokens_ordered_by_fiat_value = Enum.sort(tokens, &(Decimal.compare(&1.fiat_value, &2.fiat_value) in [:eq, :lt]))
request_ordered_by_fiat_value =
get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "fiat_value", "order" => "desc"}))
assert response_ordered_by_fiat_value = json_response(request_ordered_by_fiat_value, 200)
request_ordered_by_fiat_value_2nd_page =
get(
conn,
"/api/v2/tokens",
additional_params
|> Map.merge(%{"sort" => "fiat_value", "order" => "desc"})
|> Map.merge(response_ordered_by_fiat_value["next_page_params"])
)
assert response_ordered_by_fiat_value_2nd_page = json_response(request_ordered_by_fiat_value_2nd_page, 200)
check_paginated_response(
response_ordered_by_fiat_value,
response_ordered_by_fiat_value_2nd_page,
tokens_ordered_by_fiat_value
)
tokens_ordered_by_fiat_value_asc =
Enum.sort(tokens, &(Decimal.compare(&1.fiat_value, &2.fiat_value) in [:eq, :gt]))
request_ordered_by_fiat_value_asc =
get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "fiat_value", "order" => "asc"}))
assert response_ordered_by_fiat_value_asc = json_response(request_ordered_by_fiat_value_asc, 200)
request_ordered_by_fiat_value_asc_2nd_page =
get(
conn,
"/api/v2/tokens",
additional_params
|> Map.merge(%{"sort" => "fiat_value", "order" => "asc"})
|> Map.merge(response_ordered_by_fiat_value_asc["next_page_params"])
)
assert response_ordered_by_fiat_value_asc_2nd_page =
json_response(request_ordered_by_fiat_value_asc_2nd_page, 200)
check_paginated_response(
response_ordered_by_fiat_value_asc,
response_ordered_by_fiat_value_asc_2nd_page,
tokens_ordered_by_fiat_value_asc
)
# by holders
tokens_ordered_by_holders = Enum.sort(tokens, &(&1.holder_count <= &2.holder_count))
request_ordered_by_holders =
get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holder_count", "order" => "desc"}))
assert response_ordered_by_holders = json_response(request_ordered_by_holders, 200)
request_ordered_by_holders_2nd_page =
get(
conn,
"/api/v2/tokens",
additional_params
|> Map.merge(%{"sort" => "holder_count", "order" => "desc"})
|> Map.merge(response_ordered_by_holders["next_page_params"])
)
assert response_ordered_by_holders_2nd_page = json_response(request_ordered_by_holders_2nd_page, 200)
check_paginated_response(
response_ordered_by_holders,
response_ordered_by_holders_2nd_page,
tokens_ordered_by_holders
)
tokens_ordered_by_holders_asc = Enum.sort(tokens, &(&1.holder_count >= &2.holder_count))
request_ordered_by_holders_asc =
get(conn, "/api/v2/tokens", additional_params |> Map.merge(%{"sort" => "holder_count", "order" => "asc"}))
assert response_ordered_by_holders_asc = json_response(request_ordered_by_holders_asc, 200)
request_ordered_by_holders_asc_2nd_page =
get(
conn,
"/api/v2/tokens",
additional_params
|> Map.merge(%{"sort" => "holder_count", "order" => "asc"})
|> Map.merge(response_ordered_by_holders_asc["next_page_params"])
)
assert response_ordered_by_holders_asc_2nd_page = json_response(request_ordered_by_holders_asc_2nd_page, 200)
check_paginated_response(
response_ordered_by_holders_asc,
response_ordered_by_holders_asc_2nd_page,
tokens_ordered_by_holders_asc
)
# by circulating_market_cap
tokens_ordered_by_circulating_market_cap =
Enum.sort(tokens, &(&1.circulating_market_cap <= &2.circulating_market_cap))
request_ordered_by_circulating_market_cap =
get(
conn,
"/api/v2/tokens",
additional_params |> Map.merge(%{"sort" => "circulating_market_cap", "order" => "desc"})
)
assert response_ordered_by_circulating_market_cap = json_response(request_ordered_by_circulating_market_cap, 200)
request_ordered_by_circulating_market_cap_2nd_page =
get(
conn,
"/api/v2/tokens",
additional_params
|> Map.merge(%{"sort" => "circulating_market_cap", "order" => "desc"})
|> Map.merge(response_ordered_by_circulating_market_cap["next_page_params"])
)
assert response_ordered_by_circulating_market_cap_2nd_page =
json_response(request_ordered_by_circulating_market_cap_2nd_page, 200)
check_paginated_response(
response_ordered_by_circulating_market_cap,
response_ordered_by_circulating_market_cap_2nd_page,
tokens_ordered_by_circulating_market_cap
)
tokens_ordered_by_circulating_market_cap_asc =
Enum.sort(tokens, &(&1.circulating_market_cap >= &2.circulating_market_cap))
request_ordered_by_circulating_market_cap_asc =
get(
conn,
"/api/v2/tokens",
additional_params |> Map.merge(%{"sort" => "circulating_market_cap", "order" => "asc"})
)
assert response_ordered_by_circulating_market_cap_asc =
json_response(request_ordered_by_circulating_market_cap_asc, 200)
request_ordered_by_circulating_market_cap_asc_2nd_page =
get(
conn,
"/api/v2/tokens",
additional_params
|> Map.merge(%{"sort" => "circulating_market_cap", "order" => "asc"})
|> Map.merge(response_ordered_by_circulating_market_cap_asc["next_page_params"])
)
assert response_ordered_by_circulating_market_cap_asc_2nd_page =
json_response(request_ordered_by_circulating_market_cap_asc_2nd_page, 200)
check_paginated_response(
response_ordered_by_circulating_market_cap_asc,
response_ordered_by_circulating_market_cap_asc_2nd_page,
tokens_ordered_by_circulating_market_cap_asc
)
end
test "get empty list", %{conn: conn} do
@ -407,6 +569,70 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
assert %{"items" => [], "next_page_params" => nil} = json_response(request, 200)
end
test "tokens are filtered by single type", %{conn: conn} do
erc_20_tokens =
for i <- 0..50 do
insert(:token, fiat_value: i)
end
erc_721_tokens =
for _i <- 0..50 do
insert(:token, type: "ERC-721")
end
erc_1155_tokens =
for _i <- 0..50 do
insert(:token, type: "ERC-1155")
end
check_tokens_pagination(erc_20_tokens |> Enum.reverse(), conn, %{"type" => "ERC-20"})
check_tokens_pagination(erc_721_tokens |> Enum.reverse(), conn, %{"type" => "ERC-721"})
check_tokens_pagination(erc_1155_tokens |> Enum.reverse(), conn, %{"type" => "ERC-1155"})
end
test "tokens are filtered by multiple type", %{conn: conn} do
erc_20_tokens =
for i <- 0..25 do
insert(:token, fiat_value: i)
end
erc_721_tokens =
for _i <- 0..25 do
insert(:token, type: "ERC-721")
end
erc_1155_tokens =
for _i <- 0..24 do
insert(:token, type: "ERC-1155")
end
check_tokens_pagination(
erc_721_tokens |> Kernel.++(erc_1155_tokens) |> Enum.reverse(),
conn,
%{
"type" => "ERC-1155,ERC-721"
}
)
check_tokens_pagination(
erc_20_tokens |> Kernel.++(erc_1155_tokens) |> Enum.reverse(),
conn,
%{
"type" => "[erc-20,ERC-1155]"
}
)
end
test "sorting by fiat_value", %{conn: conn} do
tokens =
for i <- 0..50 do
insert(:token, fiat_value: i)
end
|> Enum.reverse()
check_tokens_pagination(tokens, conn)
end
# these tests that tokens paginates by each parameter separately and by any combination of them
test "pagination by address", %{conn: conn} do
tokens =
@ -419,14 +645,14 @@ defmodule BlockScoutWeb.API.V2.TokenControllerTest do
end
test "pagination by name", %{conn: conn} do
named_token = insert(:token, holder_count: 0)
empty_named_token = insert(:token, name: "", holder_count: 0)
tokens =
for i <- 0..48 do
for i <- 1..49 do
insert(:token, holder_count: i)
end
empty_named_token = insert(:token, name: "")
named_token = insert(:token)
tokens = [named_token, empty_named_token | tokens]
check_tokens_pagination(tokens, conn)

@ -2471,16 +2471,17 @@ defmodule Explorer.Chain do
def list_top_tokens(filter, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
token_type = Keyword.get(options, :token_type, nil)
sorting = Keyword.get(options, :sorting, [])
fetch_top_tokens(filter, paging_options, token_type, options)
fetch_top_tokens(filter, paging_options, token_type, sorting, options)
end
defp fetch_top_tokens(filter, paging_options, token_type, options) do
base_query = base_token_query(token_type)
defp fetch_top_tokens(filter, paging_options, token_type, sorting, options) do
base_query = Token.base_token_query(token_type, sorting)
base_query_with_paging =
base_query
|> page_tokens(paging_options)
|> Token.page_tokens(paging_options, sorting)
|> limit(^paging_options.page_size)
query =
@ -2501,31 +2502,6 @@ defmodule Explorer.Chain do
|> select_repo(options).all()
end
defp base_token_query(empty_type) when empty_type in [nil, []] do
from(t in Token,
order_by: [
desc_nulls_last: t.circulating_market_cap,
desc_nulls_last: t.holder_count,
asc: t.name,
asc: t.contract_address_hash
],
preload: [:contract_address]
)
end
defp base_token_query(token_types) when is_list(token_types) do
from(t in Token,
where: t.type in ^token_types,
order_by: [
desc_nulls_last: t.circulating_market_cap,
desc_nulls_last: t.holder_count,
asc: t.name,
asc: t.contract_address_hash
],
preload: [:contract_address]
)
end
@doc """
Calls `reducer` on a stream of `t:Explorer.Chain.Block.t/0` without `t:Explorer.Chain.Block.Reward.t/0`.
"""
@ -4645,61 +4621,6 @@ defmodule Explorer.Chain do
)
end
defp page_tokens(query, %PagingOptions{key: nil}), do: query
defp page_tokens(query, %PagingOptions{key: {circulating_market_cap, holder_count, name, contract_address_hash}}) do
from(token in query,
where: ^page_tokens_circulating_market_cap(circulating_market_cap, holder_count, name, contract_address_hash)
)
end
defp page_tokens_circulating_market_cap(nil, holder_count, name, contract_address_hash) do
dynamic(
[t],
is_nil(t.circulating_market_cap) and ^page_tokens_holder_count(holder_count, name, contract_address_hash)
)
end
defp page_tokens_circulating_market_cap(circulating_market_cap, holder_count, name, contract_address_hash) do
dynamic(
[t],
is_nil(t.circulating_market_cap) or t.circulating_market_cap < ^circulating_market_cap or
(t.circulating_market_cap == ^circulating_market_cap and
^page_tokens_holder_count(holder_count, name, contract_address_hash))
)
end
defp page_tokens_holder_count(nil, name, contract_address_hash) do
dynamic(
[t],
is_nil(t.holder_count) and ^page_tokens_name(name, contract_address_hash)
)
end
defp page_tokens_holder_count(holder_count, name, contract_address_hash) do
dynamic(
[t],
is_nil(t.holder_count) or t.holder_count < ^holder_count or
(t.holder_count == ^holder_count and ^page_tokens_name(name, contract_address_hash))
)
end
defp page_tokens_name(nil, contract_address_hash) do
dynamic([t], is_nil(t.name) and ^page_tokens_contract_address_hash(contract_address_hash))
end
defp page_tokens_name(name, contract_address_hash) do
dynamic(
[t],
is_nil(t.name) or
(t.name > ^name or (t.name == ^name and ^page_tokens_contract_address_hash(contract_address_hash)))
)
end
defp page_tokens_contract_address_hash(contract_address_hash) do
dynamic([t], t.contract_address_hash > ^contract_address_hash)
end
defp page_blocks(query, %PagingOptions{key: nil}), do: query
defp page_blocks(query, %PagingOptions{key: {block_number}}) do

@ -24,6 +24,7 @@ defmodule Explorer.Chain.Token do
alias Ecto.Changeset
alias Explorer.Chain.{Address, Hash, Token}
alias Explorer.PagingOptions
alias Explorer.SmartContract.Helper
@typedoc """
@ -156,4 +157,178 @@ defmodule Explorer.Chain.Token do
def tokens_by_contract_address_hashes(contract_address_hashes) do
from(token in __MODULE__, where: token.contract_address_hash in ^contract_address_hashes)
end
def base_token_query(type, sorting) do
query = from(t in Token, preload: [:contract_address])
query |> apply_filter(type) |> apply_sorting(sorting)
end
defp apply_filter(query, empty_type) when empty_type in [nil, []], do: query
defp apply_filter(query, token_types) when is_list(token_types) do
from(t in query, where: t.type in ^token_types)
end
@default_sorting [
desc_nulls_last: :circulating_market_cap,
desc_nulls_last: :holder_count,
asc: :name,
asc: :contract_address_hash
]
defp apply_sorting(query, sorting) when is_list(sorting) do
from(t in query, order_by: ^sorting_with_defaults(sorting))
end
defp sorting_with_defaults(sorting) when is_list(sorting) do
(sorting ++ @default_sorting)
|> Enum.uniq_by(fn {_, field} -> field end)
end
def page_tokens(query, paging_options, sorting \\ [])
def page_tokens(query, %PagingOptions{key: nil}, _sorting), do: query
def page_tokens(
query,
%PagingOptions{
key: %{} = key
},
sorting
) do
dynamic_where = sorting |> sorting_with_defaults() |> do_page_tokens()
from(token in query,
where: ^dynamic_where.(key)
)
end
defp do_page_tokens([{order, column} | rest]) do
fn key -> page_tokens_by_column(key, column, order, do_page_tokens(rest)) end
end
defp do_page_tokens([]), do: nil
defp page_tokens_by_column(%{fiat_value: nil} = key, :fiat_value, :desc_nulls_last, next_column) do
dynamic(
[t],
is_nil(t.fiat_value) and ^next_column.(key)
)
end
defp page_tokens_by_column(%{fiat_value: nil} = key, :fiat_value, :asc_nulls_first, next_column) do
next_column.(key)
end
defp page_tokens_by_column(%{fiat_value: fiat_value} = key, :fiat_value, :desc_nulls_last, next_column) do
dynamic(
[t],
is_nil(t.fiat_value) or t.fiat_value < ^fiat_value or
(t.fiat_value == ^fiat_value and ^next_column.(key))
)
end
defp page_tokens_by_column(%{fiat_value: fiat_value} = key, :fiat_value, :asc_nulls_first, next_column) do
dynamic(
[t],
not is_nil(t.fiat_value) and
(t.fiat_value > ^fiat_value or
(t.fiat_value == ^fiat_value and ^next_column.(key)))
)
end
defp page_tokens_by_column(
%{circulating_market_cap: nil} = key,
:circulating_market_cap,
:desc_nulls_last,
next_column
) do
dynamic(
[t],
is_nil(t.circulating_market_cap) and ^next_column.(key)
)
end
defp page_tokens_by_column(
%{circulating_market_cap: nil} = key,
:circulating_market_cap,
:asc_nulls_first,
next_column
) do
next_column.(key)
end
defp page_tokens_by_column(
%{circulating_market_cap: circulating_market_cap} = key,
:circulating_market_cap,
:desc_nulls_last,
next_column
) do
dynamic(
[t],
is_nil(t.circulating_market_cap) or t.circulating_market_cap < ^circulating_market_cap or
(t.circulating_market_cap == ^circulating_market_cap and ^next_column.(key))
)
end
defp page_tokens_by_column(
%{circulating_market_cap: circulating_market_cap} = key,
:circulating_market_cap,
:asc_nulls_first,
next_column
) do
dynamic(
[t],
not is_nil(t.circulating_market_cap) and
(t.circulating_market_cap > ^circulating_market_cap or
(t.circulating_market_cap == ^circulating_market_cap and ^next_column.(key)))
)
end
defp page_tokens_by_column(%{holder_count: nil} = key, :holder_count, :desc_nulls_last, next_column) do
dynamic(
[t],
is_nil(t.holder_count) and ^next_column.(key)
)
end
defp page_tokens_by_column(%{holder_count: nil} = key, :holder_count, :asc_nulls_first, next_column) do
next_column.(key)
end
defp page_tokens_by_column(%{holder_count: holder_count} = key, :holder_count, :desc_nulls_last, next_column) do
dynamic(
[t],
is_nil(t.holder_count) or t.holder_count < ^holder_count or
(t.holder_count == ^holder_count and ^next_column.(key))
)
end
defp page_tokens_by_column(%{holder_count: holder_count} = key, :holder_count, :asc_nulls_first, next_column) do
dynamic(
[t],
not is_nil(t.holder_count) and
(t.holder_count > ^holder_count or
(t.holder_count == ^holder_count and ^next_column.(key)))
)
end
defp page_tokens_by_column(%{name: nil} = key, :name, :asc, next_column) do
dynamic(
[t],
is_nil(t.name) and ^next_column.(key)
)
end
defp page_tokens_by_column(%{name: name} = key, :name, :asc, next_column) do
dynamic(
[t],
is_nil(t.name) or
(t.name > ^name or (t.name == ^name and ^next_column.(key)))
)
end
defp page_tokens_by_column(%{contract_address_hash: contract_address_hash}, :contract_address_hash, :asc, nil) do
dynamic([t], t.contract_address_hash > ^contract_address_hash)
end
end

Loading…
Cancel
Save