Import.Runner.CurrentTokenBalances update_token_holder_counts

When `CurrentTokenBalance`s are directly changed

1. Determine which rows will be deleted in
   `deleted_address_current_token_balances` using same `where` as
   `on_conflict` for `address_current_token_balances`
2. Upsert rows using pre-existing `address_current_token_balances`.
3. Calculate delta of holder count using
  `deleted_address_current_token_balances` and
  `address_current_token_balances`.  If the delta is non-zero, update
   the `tokens.holder_count`.
pull/1357/head
Luke Imhoff 6 years ago
parent 4144373367
commit 4f1254b1be
  1. 204
      apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
  2. 171
      apps/explorer/lib/explorer/chain/import/runner/blocks.ex
  3. 78
      apps/explorer/lib/explorer/chain/import/runner/tokens.ex
  4. 274
      apps/explorer/test/explorer/chain/import/runner/address/current_token_balances_test.exs
  5. 77
      apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
  6. 78
      apps/explorer/test/support/chain/import/runner_case.ex

@ -9,7 +9,8 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
alias Ecto.{Changeset, Multi, Repo} alias Ecto.{Changeset, Multi, Repo}
alias Explorer.Chain.Address.CurrentTokenBalance alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Chain.Import alias Explorer.Chain.{Hash, Import}
alias Explorer.Chain.Import.Runner.Tokens
@behaviour Import.Runner @behaviour Import.Runner
@ -18,6 +19,70 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
@type imported :: [CurrentTokenBalance.t()] @type imported :: [CurrentTokenBalance.t()]
@spec to_holder_address_hash_set_by_token_contract_address_hash([CurrentTokenBalance.t()]) :: %{
token_contract_address_hash => MapSet.t(holder_address_hash)
}
when token_contract_address_hash: Hash.Address.t(), holder_address_hash: Hash.Address.t()
def to_holder_address_hash_set_by_token_contract_address_hash(address_current_token_balances)
when is_list(address_current_token_balances) do
address_current_token_balances
|> Stream.filter(fn %{value: value} -> not is_nil(value) && Decimal.cmp(value, 0) == :gt end)
|> Enum.reduce(%{}, fn %{token_contract_address_hash: token_contract_address_hash, address_hash: address_hash},
acc_holder_address_hash_set_by_token_contract_address_hash ->
updated_holder_address_hash_set =
acc_holder_address_hash_set_by_token_contract_address_hash
|> Map.get_lazy(token_contract_address_hash, &MapSet.new/0)
|> MapSet.put(address_hash)
Map.put(
acc_holder_address_hash_set_by_token_contract_address_hash,
token_contract_address_hash,
updated_holder_address_hash_set
)
end)
end
@spec token_holder_count_deltas(%{deleted: [current_token_balance], inserted: [current_token_balance]}) :: [
Tokens.token_holder_count_delta()
]
when current_token_balance: %{
address_hash: Hash.Address.t(),
token_contract_address_hash: Hash.Address.t(),
value: Decimal.t()
}
def token_holder_count_deltas(%{deleted: deleted, inserted: inserted}) when is_list(deleted) and is_list(inserted) do
deleted_holder_address_hash_set_by_token_contract_address_hash =
to_holder_address_hash_set_by_token_contract_address_hash(deleted)
inserted_holder_address_hash_set_by_token_contract_address_hash =
to_holder_address_hash_set_by_token_contract_address_hash(inserted)
ordered_token_contract_address_hashes =
ordered_token_contract_address_hashes([
deleted_holder_address_hash_set_by_token_contract_address_hash,
inserted_holder_address_hash_set_by_token_contract_address_hash
])
Enum.flat_map(ordered_token_contract_address_hashes, fn token_contract_address_hash ->
holder_count_delta =
holder_count_delta(%{
deleted_holder_address_hash_set_by_token_contract_address_hash:
deleted_holder_address_hash_set_by_token_contract_address_hash,
inserted_holder_address_hash_set_by_token_contract_address_hash:
inserted_holder_address_hash_set_by_token_contract_address_hash,
token_contract_address_hash: token_contract_address_hash
})
case holder_count_delta do
0 ->
[]
_ ->
[%{contract_address_hash: token_contract_address_hash, delta: holder_count_delta}]
end
end)
end
@impl Import.Runner @impl Import.Runner
def ecto_schema_module, do: CurrentTokenBalance def ecto_schema_module, do: CurrentTokenBalance
@ -37,18 +102,109 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
insert_options = insert_options =
options options
|> Map.get(option_key(), %{}) |> Map.get(option_key(), %{})
|> Map.take(~w(on_conflict timeout)a) |> Map.take(~w(timeout)a)
|> Map.put_new(:timeout, @timeout) |> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps) |> Map.put(:timestamps, timestamps)
Multi.run(multi, :address_current_token_balances, fn repo, _ -> timeout = insert_options.timeout
insert(repo, changes_list, insert_options)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.address_hash, &1.token_contract_address_hash})
multi
|> Multi.run(:deleted_address_current_token_balances, fn repo, _ ->
deleted_address_current_token_balances(repo, ordered_changes_list, %{timeout: timeout})
end)
|> Multi.run(:address_current_token_balances, fn repo, _ ->
insert(repo, ordered_changes_list, insert_options)
end)
|> Multi.run(:address_current_token_balances_update_token_holder_counts, fn repo,
%{
deleted_address_current_token_balances:
deleted,
address_current_token_balances:
inserted
} ->
token_holder_count_deltas = token_holder_count_deltas(%{deleted: deleted, inserted: inserted})
Tokens.update_holder_counts_with_deltas(
repo,
token_holder_count_deltas,
insert_options
)
end) end)
end end
@impl Import.Runner @impl Import.Runner
def timeout, do: @timeout def timeout, do: @timeout
@spec deleted_address_current_token_balances(Repo.t(), [map()], %{timeout: timeout()}) ::
{:ok, [CurrentTokenBalance.t()]}
defp deleted_address_current_token_balances(_, [], _), do: {:ok, []}
defp deleted_address_current_token_balances(repo, changes_list, %{timeout: timeout})
when is_atom(repo) and is_list(changes_list) do
initial_query =
from(current_token_balance in CurrentTokenBalance,
select:
map(current_token_balance, [
:address_hash,
:token_contract_address_hash,
# to determine if a holder for `update_token_holder_counts`
:value
]),
# to maintain order of lock for `address_current_token_balances`
lock: "FOR UPDATE"
)
final_query =
Enum.reduce(changes_list, initial_query, fn %{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number
},
acc_query ->
from(current_token_balance in acc_query,
or_where:
current_token_balance.address_hash == ^address_hash and
current_token_balance.token_contract_address_hash == ^token_contract_address_hash and
current_token_balance.block_number < ^block_number
)
end)
{:ok, repo.all(final_query, timeout: timeout)}
end
defp holder_count_delta(%{
deleted_holder_address_hash_set_by_token_contract_address_hash:
deleted_holder_address_hash_set_by_token_contract_address_hash,
inserted_holder_address_hash_set_by_token_contract_address_hash:
inserted_holder_address_hash_set_by_token_contract_address_hash,
token_contract_address_hash: token_contract_address_hash
}) do
case {deleted_holder_address_hash_set_by_token_contract_address_hash[token_contract_address_hash],
inserted_holder_address_hash_set_by_token_contract_address_hash[token_contract_address_hash]} do
{deleted_holder_address_hash_set, nil} ->
-1 * Enum.count(deleted_holder_address_hash_set)
{nil, inserted_holder_address_hash_set} ->
Enum.count(inserted_holder_address_hash_set)
{deleted_holder_address_hash_set, inserted_holder_address_hash_set} ->
inserted_holder_address_hash_count =
inserted_holder_address_hash_set
|> MapSet.difference(deleted_holder_address_hash_set)
|> Enum.count()
deleted_holder_address_hash_count =
deleted_holder_address_hash_set
|> MapSet.difference(inserted_holder_address_hash_set)
|> Enum.count()
inserted_holder_address_hash_count - deleted_holder_address_hash_count
end
end
@spec insert(Repo.t(), [map()], %{ @spec insert(Repo.t(), [map()], %{
optional(:on_conflict) => Import.Runner.on_conflict(), optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout(), required(:timeout) => timeout(),
@ -56,24 +212,20 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
}) :: }) ::
{:ok, [CurrentTokenBalance.t()]} {:ok, [CurrentTokenBalance.t()]}
| {:error, [Changeset.t()]} | {:error, [Changeset.t()]}
def insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options) defp insert(repo, ordered_changes_list, %{timeout: timeout, timestamps: timestamps} = options)
when is_atom(repo) and is_list(changes_list) do when is_atom(repo) and is_list(ordered_changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0) on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order Import.insert_changes_list(
ordered_changes_list = Enum.sort_by(changes_list, &{&1.address_hash, &1.token_contract_address_hash}) repo,
ordered_changes_list,
{:ok, _} = conflict_target: ~w(address_hash token_contract_address_hash)a,
Import.insert_changes_list( on_conflict: on_conflict,
repo, for: CurrentTokenBalance,
ordered_changes_list, returning: true,
conflict_target: ~w(address_hash token_contract_address_hash)a, timeout: timeout,
on_conflict: on_conflict, timestamps: timestamps
for: CurrentTokenBalance, )
returning: true,
timeout: timeout,
timestamps: timestamps
)
end end
defp default_on_conflict do defp default_on_conflict do
@ -91,4 +243,16 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
where: fragment("? < EXCLUDED.block_number", current_token_balance.block_number) where: fragment("? < EXCLUDED.block_number", current_token_balance.block_number)
) )
end end
defp ordered_token_contract_address_hashes(holder_address_hash_set_by_token_contract_address_hash_list)
when is_list(holder_address_hash_set_by_token_contract_address_hash_list) do
holder_address_hash_set_by_token_contract_address_hash_list
|> Enum.reduce(MapSet.new(), fn holder_address_hash_set_by_token_contract_address_hash, acc ->
holder_address_hash_set_by_token_contract_address_hash
|> Map.keys()
|> MapSet.new()
|> MapSet.union(acc)
end)
|> Enum.sort()
end
end end

@ -12,6 +12,8 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
alias Explorer.Chain.{Address, Block, Hash, Import, InternalTransaction, Transaction} alias Explorer.Chain.{Address, Block, Hash, Import, InternalTransaction, Transaction}
alias Explorer.Chain.Block.Reward alias Explorer.Chain.Block.Reward
alias Explorer.Chain.Import.Runner alias Explorer.Chain.Import.Runner
alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances
alias Explorer.Chain.Import.Runner.Tokens
@behaviour Runner @behaviour Runner
@ -80,8 +82,13 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
} -> } ->
derive_address_current_token_balances(repo, deleted_address_current_token_balances, insert_options) derive_address_current_token_balances(repo, deleted_address_current_token_balances, insert_options)
end) end)
|> Multi.run(:update_token_holder_counts, fn repo, changes_so_far -> |> Multi.run(:blocks_update_token_holder_counts, fn repo,
update_token_holder_counts(repo, changes_so_far, insert_options) %{
delete_address_current_token_balances: deleted,
derive_address_current_token_balances: inserted
} ->
deltas = CurrentTokenBalances.token_holder_count_deltas(%{deleted: deleted, inserted: inserted})
Tokens.update_holder_counts_with_deltas(repo, deltas, insert_options)
end) end)
|> Multi.run(:delete_rewards, fn repo, _ -> |> Multi.run(:delete_rewards, fn repo, _ ->
delete_rewards(repo, changes_list, insert_options) delete_rewards(repo, changes_list, insert_options)
@ -474,166 +481,6 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
end end
end end
# sobelow_skip ["SQL.Query"]
defp update_token_holder_counts(repo, changes_so_far, options) when is_map(changes_so_far) do
parameters = update_token_holder_counts_parameters(changes_so_far)
update_token_holder_counts(repo, parameters, options)
end
defp update_token_holder_counts(_, [], _), do: {:ok, []}
defp update_token_holder_counts(repo, parameters, %{timeout: timeout}) do
update_sql = update_token_holder_counts_sql(parameters)
with {:ok, %Postgrex.Result{columns: ["contract_address_hash", "holder_count"], command: :update, rows: rows}} <-
SQL.query(repo, update_sql, parameters, timeout: timeout) do
update_token_holder_counts =
Enum.map(rows, fn [contract_address_hash_bytes, holder_count] ->
{:ok, contract_address_hash} = Hash.Address.cast(contract_address_hash_bytes)
%{contract_address_hash: contract_address_hash, holder_count: holder_count}
end)
{:ok, update_token_holder_counts}
end
end
defp update_token_holder_counts_parameters(%{
delete_address_current_token_balances: deleted_address_current_token_balances,
derive_address_current_token_balances: derived_address_current_token_balances
}) do
previous_holder_address_hash_set_by_token_contract_address_hash =
address_current_token_balances_to_holder_address_hash_set_by_token_contract_address_hash(
deleted_address_current_token_balances
)
current_holder_address_hash_set_by_token_contract_address_hash =
address_current_token_balances_to_holder_address_hash_set_by_token_contract_address_hash(
derived_address_current_token_balances
)
ordered_token_contract_address_hashes =
ordered_token_contract_address_hashes([
previous_holder_address_hash_set_by_token_contract_address_hash,
current_holder_address_hash_set_by_token_contract_address_hash
])
Enum.flat_map(ordered_token_contract_address_hashes, fn token_contract_address_hash ->
holder_count_delta =
holder_count_delta(%{
previous_holder_address_hash_set_by_token_contract_address_hash:
previous_holder_address_hash_set_by_token_contract_address_hash,
current_holder_address_hash_set_by_token_contract_address_hash:
current_holder_address_hash_set_by_token_contract_address_hash,
token_contract_address_hash: token_contract_address_hash
})
case holder_count_delta do
0 ->
[]
_ ->
{:ok, token_contract_address_hash_bytes} = Hash.Address.dump(token_contract_address_hash)
[token_contract_address_hash_bytes, holder_count_delta]
end
end)
end
defp ordered_token_contract_address_hashes(holder_address_hash_set_by_token_contract_address_hash_list)
when is_list(holder_address_hash_set_by_token_contract_address_hash_list) do
holder_address_hash_set_by_token_contract_address_hash_list
|> Enum.reduce(MapSet.new(), fn holder_address_hash_set_by_token_contract_address_hash, acc ->
holder_address_hash_set_by_token_contract_address_hash
|> Map.keys()
|> MapSet.new()
|> MapSet.union(acc)
end)
|> Enum.sort()
end
defp holder_count_delta(%{
previous_holder_address_hash_set_by_token_contract_address_hash:
previous_holder_address_hash_set_by_token_contract_address_hash,
current_holder_address_hash_set_by_token_contract_address_hash:
current_holder_address_hash_set_by_token_contract_address_hash,
token_contract_address_hash: token_contract_address_hash
}) do
case {previous_holder_address_hash_set_by_token_contract_address_hash[token_contract_address_hash],
current_holder_address_hash_set_by_token_contract_address_hash[token_contract_address_hash]} do
{previous_holder_address_hash_set, nil} ->
-1 * Enum.count(previous_holder_address_hash_set)
{nil, current_holder_address_hash_set} ->
Enum.count(current_holder_address_hash_set)
{previous_holder_address_hash_set, current_holder_address_hash_set} ->
added_holder_address_hash_count =
current_holder_address_hash_set
|> MapSet.difference(previous_holder_address_hash_set)
|> Enum.count()
removed_holder_address_hash_count =
previous_holder_address_hash_set
|> MapSet.difference(current_holder_address_hash_set)
|> Enum.count()
added_holder_address_hash_count - removed_holder_address_hash_count
end
end
defp update_token_holder_counts_sql(parameters) when is_list(parameters) do
parameters
|> Enum.count()
|> div(2)
|> update_token_holder_counts_sql()
end
defp update_token_holder_counts_sql(row_count) when is_integer(row_count) do
parameters_sql = update_token_holder_counts_parameters_sql(row_count)
"""
UPDATE tokens
SET holder_count = holder_count + holder_counts.delta
FROM (
VALUES
#{parameters_sql}
) AS holder_counts(contract_address_hash, delta)
WHERE tokens.contract_address_hash = holder_counts.contract_address_hash AND
holder_count IS NOT NULL
RETURNING tokens.contract_address_hash, tokens.holder_count
"""
end
defp update_token_holder_counts_parameters_sql(row_count) when is_integer(row_count) do
Enum.map_join(0..(row_count - 1), ",\n ", fn i ->
contract_address_hash_parameter_number = 2 * i + 1
holder_count_number = contract_address_hash_parameter_number + 1
"($#{contract_address_hash_parameter_number}::bytea, $#{holder_count_number}::bigint)"
end)
end
defp address_current_token_balances_to_holder_address_hash_set_by_token_contract_address_hash(
address_current_token_balances
)
when is_list(address_current_token_balances) do
address_current_token_balances
|> Stream.filter(fn %{value: value} -> Decimal.cmp(value, 0) == :gt end)
|> Enum.reduce(%{}, fn %{token_contract_address_hash: token_contract_address_hash, address_hash: address_hash},
acc_holder_address_hash_set_by_token_contract_address_hash ->
updated_holder_address_hash_set =
acc_holder_address_hash_set_by_token_contract_address_hash
|> Map.get_lazy(token_contract_address_hash, &MapSet.new/0)
|> MapSet.put(address_hash)
Map.put(
acc_holder_address_hash_set_by_token_contract_address_hash,
token_contract_address_hash,
updated_holder_address_hash_set
)
end)
end
# `block_rewards` are linked to `blocks.hash`, but fetched by `blocks.number`, so when a block with the same number is # `block_rewards` are linked to `blocks.hash`, but fetched by `blocks.number`, so when a block with the same number is
# inserted, the old block rewards need to be deleted, so that the old and new rewards aren't combined. # inserted, the old block rewards need to be deleted, so that the old and new rewards aren't combined.
defp delete_rewards(repo, blocks_changes, %{timeout: timeout}) do defp delete_rewards(repo, blocks_changes, %{timeout: timeout}) do

@ -7,8 +7,9 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
import Ecto.Query, only: [from: 2] import Ecto.Query, only: [from: 2]
alias Ecto.Adapters.SQL
alias Ecto.{Multi, Repo} alias Ecto.{Multi, Repo}
alias Explorer.Chain.{Import, Token} alias Explorer.Chain.{Hash, Import, Token}
@behaviour Import.Runner @behaviour Import.Runner
@ -17,6 +18,16 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
@type imported :: [Token.t()] @type imported :: [Token.t()]
@type token_holder_count_delta :: %{contract_address_hash: Hash.Address.t(), delta: neg_integer() | pos_integer()}
@type holder_count :: non_neg_integer()
@type token_holder_count :: %{contract_address_hash: Hash.Address.t(), count: holder_count()}
def update_holder_counts_with_deltas(repo, token_holder_count_deltas, options) do
parameters = token_holder_count_deltas_to_parameters(token_holder_count_deltas)
update_holder_counts_with_parameters(repo, parameters, options)
end
@impl Import.Runner @impl Import.Runner
def ecto_schema_module, do: Token def ecto_schema_module, do: Token
@ -99,4 +110,69 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
) )
) )
end end
defp token_holder_count_deltas_to_parameters(token_holder_count_deltas) when is_list(token_holder_count_deltas) do
Enum.flat_map(token_holder_count_deltas, fn
%{contract_address_hash: contract_address_hash, delta: delta} ->
{:ok, contract_address_hash_bytes} = Hash.Address.dump(contract_address_hash)
[contract_address_hash_bytes, delta]
end)
end
defp update_holder_counts_with_parameters(_, [], _), do: {:ok, []}
# sobelow_skip ["SQL.Query"]
defp update_holder_counts_with_parameters(repo, parameters, %{timeout: timeout, timestamps: %{updated_at: updated_at}})
when is_list(parameters) do
update_sql = update_holder_counts_sql(parameters)
with {:ok, %Postgrex.Result{columns: ["contract_address_hash", "holder_count"], command: :update, rows: rows}} <-
SQL.query(repo, update_sql, [updated_at | parameters], timeout: timeout) do
update_token_holder_counts =
Enum.map(rows, fn [contract_address_hash_bytes, holder_count] ->
{:ok, contract_address_hash} = Hash.Address.cast(contract_address_hash_bytes)
%{contract_address_hash: contract_address_hash, holder_count: holder_count}
end)
{:ok, update_token_holder_counts}
end
end
defp update_holder_counts_sql(parameters) when is_list(parameters) do
parameters
|> Enum.count()
|> div(2)
|> update_holder_counts_sql()
end
defp update_holder_counts_sql(row_count) when is_integer(row_count) do
parameters_sql =
update_holder_counts_parameters_sql(
row_count,
# skip $1 as it is used for the common `updated_at` timestamp
2
)
"""
UPDATE tokens
SET holder_count = holder_count + holder_counts.delta,
updated_at = $1
FROM (
VALUES
#{parameters_sql}
) AS holder_counts(contract_address_hash, delta)
WHERE tokens.contract_address_hash = holder_counts.contract_address_hash AND
holder_count IS NOT NULL
RETURNING tokens.contract_address_hash, tokens.holder_count
"""
end
defp update_holder_counts_parameters_sql(row_count, start) when is_integer(row_count) do
Enum.map_join(0..(row_count - 1), ",\n ", fn i ->
contract_address_hash_parameter_number = 2 * i + start
holder_count_number = contract_address_hash_parameter_number + 1
"($#{contract_address_hash_parameter_number}::bytea, $#{holder_count_number}::bigint)"
end)
end
end end

@ -1,34 +1,61 @@
defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
use Explorer.DataCase use Explorer.DataCase
import Explorer.Chain.Import.RunnerCase, only: [insert_token_balance: 1, update_holder_count!: 2]
alias Ecto.Multi
alias Explorer.Chain.{Address, Token}
alias Explorer.Chain.Address.CurrentTokenBalance alias Explorer.Chain.Address.CurrentTokenBalance
alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances
alias Explorer.Repo alias Explorer.Repo
describe "insert/2" do describe "run/2" do
setup do setup do
address = insert(:address, hash: "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca") address = insert(:address)
token = insert(:token) token = insert(:token, holder_count: 0)
insert_options = %{ options = %{
timeout: :infinity, timeout: :infinity,
timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()} timestamps: %{inserted_at: DateTime.utc_now(), updated_at: DateTime.utc_now()}
} }
%{address: address, token: token, insert_options: insert_options} %{address: address, token: token, options: options}
end end
test "inserts in the current token balances", %{address: address, token: token, insert_options: insert_options} do test "inserts in the current token balances", %{
changes = [ address: %Address{hash: address_hash},
%{ token: %Token{contract_address_hash: token_contract_address_hash},
address_hash: address.hash, options: options
block_number: 1, } do
token_contract_address_hash: token.contract_address_hash, value = Decimal.new(100)
value: Decimal.new(100) block_number = 1
}
]
CurrentTokenBalances.insert(Repo, changes, insert_options) assert {:ok,
%{
address_current_token_balances: [
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
value: ^value
}
],
address_current_token_balances_update_token_holder_counts: [
%{
contract_address_hash: ^token_contract_address_hash,
holder_count: 1
}
]
}} =
run_changes(
%{
address_hash: address_hash,
block_number: block_number,
token_contract_address_hash: token_contract_address_hash,
value: value
},
options
)
current_token_balances = current_token_balances =
CurrentTokenBalance CurrentTokenBalance
@ -41,7 +68,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
test "updates when the new block number is greater", %{ test "updates when the new block number is greater", %{
address: address, address: address,
token: token, token: token,
insert_options: insert_options options: options
} do } do
insert( insert(
:address_current_token_balance, :address_current_token_balance,
@ -51,16 +78,15 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
value: 100 value: 100
) )
changes = [ run_changes(
%{ %{
address_hash: address.hash, address_hash: address.hash,
block_number: 2, block_number: 2,
token_contract_address_hash: token.contract_address_hash, token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(200) value: Decimal.new(200)
} },
] options
)
CurrentTokenBalances.insert(Repo, changes, insert_options)
current_token_balance = Repo.get_by(CurrentTokenBalance, address_hash: address.hash) current_token_balance = Repo.get_by(CurrentTokenBalance, address_hash: address.hash)
@ -69,33 +95,211 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalancesTest do
end end
test "ignores when the new block number is lesser", %{ test "ignores when the new block number is lesser", %{
address: address, address: %Address{hash: address_hash} = address,
token: token, token: %Token{contract_address_hash: token_contract_address_hash},
insert_options: insert_options options: options
} do } do
insert( insert(
:address_current_token_balance, :address_current_token_balance,
address: address, address: address,
block_number: 2, block_number: 2,
token_contract_address_hash: token.contract_address_hash, token_contract_address_hash: token_contract_address_hash,
value: 200 value: 200
) )
changes = [ update_holder_count!(token_contract_address_hash, 1)
%{
address_hash: address.hash,
block_number: 1,
token_contract_address_hash: token.contract_address_hash,
value: Decimal.new(100)
}
]
CurrentTokenBalances.insert(Repo, changes, insert_options) assert {:ok, %{address_current_token_balances: [], address_current_token_balances_update_token_holder_counts: []}} =
run_changes(
%{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: 1,
value: Decimal.new(100)
},
options
)
current_token_balance = Repo.get_by(CurrentTokenBalance, address_hash: address.hash) current_token_balance = Repo.get_by(CurrentTokenBalance, address_hash: address_hash)
assert current_token_balance.block_number == 2 assert current_token_balance.block_number == 2
assert current_token_balance.value == Decimal.new(200) assert current_token_balance.value == Decimal.new(200)
end end
test "a non-holder updating to a holder increases the holder_count", %{
address: %Address{hash: address_hash} = address,
token: %Token{contract_address_hash: token_contract_address_hash},
options: options
} do
previous_block_number = 1
insert_token_balance(%{
address: address,
token_contract_address_hash: token_contract_address_hash,
block_number: previous_block_number,
value: 0
})
block_number = previous_block_number + 1
value = Decimal.new(1)
assert {:ok,
%{
address_current_token_balances: [
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
value: ^value
}
],
address_current_token_balances_update_token_holder_counts: [
%{
contract_address_hash: ^token_contract_address_hash,
holder_count: 1
}
]
}} =
run_changes(
%{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value
},
options
)
end
test "a holder updating to a non-holder decreases the holder_count", %{
address: %Address{hash: address_hash} = address,
token: %Token{contract_address_hash: token_contract_address_hash},
options: options
} do
previous_block_number = 1
insert_token_balance(%{
address: address,
token_contract_address_hash: token_contract_address_hash,
block_number: previous_block_number,
value: 1
})
update_holder_count!(token_contract_address_hash, 1)
block_number = previous_block_number + 1
value = Decimal.new(0)
assert {:ok,
%{
address_current_token_balances: [
%Explorer.Chain.Address.CurrentTokenBalance{
address_hash: ^address_hash,
block_number: ^block_number,
token_contract_address_hash: ^token_contract_address_hash,
value: ^value
}
],
address_current_token_balances_update_token_holder_counts: [
%{contract_address_hash: ^token_contract_address_hash, holder_count: 0}
]
}} =
run_changes(
%{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value
},
options
)
end
test "a non-holder becoming and a holder becoming while a holder becomes a non-holder cancels out and holder_count does not change",
%{
address: %Address{hash: non_holder_becomes_holder_address_hash} = non_holder_becomes_holder_address,
token: %Token{contract_address_hash: token_contract_address_hash},
options: options
} do
previous_block_number = 1
insert_token_balance(%{
address: non_holder_becomes_holder_address,
token_contract_address_hash: token_contract_address_hash,
block_number: previous_block_number,
value: 0
})
%Address{hash: holder_becomes_non_holder_address_hash} = holder_becomes_non_holder_address = insert(:address)
insert_token_balance(%{
address: holder_becomes_non_holder_address,
token_contract_address_hash: token_contract_address_hash,
block_number: previous_block_number,
value: 1
})
update_holder_count!(token_contract_address_hash, 1)
block_number = previous_block_number + 1
non_holder_becomes_holder_value = Decimal.new(1)
holder_becomes_non_holder_value = Decimal.new(0)
assert {:ok,
%{
deleted_address_current_token_balances: [
%{
address_hash: ^non_holder_becomes_holder_address_hash,
token_contract_address_hash: ^token_contract_address_hash
},
%{
address_hash: ^holder_becomes_non_holder_address_hash,
token_contract_address_hash: ^token_contract_address_hash
}
],
address_current_token_balances: [
%{
address_hash: ^non_holder_becomes_holder_address_hash,
token_contract_address_hash: ^token_contract_address_hash,
block_number: ^block_number,
value: ^non_holder_becomes_holder_value
},
%{
address_hash: ^holder_becomes_non_holder_address_hash,
token_contract_address_hash: ^token_contract_address_hash,
block_number: ^block_number,
value: ^holder_becomes_non_holder_value
}
],
address_current_token_balances_update_token_holder_counts: []
}} =
run_changes_list(
[
%{
address_hash: non_holder_becomes_holder_address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: non_holder_becomes_holder_value
},
%{
address_hash: holder_becomes_non_holder_address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: holder_becomes_non_holder_value
}
],
options
)
end
end
defp run_changes(changes, options) when is_map(changes) do
run_changes_list([changes], options)
end
defp run_changes_list(changes_list, options) when is_list(changes_list) do
Multi.new()
|> CurrentTokenBalances.run(changes_list, options)
|> Repo.transaction()
end end
end end

@ -3,9 +3,11 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
import Ecto.Query, only: [from: 2, select: 2, where: 2] import Ecto.Query, only: [from: 2, select: 2, where: 2]
import Explorer.Chain.Import.RunnerCase, only: [insert_address_with_token_balances: 1, update_holder_count!: 2]
alias Ecto.Multi alias Ecto.Multi
alias Explorer.Chain.Import.Runner.{Blocks, Transaction} alias Explorer.Chain.Import.Runner.{Blocks, Transaction}
alias Explorer.Chain.{Address, Block, Token, Transaction} alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.Repo alias Explorer.Repo
describe "run/1" do describe "run/1" do
@ -122,7 +124,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
token_contract_address_hash: token_contract_address_hash token_contract_address_hash: token_contract_address_hash
}) })
# Token must exist with non-`nil` `holder_count` for `update_token_holder_counts` to update # Token must exist with non-`nil` `holder_count` for `blocks_update_token_holder_counts` to update
update_holder_count!(token_contract_address_hash, 1) update_holder_count!(token_contract_address_hash, 1)
assert count(Address.TokenBalance) == 2 assert count(Address.TokenBalance) == 2
@ -153,7 +155,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
} }
], ],
# no updates because it both deletes and derives a holder # no updates because it both deletes and derives a holder
update_token_holder_counts: [] blocks_update_token_holder_counts: []
}} = run_block_consensus_change(block, true, options) }} = run_block_consensus_change(block, true, options)
assert count(Address.TokenBalance) == 1 assert count(Address.TokenBalance) == 1
@ -178,7 +180,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
token_contract_address_hash: token_contract_address_hash token_contract_address_hash: token_contract_address_hash
}) })
# Token must exist with non-`nil` `holder_count` for `update_token_holder_counts` to update # Token must exist with non-`nil` `holder_count` for `blocks_update_token_holder_counts` to update
update_holder_count!(token_contract_address_hash, 0) update_holder_count!(token_contract_address_hash, 0)
block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: true) block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: true)
@ -188,7 +190,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
assert {:ok, assert {:ok,
%{ %{
update_token_holder_counts: [ blocks_update_token_holder_counts: [
%{ %{
contract_address_hash: ^token_contract_address_hash, contract_address_hash: ^token_contract_address_hash,
holder_count: 1 holder_count: 1
@ -210,7 +212,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
token_contract_address_hash: token_contract_address_hash token_contract_address_hash: token_contract_address_hash
}) })
# Token must exist with non-`nil` `holder_count` for `update_token_holder_counts` to update # Token must exist with non-`nil` `holder_count` for `blocks_update_token_holder_counts` to update
update_holder_count!(token_contract_address_hash, 1) update_holder_count!(token_contract_address_hash, 1)
block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: true) block_params = params_for(:block, hash: block_hash, miner_hash: miner_hash, number: block_number, consensus: true)
@ -220,7 +222,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
assert {:ok, assert {:ok,
%{ %{
update_token_holder_counts: [ blocks_update_token_holder_counts: [
%{ %{
contract_address_hash: ^token_contract_address_hash, contract_address_hash: ^token_contract_address_hash,
holder_count: 0 holder_count: 0
@ -247,13 +249,13 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
token_contract_address_hash: token_contract_address_hash token_contract_address_hash: token_contract_address_hash
}) })
# Token must exist with non-`nil` `holder_count` for `update_token_holder_counts` to update # Token must exist with non-`nil` `holder_count` for `blocks_update_token_holder_counts` to update
update_holder_count!(token_contract_address_hash, 1) update_holder_count!(token_contract_address_hash, 1)
assert {:ok, assert {:ok,
%{ %{
# cancels out to no change # cancels out to no change
update_token_holder_counts: [] blocks_update_token_holder_counts: []
}} = run_block_consensus_change(block, true, options) }} = run_block_consensus_change(block, true, options)
end end
end end
@ -284,52 +286,6 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
}) })
end end
defp insert_address_with_token_balances(%{
previous: %{value: previous_value},
current: %{block_number: current_block_number, value: current_value},
token_contract_address_hash: token_contract_address_hash
}) do
%Address.TokenBalance{
address_hash: address_hash,
token_contract_address_hash: ^token_contract_address_hash
} =
insert(:token_balance,
token_contract_address_hash: token_contract_address_hash,
block_number: current_block_number - 1,
value: previous_value
)
address = Repo.get(Address, address_hash)
%Address.TokenBalance{
address_hash: ^address_hash,
token_contract_address_hash: ^token_contract_address_hash,
block_number: ^current_block_number,
value: holder_current_value
} =
insert(:token_balance,
address: address,
token_contract_address_hash: token_contract_address_hash,
block_number: current_block_number,
value: current_value
)
%Address.CurrentTokenBalance{
address_hash: ^address_hash,
token_contract_address_hash: ^token_contract_address_hash,
block_number: ^current_block_number,
value: ^holder_current_value
} =
insert(:address_current_token_balance,
address: address,
token_contract_address_hash: token_contract_address_hash,
block_number: current_block_number,
value: holder_current_value
)
address
end
defp run_block_consensus_change( defp run_block_consensus_change(
%Block{hash: block_hash, miner_hash: miner_hash, number: block_number}, %Block{hash: block_hash, miner_hash: miner_hash, number: block_number},
consensus, consensus,
@ -345,15 +301,4 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
|> Blocks.run(changes_list, options) |> Blocks.run(changes_list, options)
|> Repo.transaction() |> Repo.transaction()
end end
defp update_holder_count!(contract_address_hash, holder_count) do
{1, [%{holder_count: ^holder_count}]} =
Repo.update_all(
from(token in Token,
where: token.contract_address_hash == ^contract_address_hash,
select: map(token, [:holder_count])
),
set: [holder_count: holder_count]
)
end
end end

@ -0,0 +1,78 @@
defmodule Explorer.Chain.Import.RunnerCase do
import Explorer.Factory, only: [insert: 2]
import Ecto.Query, only: [from: 2]
alias Explorer.Chain.{Address, Token}
alias Explorer.Repo
def insert_address_with_token_balances(%{
previous: %{value: previous_value},
current: %{block_number: current_block_number, value: current_value},
token_contract_address_hash: token_contract_address_hash
}) do
%Address.TokenBalance{
address_hash: address_hash,
token_contract_address_hash: ^token_contract_address_hash
} =
insert(:token_balance,
token_contract_address_hash: token_contract_address_hash,
block_number: current_block_number - 1,
value: previous_value
)
address = Repo.get(Address, address_hash)
insert_token_balance(%{
address: address,
token_contract_address_hash: token_contract_address_hash,
block_number: current_block_number,
value: current_value
})
address
end
def insert_token_balance(%{
address: %Address{hash: address_hash} = address,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value
}) do
%Address.TokenBalance{
address_hash: ^address_hash,
token_contract_address_hash: ^token_contract_address_hash,
block_number: ^block_number,
value: cast_value
} =
insert(:token_balance,
address: address,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value
)
%Address.CurrentTokenBalance{
address_hash: ^address_hash,
token_contract_address_hash: ^token_contract_address_hash,
block_number: ^block_number,
value: ^cast_value
} =
insert(:address_current_token_balance,
address: address,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value
)
end
def update_holder_count!(contract_address_hash, holder_count) do
{1, [%{holder_count: ^holder_count}]} =
Repo.update_all(
from(token in Token,
where: token.contract_address_hash == ^contract_address_hash,
select: map(token, [:holder_count])
),
set: [holder_count: holder_count]
)
end
end
Loading…
Cancel
Save