alter query to bring balances and count transfers all at once

pull/747/head
Gustavo Santos Ferreira 6 years ago
parent 172a2cd6bc
commit 7e8974c4fb
  1. 8
      apps/block_scout_web/test/block_scout_web/controllers/address_token_controller_test.exs
  2. 25
      apps/explorer/lib/explorer/chain.ex
  3. 48
      apps/explorer/lib/explorer/chain/token.ex
  4. 260
      apps/explorer/test/explorer/chain_test.exs

@ -89,6 +89,14 @@ defmodule BlockScoutWeb.AddressTokenControllerTest do
Enum.each(1..51, fn i ->
token = insert(:token, name: "A Token#{i}", type: "ERC-20")
insert(
:token_balance,
token_contract_address_hash: token.contract_address_hash,
address: address,
value: 1000
)
insert(:token_transfer, token_contract_address: token.contract_address, from_address: address)
end)

@ -1714,36 +1714,11 @@ defmodule Explorer.Chain do
@spec tokens_with_number_of_transfers_from_address(Hash.Address.t(), [any()]) :: []
def tokens_with_number_of_transfers_from_address(address_hash, paging_options \\ []) do
address_hash
|> fetch_tokens_from_address_hash(paging_options)
|> add_number_of_transfers_to_tokens_from_address(address_hash)
end
@spec fetch_tokens_from_address_hash(Hash.Address.t(), [any()]) :: []
def fetch_tokens_from_address_hash(address_hash, paging_options \\ []) do
address_hash
|> Token.with_transfers_by_address(paging_options)
|> Repo.all()
end
@spec add_number_of_transfers_to_tokens_from_address([Token], Hash.Address.t()) :: []
defp add_number_of_transfers_to_tokens_from_address(tokens, address_hash) do
Enum.map(tokens, fn token ->
Map.put(
token,
:number_of_transfers,
count_token_transfers_from_address_hash(token.contract_address_hash, address_hash)
)
end)
end
@spec count_token_transfers_from_address_hash(Hash.Address.t(), Hash.Address.t()) :: []
def count_token_transfers_from_address_hash(token_hash, address_hash) do
token_hash
|> Token.interactions_with_address(address_hash)
|> Repo.aggregate(:count, :name)
end
@doc """
Update a new `t:Token.t/0` record.

@ -82,7 +82,11 @@ defmodule Explorer.Chain.Token do
end
@doc """
Builds an `Ecto.Query` to fetch tokens that the given address has interacted with.
Builds an `Ecto.Query` to fetch tokens that the given address has interacted with
along with the latest balance of each token for that address and the count of
transfers the address had with each token
warning: tokens with a latest balance of zero will be ignored
In order to fetch a token, the given address must have transfered tokens to or received tokens
from another address. This quey orders by the token type and name.
@ -91,36 +95,48 @@ defmodule Explorer.Chain.Token do
def with_transfers_by_address(address_hash, options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
subquery =
query =
from(
token in Token,
join: tt in TokenTransfer,
on: tt.token_contract_address_hash == token.contract_address_hash,
where: tt.to_address_hash == ^address_hash or tt.from_address_hash == ^address_hash,
distinct: [:contract_address_hash]
tb in TokenBalance,
where: tb.address_hash == ^address_hash,
distinct: :token_contract_address_hash,
order_by: [desc: :block_number],
select: %{value: tb.value, token_contract_address_hash: tb.token_contract_address_hash}
)
query = from(t in subquery(subquery), order_by: [desc: :type, asc: :name])
query
|> with_last_balance_and_transfers_count(address_hash)
|> page_token(paging_options)
|> limit(^paging_options.page_size)
end
@doc """
Builds an `Ecto.Query` to fetch the transactions between a token and an address.
"""
def interactions_with_address(token_hash, address_hash) do
defp with_last_balance_and_transfers_count(last_balance_query, address_hash) do
from(
t in Token,
join: tt in TokenTransfer,
on: tt.token_contract_address_hash == t.contract_address_hash,
where: t.contract_address_hash == ^token_hash,
where: tt.to_address_hash == ^address_hash or tt.from_address_hash == ^address_hash,
select: tt
join: tb in subquery(last_balance_query),
on: tb.token_contract_address_hash == t.contract_address_hash,
order_by: [desc: t.type, asc: fragment("UPPER(?)", t.name)],
where: (tt.to_address_hash == ^address_hash or tt.from_address_hash == ^address_hash) and tb.value > 0,
group_by: [t.name, t.symbol, tb.value, t.type, t.contract_address_hash],
select: %{
contract_address_hash: t.contract_address_hash,
inserted_at: max(t.inserted_at),
name: t.name,
symbol: t.symbol,
value: tb.value,
decimals: max(t.decimals),
type: t.type,
transfers: count(t.name)
}
)
end
@doc """
adds to the passed `Ecto.Query` a criteria indicating the first token of the current page
returns the original query if the received `PagingOptions` has a nil value for the key attribute
"""
def page_token(query, %PagingOptions{key: nil}), do: query
def page_token(query, %PagingOptions{key: {name, type, inserted_at}}) do

@ -2320,14 +2320,21 @@ defmodule Explorer.ChainTest do
end
describe "tokens_with_number_of_transfers_from_address/2" do
test "returns tokens with number of transfers attached" do
test "returns tokens with number of transfers and balance value attached" do
address = insert(:address)
token =
:token
|> insert(name: "token-c", type: "ERC-721")
|> insert(name: "token-c", type: "ERC-721", decimals: 0, symbol: "TC")
|> Repo.preload(:contract_address)
insert(
:token_balance,
address: address,
token_contract_address_hash: token.contract_address_hash,
value: 1000
)
insert(
:token_transfer,
token_contract_address: token.contract_address,
@ -2347,228 +2354,100 @@ defmodule Explorer.ChainTest do
|> Chain.tokens_with_number_of_transfers_from_address()
|> List.first()
assert fetched_token.name == "token-c"
assert fetched_token.number_of_transfers == 2
assert fetched_token == %{
contract_address_hash: token.contract_address_hash,
inserted_at: token.inserted_at,
name: "token-c",
symbol: "TC",
value: Decimal.new(1000),
decimals: 0,
type: "ERC-721",
transfers: 2
}
end
end
describe "fetch_tokens_from_address_hash/1" do
test "only returns tokens that a given address has interacted with" do
alice = insert(:address)
token_a =
:token
|> insert(name: "token-1")
|> Repo.preload(:contract_address)
token_b =
:token
|> insert(name: "token-2")
|> Repo.preload(:contract_address)
test "does not return tokens with zero balance" do
address = insert(:address)
token_c =
token =
:token
|> insert(name: "token-3")
|> insert(name: "atoken", type: "ERC-721", decimals: 0, symbol: "AT")
|> Repo.preload(:contract_address)
insert(
:token_transfer,
token_contract_address: token_a.contract_address,
from_address: alice,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: token_b.contract_address,
from_address: build(:address),
to_address: alice
)
insert(
:token_transfer,
token_contract_address: token_c.contract_address,
from_address: build(:address),
to_address: build(:address)
:token_balance,
address: address,
token_contract_address_hash: token.contract_address_hash,
value: 0
)
expected_tokens =
alice.hash
|> Chain.fetch_tokens_from_address_hash()
|> Enum.map(& &1.name)
fetched_token =
address.hash
|> Chain.tokens_with_number_of_transfers_from_address()
|> Enum.find(fn t -> t.name == "atoken" end)
assert expected_tokens == [token_a.name, token_b.name]
assert fetched_token == nil
end
test "returns an empty list when the given address hasn't interacted with any tokens" do
alice = insert(:address)
test "brings the value of the last balance" do
address = insert(:address)
token =
:token
|> insert(name: "token-1")
|> insert(name: "atoken", type: "ERC-721", decimals: 0, symbol: "AT")
|> Repo.preload(:contract_address)
insert(
:token_transfer,
token_contract_address: token.contract_address,
from_address: build(:address),
to_address: build(:address)
:token_balance,
address: address,
token_contract_address_hash: token.contract_address_hash,
value: 1000
)
assert Chain.fetch_tokens_from_address_hash(alice.hash) == []
end
test "distinct tokens by contract_address_hash" do
alice = insert(:address)
token =
:token
|> insert(name: "token-1")
|> Repo.preload(:contract_address)
insert(
:token_transfer,
token_contract_address: token.contract_address,
from_address: alice,
to_address: build(:address)
:token_balance,
address: address,
token_contract_address_hash: token.contract_address_hash,
value: 1234
)
insert(
:token_transfer,
token_contract_address: token.contract_address,
from_address: build(:address),
to_address: alice
)
expected_tokens =
alice.hash
|> Chain.fetch_tokens_from_address_hash()
|> Enum.map(& &1.name)
assert expected_tokens == [token.name]
end
test "orders by type, name and inserted_at time" do
address = insert(:address)
first_token =
:token
|> insert(name: "token-c", type: "ERC-721")
|> Repo.preload(:contract_address)
second_token =
:token
|> insert(name: "token-a", type: "ERC-20")
|> Repo.preload(:contract_address)
third_token =
:token
|> insert(name: "token-b", type: "ERC-20")
|> Repo.preload(:contract_address)
fourth_token =
:token
|> insert(name: "token-b", type: "ERC-20", inserted_at: third_token.inserted_at)
|> Repo.preload(:contract_address)
insert(
:token_transfer,
token_contract_address: first_token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: second_token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: third_token.contract_address,
from_address: build(:address),
to_address: address
)
insert(
:token_transfer,
token_contract_address: fourth_token.contract_address,
from_address: build(:address),
to_address: address
)
fetched_tokens =
fetched_token =
address.hash
|> Chain.fetch_tokens_from_address_hash()
|> Enum.map(&Repo.preload(&1, :contract_address))
|> Chain.tokens_with_number_of_transfers_from_address()
|> List.first()
assert fetched_tokens == [first_token, second_token, third_token, fourth_token]
assert fetched_token.value == Decimal.new(1234)
end
test "supports pagination" do
test "ignores token if the last balance is zero" do
address = insert(:address)
first_token =
:token
|> insert(name: "token-c", type: "ERC-721")
|> Repo.preload(:contract_address)
second_token =
:token
|> insert(name: "token-a", type: "ERC-20")
|> Repo.preload(:contract_address)
third_token =
token =
:token
|> insert(name: "token-b", type: "ERC-20")
|> insert(name: "atoken", type: "ERC-721", decimals: 0, symbol: "AT")
|> Repo.preload(:contract_address)
paging_options = %PagingOptions{
page_size: 1,
key: {first_token.name, first_token.type, first_token.inserted_at}
}
insert(
:token_transfer,
token_contract_address: first_token.contract_address,
from_address: address,
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: second_token.contract_address,
from_address: address,
to_address: build(:address)
:token_balance,
address: address,
token_contract_address_hash: token.contract_address_hash,
value: 1000
)
insert(
:token_transfer,
token_contract_address: third_token.contract_address,
from_address: build(:address),
to_address: address
:token_balance,
address: address,
token_contract_address_hash: token.contract_address_hash,
value: 0
)
fetched_tokens =
address.hash
|> Chain.fetch_tokens_from_address_hash(paging_options: paging_options)
|> Enum.map(& &1.name)
assert fetched_tokens == [second_token.name]
end
end
describe "count_token_transfers_from_address_hash/2" do
test "returns the number of times an address has interacted with a token" do
address = insert(:address)
token =
:token
|> insert(name: "token")
|> Repo.preload(:contract_address)
insert(
:token_transfer,
token_contract_address: token.contract_address,
@ -2576,25 +2455,12 @@ defmodule Explorer.ChainTest do
to_address: build(:address)
)
insert(
:token_transfer,
token_contract_address: token.contract_address,
from_address: build(:address),
to_address: address
)
assert Chain.count_token_transfers_from_address_hash(token.contract_address.hash, address.hash) == 2
end
test "returns 0 if no interaction is found" do
address = insert(:address)
token =
:token
|> insert(name: "token")
|> Repo.preload(:contract_address)
fetched_token =
address.hash
|> Chain.tokens_with_number_of_transfers_from_address()
|> List.first()
assert Chain.count_token_transfers_from_address_hash(token.contract_address.hash, address.hash) == 0
assert fetched_token == nil
end
end

Loading…
Cancel
Save