From d52bb7acc7d94fe2d660a3a4ac136cd77cd0d980 Mon Sep 17 00:00:00 2001 From: Maxim Filonov <53992153+sl1depengwyn@users.noreply.github.com> Date: Mon, 10 Jul 2023 14:52:06 +0300 Subject: [PATCH] API v2: Add sorting to tokens page --- CHANGELOG.md | 1 + .../lib/block_scout_web/chain.ex | 38 ++- .../controllers/api/v2/token_controller.ex | 3 +- .../controllers/tokens/tokens_controller.ex | 1 - .../lib/block_scout_web/paging_helper.ex | 34 ++- .../api/v2/token_controller_test.exs | 240 +++++++++++++++++- apps/explorer/lib/explorer/chain.ex | 89 +------ apps/explorer/lib/explorer/chain/token.ex | 175 +++++++++++++ 8 files changed, 469 insertions(+), 112 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ae959948d..c7300ba061 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 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 488f14279b..6409cc2053 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex index fdde35d0bd..647e38ab62 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/v2/token_controller.ex @@ -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() diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex index 4e4f289fcf..a0bae11f5e 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/tokens/tokens_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex index 98fd11f72d..bcbbc9b0a2 100644 --- a/apps/block_scout_web/lib/block_scout_web/paging_helper.ex +++ b/apps/block_scout_web/lib/block_scout_web/paging_helper.ex @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs index f46da387f7..fbed9880a6 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/api/v2/token_controller_test.exs @@ -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) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index f2d2e5f023..c9484c673e 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/token.ex b/apps/explorer/lib/explorer/chain/token.ex index 4701f109c2..b59f6ef2de 100644 --- a/apps/explorer/lib/explorer/chain/token.ex +++ b/apps/explorer/lib/explorer/chain/token.ex @@ -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