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 bb3511a1b3..08e4e5c321 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.Chain do alias Explorer.Chain.{ Address, - Address.TokenBalance, + Address.CurrentTokenBalance, Block, InternalTransaction, Log, @@ -198,7 +198,7 @@ defmodule BlockScoutWeb.Chain do %{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime} end - defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do + defp paging_params(%CurrentTokenBalance{address_hash: address_hash, value: value}) do %{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)} end diff --git a/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs index 6ce8d7915b..66c8ce9de4 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs @@ -22,7 +22,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do insert_list( 2, - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash ) @@ -43,7 +43,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do 1..50 |> Enum.map( &insert( - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash, value: &1 + 1000 ) @@ -52,7 +52,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do token_balance = insert( - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash, value: 50000 ) @@ -78,7 +78,7 @@ defmodule BlockScoutWeb.Tokens.HolderControllerTest do Enum.each( 1..51, &insert( - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash, value: &1 + 1000 ) diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs index 8a5d769e62..06f1255964 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs @@ -9,7 +9,7 @@ defmodule BlockScoutWeb.ViewingTokensTest do insert_list( 2, - :token_balance, + :address_current_token_balance, token_contract_address_hash: token.contract_address_hash ) diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 2140e36b14..51eac0ed1a 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -23,6 +23,7 @@ defmodule Explorer.Chain do alias Explorer.Chain.{ Address, Address.CoinBalance, + Address.CurrentTokenBalance, Address.TokenBalance, Block, Data, @@ -2070,7 +2071,7 @@ defmodule Explorer.Chain do @spec fetch_token_holders_from_token_hash(Hash.Address.t(), [paging_options]) :: [TokenBalance.t()] def fetch_token_holders_from_token_hash(contract_address_hash, options) do contract_address_hash - |> TokenBalance.token_holders_ordered_by_value(options) + |> CurrentTokenBalance.token_holders_ordered_by_value(options) |> Repo.all() end diff --git a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex index 448ae7da34..21f20e1f8c 100644 --- a/apps/explorer/lib/explorer/chain/address/current_token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/current_token_balance.ex @@ -5,9 +5,13 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do use Ecto.Schema import Ecto.Changeset + import Ecto.Query, only: [from: 2, limit: 2, order_by: 3, preload: 2, where: 3] + alias Explorer.{Chain, PagingOptions} alias Explorer.Chain.{Address, Block, Hash, Token} + @default_paging_options %PagingOptions{page_size: 50} + @typedoc """ * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address_hash` - The address hash foreign key. @@ -57,4 +61,48 @@ defmodule Explorer.Chain.Address.CurrentTokenBalance do |> foreign_key_constraint(:address_hash) |> foreign_key_constraint(:token_contract_address_hash) end + + {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") + @burn_address_hash burn_address_hash + + @doc """ + Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash. + + The Token Holders are the addresses that own a positive amount of the Token. So this query is + considering the following conditions: + + * The token balance from the last block. + * Balances greater than 0. + * Excluding the burn address (0x0000000000000000000000000000000000000000). + + """ + def token_holders_ordered_by_value(token_contract_address_hash, options \\ []) do + paging_options = Keyword.get(options, :paging_options, @default_paging_options) + + token_contract_address_hash + |> token_holders_query + |> preload(:address) + |> order_by([tb], desc: :value) + |> page_token_balances(paging_options) + |> limit(^paging_options.page_size) + end + + defp token_holders_query(token_contract_address_hash) do + from( + tb in __MODULE__, + where: tb.token_contract_address_hash == ^token_contract_address_hash, + where: tb.address_hash != ^@burn_address_hash, + where: tb.value > 0 + ) + end + + defp page_token_balances(query, %PagingOptions{key: nil}), do: query + + defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do + where( + query, + [tb], + tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash) + ) + end end diff --git a/apps/explorer/lib/explorer/chain/address/token_balance.ex b/apps/explorer/lib/explorer/chain/address/token_balance.ex index 2b1c4f64b8..383eb24ec2 100644 --- a/apps/explorer/lib/explorer/chain/address/token_balance.ex +++ b/apps/explorer/lib/explorer/chain/address/token_balance.ex @@ -5,14 +5,12 @@ defmodule Explorer.Chain.Address.TokenBalance do use Ecto.Schema import Ecto.Changeset - import Ecto.Query, only: [from: 2, limit: 2, where: 3, subquery: 1, order_by: 3, preload: 2] + import Ecto.Query, only: [from: 2, subquery: 1] - alias Explorer.{Chain, PagingOptions} + alias Explorer.Chain alias Explorer.Chain.Address.TokenBalance alias Explorer.Chain.{Address, Block, Hash, Token} - @default_paging_options %PagingOptions{page_size: 50} - @typedoc """ * `address` - The `t:Explorer.Chain.Address.t/0` that is the balance's owner. * `address_hash` - The address hash foreign key. @@ -84,43 +82,6 @@ defmodule Explorer.Chain.Address.TokenBalance do from(tb in subquery(query), where: tb.value > 0, preload: :token) end - @doc """ - Builds an `Ecto.Query` to fetch the token holders from the given token contract address hash. - - The Token Holders are the addresses that own a positive amount of the Token. So this query is - considering the following conditions: - - * The token balance from the last block. - * Balances greater than 0. - * Excluding the burn address (0x0000000000000000000000000000000000000000). - - """ - def token_holders_from_token_hash(token_contract_address_hash) do - query = token_holders_query(token_contract_address_hash) - - from(tb in subquery(query), where: tb.value > 0) - end - - def token_holders_ordered_by_value(token_contract_address_hash, options) do - paging_options = Keyword.get(options, :paging_options, @default_paging_options) - - token_contract_address_hash - |> token_holders_from_token_hash() - |> order_by([tb], desc: tb.value, desc: tb.address_hash) - |> preload(:address) - |> page_token_balances(paging_options) - |> limit(^paging_options.page_size) - end - - defp token_holders_query(contract_address_hash) do - from( - tb in TokenBalance, - distinct: :address_hash, - where: tb.token_contract_address_hash == ^contract_address_hash and tb.address_hash != ^@burn_address_hash, - order_by: [desc: :block_number] - ) - end - @doc """ Builds an `Ecto.Query` to group all tokens with their number of holders. """ @@ -144,16 +105,6 @@ defmodule Explorer.Chain.Address.TokenBalance do ) end - defp page_token_balances(query, %PagingOptions{key: nil}), do: query - - defp page_token_balances(query, %PagingOptions{key: {value, address_hash}}) do - where( - query, - [tb], - tb.value < ^value or (tb.value == ^value and tb.address_hash < ^address_hash) - ) - end - @doc """ Builds an `Ecto.Query` to fetch the unfetched token balances. diff --git a/apps/explorer/test/explorer/chain/address/current_token_balance_test.exs b/apps/explorer/test/explorer/chain/address/current_token_balance_test.exs new file mode 100644 index 0000000000..f02538f96b --- /dev/null +++ b/apps/explorer/test/explorer/chain/address/current_token_balance_test.exs @@ -0,0 +1,149 @@ +defmodule Explorer.Chain.Address.CurrentTokenBalanceTest do + use Explorer.DataCase + + alias Explorer.{Chain, PagingOptions, Repo} + alias Explorer.Chain.Token + alias Explorer.Chain.Address.CurrentTokenBalance + + describe "token_holders_ordered_by_value/2" do + test "returns the last value for each address" do + %Token{contract_address_hash: contract_address_hash} = insert(:token) + address_a = insert(:address) + address_b = insert(:address) + + insert( + :address_current_token_balance, + address: address_a, + token_contract_address_hash: contract_address_hash, + value: 5000 + ) + + insert( + :address_current_token_balance, + address: address_b, + block_number: 1001, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + token_holders_count = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + |> Enum.count() + + assert token_holders_count == 2 + end + + test "sort by the highest value" do + %Token{contract_address_hash: contract_address_hash} = insert(:token) + address_a = insert(:address) + address_b = insert(:address) + address_c = insert(:address) + + insert( + :address_current_token_balance, + address: address_a, + token_contract_address_hash: contract_address_hash, + value: 5000 + ) + + insert( + :address_current_token_balance, + address: address_b, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + insert( + :address_current_token_balance, + address: address_c, + token_contract_address_hash: contract_address_hash, + value: 15000 + ) + + token_holders_values = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + |> Enum.map(&Decimal.to_integer(&1.value)) + + assert token_holders_values == [15_000, 5_000, 4_000] + end + + test "returns only token balances that have value greater than 0" do + %Token{contract_address_hash: contract_address_hash} = insert(:token) + + insert( + :address_current_token_balance, + token_contract_address_hash: contract_address_hash, + value: 0 + ) + + result = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + + assert result == [] + end + + test "ignores the burn address" do + {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") + + burn_address = insert(:address, hash: burn_address_hash) + + %Token{contract_address_hash: contract_address_hash} = insert(:token) + + insert( + :address_current_token_balance, + address: burn_address, + token_contract_address_hash: contract_address_hash, + value: 1000 + ) + + result = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value() + |> Repo.all() + + assert result == [] + end + + test "paginates the result by value and different address" do + address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a") + address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354") + + %Token{contract_address_hash: contract_address_hash} = insert(:token) + + first_page = + insert( + :address_current_token_balance, + address: address_a, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + second_page = + insert( + :address_current_token_balance, + address: address_b, + token_contract_address_hash: contract_address_hash, + value: 4000 + ) + + paging_options = %PagingOptions{ + key: {first_page.value, first_page.address_hash}, + page_size: 2 + } + + result_paginated = + contract_address_hash + |> CurrentTokenBalance.token_holders_ordered_by_value(paging_options: paging_options) + |> Repo.all() + |> Enum.map(& &1.address_hash) + + assert result_paginated == [second_page.address_hash] + end + end +end diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 414a54e217..0634f02bf0 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -2956,173 +2956,32 @@ defmodule Explorer.ChainTest do end describe "fetch_token_holders_from_token_hash/2" do - test "returns the last value for each address" do + test "returns the token holders" do %Token{contract_address_hash: contract_address_hash} = insert(:token) - address = insert(:address) + address_a = insert(:address) + address_b = insert(:address) insert( - :token_balance, - address: address, - block_number: 1000, + :address_current_token_balance, + address: address_a, token_contract_address_hash: contract_address_hash, value: 5000 ) insert( - :token_balance, - block_number: 1001, - token_contract_address_hash: contract_address_hash, - value: 4000 - ) - - insert( - :token_balance, - address: address, - block_number: 1002, - token_contract_address_hash: contract_address_hash, - value: 2000 - ) - - values = - contract_address_hash - |> Chain.fetch_token_holders_from_token_hash([]) - |> Enum.map(&Decimal.to_integer(&1.value)) - - assert values == [4000, 2000] - end - - test "sort by the highest value" do - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - block_number: 1000, - token_contract_address_hash: contract_address_hash, - value: 2000 - ) - - insert( - :token_balance, + :address_current_token_balance, + address: address_b, block_number: 1001, token_contract_address_hash: contract_address_hash, - value: 1000 - ) - - insert( - :token_balance, - block_number: 1002, - token_contract_address_hash: contract_address_hash, value: 4000 ) - insert( - :token_balance, - block_number: 1002, - token_contract_address_hash: contract_address_hash, - value: 3000 - ) - - values = + token_holders_count = contract_address_hash |> Chain.fetch_token_holders_from_token_hash([]) - |> Enum.map(&Decimal.to_integer(&1.value)) - - assert values == [4000, 3000, 2000, 1000] - end - - test "returns only token balances that have value" do - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - token_contract_address_hash: contract_address_hash, - value: 0 - ) - - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] - end - - test "returns an empty list when there are no address with value greater than 0" do - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert(:token_balance, value: 1000) - - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] - end - - test "ignores the burn address" do - {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") - - burn_address = insert(:address, hash: burn_address_hash) - - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - address: burn_address, - token_contract_address_hash: contract_address_hash, - value: 1000 - ) - - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] - end - - test "paginates the result by value and different address" do - address_a = build(:address, hash: "0xcb2cf1fd3199584ac5faa16c6aca49472dc6495a") - address_b = build(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354") - - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - first_page = - insert( - :token_balance, - address: address_a, - token_contract_address_hash: contract_address_hash, - value: 4000 - ) - - second_page = - insert( - :token_balance, - address: address_b, - token_contract_address_hash: contract_address_hash, - value: 4000 - ) - - paging_options = %PagingOptions{ - key: {first_page.value, first_page.address_hash}, - page_size: 2 - } - - holders_paginated = - contract_address_hash - |> Chain.fetch_token_holders_from_token_hash(paging_options: paging_options) - |> Enum.map(& &1.address_hash) - - assert holders_paginated == [second_page.address_hash] - end - - test "considers the last block only if it has value" do - address = insert(:address, hash: "0x5f26097334b6a32b7951df61fd0c5803ec5d8354") - %Token{contract_address_hash: contract_address_hash} = insert(:token) - - insert( - :token_balance, - address: address, - block_number: 1000, - token_contract_address_hash: contract_address_hash, - value: 5000 - ) - - insert( - :token_balance, - address: address, - block_number: 1002, - token_contract_address_hash: contract_address_hash, - value: 0 - ) + |> Enum.count() - assert Chain.fetch_token_holders_from_token_hash(contract_address_hash, []) == [] + assert token_holders_count == 2 end end