Merge branch 'master' into ag-clique-miner

pull/1028/head
Alex Garibay 6 years ago committed by GitHub
commit fca47ac4aa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      apps/block_scout_web/lib/block_scout_web/chain.ex
  2. 12
      apps/block_scout_web/lib/block_scout_web/resolvers/address.ex
  3. 10
      apps/block_scout_web/lib/block_scout_web/router.ex
  4. 9
      apps/block_scout_web/lib/block_scout_web/schema.ex
  5. 19
      apps/block_scout_web/lib/block_scout_web/schema/scalars.ex
  6. 11
      apps/block_scout_web/lib/block_scout_web/schema/types.ex
  7. 8
      apps/block_scout_web/test/block_scout_web/controllers/tokens/holder_controller_test.exs
  8. 2
      apps/block_scout_web/test/block_scout_web/features/viewing_tokens_test.exs
  9. 142
      apps/block_scout_web/test/block_scout_web/schema/query/address_test.exs
  10. 2
      apps/block_scout_web/test/block_scout_web/schema/query/block_test.exs
  11. 3
      apps/explorer/lib/explorer/chain.ex
  12. 108
      apps/explorer/lib/explorer/chain/address/current_token_balance.ex
  13. 53
      apps/explorer/lib/explorer/chain/address/token_balance.ex
  14. 1
      apps/explorer/lib/explorer/chain/import.ex
  15. 124
      apps/explorer/lib/explorer/chain/import/address/current_token_balances.ex
  16. 32
      apps/explorer/priv/repo/migrations/20181026180921_create_address_current_token_balances.exs
  17. 149
      apps/explorer/test/explorer/chain/address/current_token_balance_test.exs
  18. 95
      apps/explorer/test/explorer/chain/import/address/address/current_token_balances_test.exs
  19. 50
      apps/explorer/test/explorer/chain/import_test.exs
  20. 161
      apps/explorer/test/explorer/chain_test.exs
  21. 11
      apps/explorer/test/support/factory.ex
  22. 1
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  23. 8
      apps/indexer/lib/indexer/token_balance/fetcher.ex
  24. 11
      apps/indexer/lib/indexer/token_balances.ex
  25. 25
      apps/indexer/test/indexer/token_balances_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

@ -0,0 +1,12 @@
defmodule BlockScoutWeb.Resolvers.Address do
@moduledoc false
alias Explorer.Chain
def get_by(_, %{hashes: hashes}, _) do
case Chain.hashes_to_addresses(hashes) do
[] -> {:error, "Addresses not found."}
result -> {:ok, result}
end
end
end

@ -38,12 +38,18 @@ defmodule BlockScoutWeb.Router do
})
end
forward("/graphql", Absinthe.Plug, schema: BlockScoutWeb.Schema)
forward("/graphql", Absinthe.Plug,
schema: BlockScoutWeb.Schema,
analyze_complexity: true,
max_complexity: 50
)
forward("/graphiql", Absinthe.Plug.GraphiQL,
schema: BlockScoutWeb.Schema,
interface: :playground,
socket: BlockScoutWeb.UserSocket
socket: BlockScoutWeb.UserSocket,
analyze_complexity: true,
max_complexity: 50
)
scope "/", BlockScoutWeb do

@ -3,11 +3,18 @@ defmodule BlockScoutWeb.Schema do
use Absinthe.Schema
alias BlockScoutWeb.Resolvers.{Block, Transaction}
alias BlockScoutWeb.Resolvers.{Address, Block, Transaction}
import_types(BlockScoutWeb.Schema.Types)
query do
@desc "Gets addresses by address hash."
field :addresses, list_of(:address) do
arg(:hashes, non_null(list_of(non_null(:address_hash))))
resolve(&Address.get_by/3)
complexity(fn %{hashes: hashes}, child_complexity -> length(hashes) * child_complexity end)
end
@desc "Gets a block by number."
field :block, :block do
arg(:number, non_null(:integer))

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Schema.Scalars do
use Absinthe.Schema.Notation
alias Explorer.Chain.{Hash, Wei}
alias Explorer.Chain.{Data, Hash, Wei}
alias Explorer.Chain.Hash.{Address, Full, Nonce}
@desc """
@ -24,6 +24,23 @@ defmodule BlockScoutWeb.Schema.Scalars do
serialize(&to_string/1)
end
@desc """
An unpadded hexadecimal number with 0 or more digits. Each pair of digits
maps directly to a byte in the underlying binary representation. When
interpreted as a number, it should be treated as big-endian.
"""
scalar :data do
parse(fn
%Absinthe.Blueprint.Input.String{value: value} ->
Data.cast(value)
_ ->
:error
end)
serialize(&to_string/1)
end
@desc """
A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash.
"""

@ -6,12 +6,23 @@ defmodule BlockScoutWeb.Schema.Types do
import_types(Absinthe.Type.Custom)
import_types(BlockScoutWeb.Schema.Scalars)
@desc """
A stored representation of a Web3 address.
"""
object :address do
field(:hash, :address_hash)
field(:fetched_coin_balance, :wei)
field(:fetched_coin_balance_block_number, :integer)
field(:contract_code, :data)
end
@desc """
A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally
other data. Because each block (except for the initial "genesis block") points to the previous block, the data
structure that they form is called a "blockchain".
"""
object :block do
field(:hash, :full_hash)
field(:consensus, :boolean)
field(:difficulty, :decimal)
field(:gas_limit, :decimal)

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

@ -0,0 +1,142 @@
defmodule BlockScoutWeb.Schema.Query.AddressTest do
use BlockScoutWeb.ConnCase
describe "address field" do
test "with valid argument 'hashes', returns all expected fields", %{conn: conn} do
address = insert(:address, fetched_coin_balance: 100)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
hash
fetched_coin_balance
fetched_coin_balance_block_number
contract_code
}
}
"""
variables = %{"hashes" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"addresses" => [
%{
"hash" => to_string(address.hash),
"fetched_coin_balance" => to_string(address.fetched_coin_balance.value),
"fetched_coin_balance_block_number" => address.fetched_coin_balance_block_number,
"contract_code" => nil
}
]
}
}
end
test "with contract address, `contract_code` is serialized as expected", %{conn: conn} do
address = insert(:contract_address, fetched_coin_balance: 100)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
contract_code
}
}
"""
variables = %{"hashes" => to_string(address.hash)}
conn = get(conn, "/graphql", query: query, variables: variables)
assert json_response(conn, 200) == %{
"data" => %{
"addresses" => [
%{
"contract_code" => to_string(address.contract_code)
}
]
}
}
end
test "errors for non-existent address hashes", %{conn: conn} do
address = build(:address)
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
fetched_coin_balance
}
}
"""
variables = %{"hashes" => [to_string(address.hash)]}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Addresses not found.)
end
test "errors if argument 'hashes' is missing", %{conn: conn} do
query = """
query {
addresses {
fetched_coin_balance
}
}
"""
variables = %{}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] == ~s(In argument "hashes": Expected type "[AddressHash!]!", found null.)
end
test "errors if argument 'hashes' is not a list of address hashes", %{conn: conn} do
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
fetched_coin_balance
}
}
"""
variables = %{"hashes" => ["someInvalidHash"]}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error]} = json_response(conn, 200)
assert error["message"] =~ ~s(Argument "hashes" has invalid value)
end
test "correlates complexity to size of 'hashes' argument", %{conn: conn} do
# max of 12 addresses with four fields of complexity 1 can be fetched
# per query:
# 12 * 4 = 48, which is less than a max complexity of 50
hashes = 13 |> build_list(:address) |> Enum.map(&to_string(&1.hash))
query = """
query ($hashes: [AddressHash!]!) {
addresses(hashes: $hashes) {
hash
fetched_coin_balance
fetched_coin_balance_block_number
contract_code
}
}
"""
variables = %{"hashes" => hashes}
conn = get(conn, "/graphql", query: query, variables: variables)
assert %{"errors" => [error1, error2]} = json_response(conn, 200)
assert error1["message"] =~ ~s(Field addresses is too complex)
assert error2["message"] =~ ~s(Operation is too complex)
end
end
end

@ -8,6 +8,7 @@ defmodule BlockScoutWeb.Schema.Query.BlockTest do
query = """
query ($number: Int!) {
block(number: $number) {
hash
consensus
difficulty
gas_limit
@ -31,6 +32,7 @@ defmodule BlockScoutWeb.Schema.Query.BlockTest do
assert json_response(conn, 200) == %{
"data" => %{
"block" => %{
"hash" => to_string(block.hash),
"consensus" => block.consensus,
"difficulty" => to_string(block.difficulty),
"gas_limit" => to_string(block.gas_limit),

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

@ -0,0 +1,108 @@
defmodule Explorer.Chain.Address.CurrentTokenBalance do
@moduledoc """
Represents the current token balance from addresses according to the last block.
"""
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.
* `token` - The `t:Explorer.Chain.Token/0` so that the address has the balance.
* `token_contract_address_hash` - The contract address hash foreign key.
* `block_number` - The block's number that the transfer took place.
* `value` - The value that's represents the balance.
"""
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Address.t(),
token: %Ecto.Association.NotLoaded{} | Token.t(),
token_contract_address_hash: Hash.Address,
block_number: Block.block_number(),
inserted_at: DateTime.t(),
updated_at: DateTime.t(),
value: Decimal.t() | nil
}
schema "address_current_token_balances" do
field(:value, :decimal)
field(:block_number, :integer)
field(:value_fetched_at, :utc_datetime)
belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address)
belongs_to(
:token,
Token,
foreign_key: :token_contract_address_hash,
references: :contract_address_hash,
type: Hash.Address
)
timestamps()
end
@optional_fields ~w(value value_fetched_at)a
@required_fields ~w(address_hash block_number token_contract_address_hash)a
@allowed_fields @optional_fields ++ @required_fields
@doc false
def changeset(%__MODULE__{} = token_balance, attrs) do
token_balance
|> cast(attrs, @allowed_fields)
|> validate_required(@required_fields)
|> 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.

@ -19,6 +19,7 @@ defmodule Explorer.Chain.Import do
Import.Logs,
Import.Tokens,
Import.TokenTransfers,
Import.Address.CurrentTokenBalances,
Import.Address.TokenBalances
]

@ -0,0 +1,124 @@
defmodule Explorer.Chain.Import.Address.CurrentTokenBalances do
@moduledoc """
Bulk imports `t:Explorer.Chain.Address.CurrentTokenBalance.t/0`.
"""
require Ecto.Query
import Ecto.Query, only: [from: 2]
alias Ecto.{Changeset, Multi}
alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Chain.Import
@behaviour Import.Runner
# milliseconds
@timeout 60_000
@type imported :: [CurrentTokenBalance.t()]
@impl Import.Runner
def ecto_schema_module, do: CurrentTokenBalance
@impl Import.Runner
def option_key, do: :address_current_token_balances
@impl Import.Runner
def imported_table_row do
%{
value_type: "[#{ecto_schema_module()}.t()]",
value_description: "List of `t:#{ecto_schema_module()}.t/0`s"
}
end
@impl Import.Runner
def run(multi, changes_list, %{timestamps: timestamps} = options) do
insert_options =
options
|> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a)
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
Multi.run(multi, :address_current_token_balances, fn _ ->
insert(changes_list, insert_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
@spec insert([map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout(),
required(:timestamps) => Import.timestamps()
}) ::
{:ok, [CurrentTokenBalance.t()]}
| {:error, [Changeset.t()]}
def insert(changes_list, %{timeout: timeout, timestamps: timestamps} = options) when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
{:ok, _} =
Import.insert_changes_list(
unique_token_balances(changes_list),
conflict_target: ~w(address_hash token_contract_address_hash)a,
on_conflict: on_conflict,
for: CurrentTokenBalance,
returning: true,
timeout: timeout,
timestamps: timestamps
)
end
# Remove duplicated token balances based on `{address_hash, token_hash}` considering the last block
# to avoid `cardinality_violation` error in Postgres. This error happens when there are duplicated
# rows being inserted.
defp unique_token_balances(changes_list) do
changes_list
|> Enum.sort(&(&1.block_number > &2.block_number))
|> Enum.uniq_by(fn %{address_hash: address_hash, token_contract_address_hash: token_hash} ->
{address_hash, token_hash}
end)
end
defp default_on_conflict do
from(
current_token_balance in CurrentTokenBalance,
update: [
set: [
block_number:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.block_number ELSE ? END",
current_token_balance.block_number,
current_token_balance.block_number
),
inserted_at:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.inserted_at ELSE ? END",
current_token_balance.block_number,
current_token_balance.inserted_at
),
updated_at:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.updated_at ELSE ? END",
current_token_balance.block_number,
current_token_balance.updated_at
),
value:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.value ELSE ? END",
current_token_balance.block_number,
current_token_balance.value
),
value_fetched_at:
fragment(
"CASE WHEN EXCLUDED.block_number > ? THEN EXCLUDED.value_fetched_at ELSE ? END",
current_token_balance.block_number,
current_token_balance.value_fetched_at
)
]
]
)
end
end

@ -0,0 +1,32 @@
defmodule Explorer.Repo.Migrations.CreateAddressCurrentTokenBalances do
use Ecto.Migration
def change do
create table(:address_current_token_balances) do
add(:address_hash, references(:addresses, column: :hash, type: :bytea), null: false)
add(:block_number, :bigint, null: false)
add(
:token_contract_address_hash,
references(:tokens, column: :contract_address_hash, type: :bytea),
null: false
)
add(:value, :decimal, null: true)
add(:value_fetched_at, :utc_datetime, default: fragment("NULL"), null: true)
timestamps(null: false, type: :utc_datetime)
end
create(unique_index(:address_current_token_balances, ~w(address_hash token_contract_address_hash)a))
create(
index(
:address_current_token_balances,
[:value],
name: :address_current_token_balances_value,
where: "value IS NOT NULL"
)
)
end
end

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

@ -0,0 +1,95 @@
defmodule Explorer.Chain.Import.Address.CurrentTokenBalancesTest do
use Explorer.DataCase
alias Explorer.Chain.Import.Address.CurrentTokenBalances
alias Explorer.Chain.{Address.CurrentTokenBalance}
describe "insert/2" do
setup do
address = insert(:address, hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca")
token = insert(:token)
insert_options = %{
timeout: :infinity,
timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}
}
%{address: address, token: token, insert_options: insert_options}
end
test "inserts in the current token balances", %{address: address, token: token, insert_options: insert_options} do
changes = [
%{
address_hash: address.hash,
block_number: 1,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(100)
}
]
CurrentTokenBalances.insert(changes, insert_options)
current_token_balances =
CurrentTokenBalance
|> Explorer.Repo.all()
|> Enum.count()
assert current_token_balances == 1
end
test "considers the last block upserting", %{address: address, token: token, insert_options: insert_options} do
insert(
:address_current_token_balance,
address: address,
block_number: 1,
token_contract_address_hash: token.contract_address_hash,
value: 100
)
changes = [
%{
address_hash: address.hash,
block_number: 2,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(200)
}
]
CurrentTokenBalances.insert(changes, insert_options)
current_token_balance = Explorer.Repo.get_by(CurrentTokenBalance, address_hash: address.hash)
assert current_token_balance.block_number == 2
assert current_token_balance.value == Decimal.new(200)
end
test "considers the last block when there are duplicated params", %{
address: address,
token: token,
insert_options: insert_options
} do
changes = [
%{
address_hash: address.hash,
block_number: 4,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(200)
},
%{
address_hash: address.hash,
block_number: 1,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(100)
}
]
CurrentTokenBalances.insert(changes, insert_options)
current_token_balance = Explorer.Repo.get_by(CurrentTokenBalance, address_hash: address.hash)
assert current_token_balance.block_number == 4
assert current_token_balance.value == Decimal.new(200)
end
end
end

@ -6,6 +6,7 @@ defmodule Explorer.Chain.ImportTest do
alias Explorer.Chain.{
Address,
Address.TokenBalance,
Address.CurrentTokenBalance,
Block,
Data,
Log,
@ -395,6 +396,55 @@ defmodule Explorer.Chain.ImportTest do
assert 3 == count
end
test "inserts a current_token_balance" do
params = %{
addresses: %{
params: [
%{hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca"},
%{hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d"},
%{hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b"}
],
timeout: 5
},
tokens: %{
on_conflict: :nothing,
params: [
%{
contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
type: "ERC-20"
}
],
timeout: 5
},
address_current_token_balances: %{
params: [
%{
address_hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37",
value: 200
},
%{
address_hash: "0x515c09c5bba1ed566b02a5b0599ec5d5d0aee73d",
token_contract_address_hash: "0x8bf38d4764929064f2d4d3a56520a76ab3df415b",
block_number: "37",
value: 100
}
],
timeout: 5
}
}
Import.all(params)
count =
CurrentTokenBalance
|> Explorer.Repo.all()
|> Enum.count()
assert count == 2
end
test "with empty map" do
assert {:ok, %{}} == Import.all(%{})
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

@ -13,6 +13,7 @@ defmodule Explorer.Factory do
alias Explorer.Chain.{
Address,
Address.CurrentTokenBalance,
Address.TokenBalance,
Address.CoinBalance,
Block,
@ -480,6 +481,16 @@ defmodule Explorer.Factory do
}
end
def address_current_token_balance_factory() do
%CurrentTokenBalance{
address: build(:address),
token_contract_address_hash: insert(:token).contract_address_hash,
block_number: block_number(),
value: Enum.random(1..100_000),
value_fetched_at: DateTime.utc_now()
}
end
defmacrop left + right do
quote do
fragment("? + ?", unquote(left), unquote(right))

@ -107,6 +107,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
|> put_in([:addresses, :params], balances_addresses_params)
|> put_in([:blocks, :params, Access.all(), :consensus], true)
|> put_in([Access.key(:address_coin_balances, %{}), :params], balances_params)
|> put_in([Access.key(:address_current_token_balances, %{}), :params], address_token_balances)
|> put_in([Access.key(:address_token_balances), :params], address_token_balances)
|> put_in([Access.key(:internal_transactions, %{}), :params], internal_transactions_params),
{:ok, imported} = ok <- Chain.import(chain_import_options) do

@ -80,7 +80,13 @@ defmodule Indexer.TokenBalance.Fetcher do
end
def import_token_balances(token_balances_params) do
case Chain.import(%{address_token_balances: %{params: token_balances_params}, timeout: :infinity}) do
import_params = %{
address_token_balances: %{params: token_balances_params},
address_current_token_balances: %{params: token_balances_params},
timeout: :infinity
}
case Chain.import(import_params) do
{:ok, _} ->
:ok

@ -29,7 +29,7 @@ defmodule Indexer.TokenBalances do
token_balances
|> Task.async_stream(&fetch_token_balance/1, on_timeout: :kill_task)
|> Stream.map(&format_task_results/1)
|> Enum.filter(&ignore_request_with_timeouts/1)
|> Enum.filter(&ignore_request_with_errors/1)
token_balances
|> MapSet.new()
@ -70,11 +70,12 @@ defmodule Indexer.TokenBalances do
|> TokenBalance.Fetcher.async_fetch()
end
def format_task_results({:exit, :timeout}), do: {:error, :timeout}
def format_task_results({:ok, token_balance}), do: token_balance
defp format_task_results({:exit, :timeout}), do: {:error, :timeout}
defp format_task_results({:ok, token_balance}), do: token_balance
def ignore_request_with_timeouts({:error, :timeout}), do: false
def ignore_request_with_timeouts(_token_balance), do: true
defp ignore_request_with_errors({:error, :timeout}), do: false
defp ignore_request_with_errors(%{value: nil, value_fetched_at: nil, error: _error}), do: false
defp ignore_request_with_errors(_token_balance), do: true
def log_fetching_errors(from, token_balances_params) do
error_messages =

@ -45,28 +45,21 @@ defmodule Indexer.TokenBalancesTest do
} = List.first(result)
end
test "does not ignore calls that were returned with error" do
address = insert(:address)
test "ignores calls that gave errors to try fetch they again later" do
address = insert(:address, hash: "0x7113ffcb9c18a97da1b9cfc43e6cb44ed9165509")
token = insert(:token, contract_address: build(:contract_address))
address_hash_string = Hash.to_string(address.hash)
data = %{
token_contract_address_hash: token.contract_address_hash,
address_hash: address_hash_string,
block_number: 1_000
token_balances = [
%{
address_hash: to_string(address.hash),
block_number: 1_000,
token_contract_address_hash: to_string(token.contract_address_hash)
}
]
get_balance_from_blockchain_with_error()
{:ok, result} = TokenBalances.fetch_token_balances_from_blockchain([data])
assert %{
value: nil,
token_contract_address_hash: token_contract_address_hash,
address_hash: address_hash,
block_number: 1_000,
value_fetched_at: nil
} = List.first(result)
assert TokenBalances.fetch_token_balances_from_blockchain(token_balances) == {:ok, []}
end
test "ignores results that raised :timeout" do

Loading…
Cancel
Save