Move Token holder's query to Address.CurrentTokenBalance

pull/1010/head
Felipe Renan 6 years ago
parent ed183c988d
commit 3f7dd2bcdd
  1. 4
      apps/block_scout_web/lib/block_scout_web/chain.ex
  2. 8
      apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs
  3. 2
      apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs
  4. 3
      apps/explorer/lib/explorer/chain.ex
  5. 48
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  6. 53
      apps/explorer/lib/explorer/chain/address/token_balance.ex
  7. 149
      apps/explorer/test/explorer/chain/address/current_token_balance_test.exs
  8. 161
      apps/explorer/test/explorer/chain_test.exs

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

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

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

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

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

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

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

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

Loading…
Cancel
Save