Merge pull request #3125 from poanetwork/vb-fix-performance-coin-balance-history-nnew-wave

Fix performance of coin balance history chart
pull/3142/head
Victor Baranov 5 years ago committed by GitHub
commit 70197692a4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      CHANGELOG.md
  2. 4
      apps/block_scout_web/config/config.exs
  3. 2
      apps/block_scout_web/test/block_scout_web/controllers/address_coin_balance_by_day_controller_test.exs
  4. 71
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  5. 5
      apps/explorer/lib/explorer/chain.ex
  6. 74
      apps/explorer/lib/explorer/chain/address/coin_balance_daily.ex
  7. 139
      apps/explorer/lib/explorer/chain/import/runner/address/coin_balances_daily.ex
  8. 3
      apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex
  9. 22
      apps/explorer/lib/explorer/chain_spec/parity/importer.ex
  10. 17
      apps/explorer/priv/repo/migrations/20200525115811_address_coin_balances_daily.exs
  11. 78
      apps/explorer/test/explorer/chain_spec/parity/importer_test.exs
  12. 4
      apps/explorer/test/explorer/chain_test.exs
  13. 13
      apps/explorer/test/support/factory.ex
  14. 11
      apps/indexer/lib/indexer/block/fetcher.ex
  15. 45
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  16. 6
      apps/indexer/lib/indexer/fetcher/block_reward.ex
  17. 79
      apps/indexer/lib/indexer/fetcher/coin_balance.ex
  18. 55
      apps/indexer/lib/indexer/fetcher/coin_balance_on_demand.ex
  19. 155
      apps/indexer/lib/indexer/transform/address_coin_balances_daily.ex
  20. 135
      apps/indexer/test/indexer/block/fetcher_test.exs
  21. 120
      apps/indexer/test/indexer/block/realtime/fetcher_test.exs
  22. 126
      apps/indexer/test/indexer/fetcher/block_reward_test.exs
  23. 65
      apps/indexer/test/indexer/fetcher/coin_balance_on_demand_test.exs
  24. 137
      apps/indexer/test/indexer/fetcher/coin_balance_test.exs

@ -8,6 +8,7 @@
- [#3130](https://github.com/poanetwork/blockscout/pull/3130) - Take into account FIRST_BLOCK for block rewards fetching
- [#3128](https://github.com/poanetwork/blockscout/pull/3128) - Token instance metadata retriever refinement: add processing of token metadata if only image URL is passed to token URI
- [#3126](https://github.com/poanetwork/blockscout/pull/3126) - Fetch balance only for blocks which are greater or equal block with FIRST_BLOCK number
- [#3125](https://github.com/poanetwork/blockscout/pull/3125) - Fix performance of coin balance history chart
- [#3122](https://github.com/poanetwork/blockscout/pull/3122) - Exclude balance percentage calculation for burn address on accounts page
- [#3121](https://github.com/poanetwork/blockscout/pull/3121) - Geth: handle response from eth_getblockbyhash JSON RPC method without totalDifficulty (uncle blocks)
- [#3119](https://github.com/poanetwork/blockscout/pull/3119), [#3120](https://github.com/poanetwork/blockscout/pull/3120) - Fix performance of Inventory tab loading for ERC-721 tokens

@ -84,6 +84,10 @@ config :block_scout_web, BlockScoutWeb.Chain.TransactionHistoryChartController,
# days
history_size: 30
config :block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance,
# days
coin_balance_history_days: System.get_env("COIN_BALANCE_HISTORY_DAYS", "10")
config :ex_cldr,
default_locale: "en",
default_backend: BlockScoutWeb.Cldr

@ -11,6 +11,8 @@ defmodule BlockScoutWeb.AddressCoinBalanceByDayControllerTest do
block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1), number: 1)
insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block.number)
insert(:fetched_balance, address_hash: address.hash, value: 2000, block_number: block_one_day_ago.number)
insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: noon)
insert(:fetched_balance_daily, address_hash: address.hash, value: 2000, day: Timex.shift(noon, days: -1))
conn = get(conn, address_coin_balance_by_day_path(conn, :index, Address.checksum(address)), %{"type" => "JSON"})

@ -111,6 +111,30 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
address_hash = to_string(address.hash)
expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [
%{
id: id,
method: "eth_getBalance",
params: [^mining_address_hash, "0x65"]
}
],
_options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]}
end)
res = eth_block_number_fake_response("0x65")
expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [
%{
id: 0,
method: "eth_getBlockByNumber",
params: ["0x65", true]
}
],
_ ->
{:ok, [res]}
end)
expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [
%{
id: id,
@ -122,6 +146,17 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]}
end)
expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [
%{
id: 0,
method: "eth_getBlockByNumber",
params: ["0x65", true]
}
],
_ ->
{:ok, [res]}
end)
response =
conn
|> get("/api", params)
@ -2719,4 +2754,40 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
|> put_in(["properties", "result"], result)
|> ExJsonSchema.Schema.resolve()
end
defp eth_block_number_fake_response(block_quantity) do
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x0000000000000000000000000000000000000000",
"difficulty" => "0x20000",
"extraData" => "0x",
"gasLimit" => "0x663be0",
"gasUsed" => "0x0",
"hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x0000000000000000000000000000000000000000",
"number" => block_quantity,
"parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x80",
"0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"size" => "0x215",
"stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
"step" => "0",
"timestamp" => "0x0",
"totalDifficulty" => "0x20000",
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
}
end
end

@ -35,6 +35,7 @@ defmodule Explorer.Chain do
alias Explorer.Chain.{
Address,
Address.CoinBalance,
Address.CoinBalanceDaily,
Address.CurrentTokenBalance,
Address.TokenBalance,
Block,
@ -3519,8 +3520,9 @@ defmodule Explorer.Chain do
|> Repo.one()
address_hash
|> CoinBalance.balances_by_day(latest_block_timestamp)
|> CoinBalanceDaily.balances_by_day()
|> Repo.all()
|> Enum.sort_by(fn %{date: d} -> {d.year, d.month, d.day} end)
|> replace_last_value(latest_block_timestamp)
|> normalize_balances_by_day()
end
@ -3535,7 +3537,6 @@ defmodule Explorer.Chain do
defp normalize_balances_by_day(balances_by_day) do
result =
balances_by_day
|> Enum.map(fn day -> Map.take(day, [:date, :value]) end)
|> Enum.filter(fn day -> day.value end)
|> Enum.map(fn day -> Map.update!(day, :date, &to_string(&1)) end)
|> Enum.map(fn day -> Map.update!(day, :value, &Wei.to(&1, :ether)) end)

@ -0,0 +1,74 @@
defmodule Explorer.Chain.Address.CoinBalanceDaily do
@moduledoc """
Maximum `t:Explorer.Chain.Wei.t/0` `value` of `t:Explorer.Chain.Address.t/0` at the day.
This table is used to display coinn balance history chart.
"""
use Explorer.Schema
alias Explorer.Chain.{Address, Hash, Wei}
alias Explorer.Chain.Address.CoinBalanceDaily
@optional_fields ~w(value)a
@required_fields ~w(address_hash day)a
@allowed_fields @optional_fields ++ @required_fields
@typedoc """
* `address` - the `t:Explorer.Chain.Address.t/0`.
* `address_hash` - foreign key for `address`.
* `day` - the `t:Date.t/0`.
* `inserted_at` - When the balance was first inserted into the database.
* `updated_at` - When the balance was last updated.
* `value` - the max balance (`value`) of `address` during the `day`.
"""
@type t :: %__MODULE__{
address: %Ecto.Association.NotLoaded{} | Address.t(),
address_hash: Hash.Address.t(),
day: Date.t(),
inserted_at: DateTime.t(),
updated_at: DateTime.t(),
value: Wei.t() | nil
}
@primary_key false
schema "address_coin_balances_daily" do
field(:day, :date)
field(:value, Wei)
timestamps()
belongs_to(:address, Address, foreign_key: :address_hash, references: :hash, type: Hash.Address)
end
@doc """
Builds an `Ecto.Query` to fetch a series of balances by day for the given account. Each element in the series
corresponds to the maximum balance in that day. Only the last `n` days of data are used.
`n` is configurable via COIN_BALANCE_HISTORY_DAYS ENV var.
"""
def balances_by_day(address_hash) do
{days_to_consider, _} =
Application.get_env(:block_scout_web, BlockScoutWeb.Chain.Address.CoinBalance)[:coin_balance_history_days]
|> Integer.parse()
CoinBalanceDaily
|> where([cbd], cbd.address_hash == ^address_hash)
|> limit_time_interval(days_to_consider)
|> select([cbd], %{date: cbd.day, value: cbd.value})
end
def limit_time_interval(query, days_to_consider) do
query
|> where(
[cbd],
cbd.day >= fragment("date_trunc('day', now() - CAST(? AS INTERVAL))", ^%Postgrex.Interval{days: days_to_consider})
)
end
def changeset(%__MODULE__{} = balance, params) do
balance
|> cast(params, @allowed_fields)
|> validate_required(@required_fields)
|> foreign_key_constraint(:address_hash)
|> unique_constraint(:day, name: :address_coin_balances_daily_address_hash_day_index)
end
end

@ -0,0 +1,139 @@
defmodule Explorer.Chain.Import.Runner.Address.CoinBalancesDaily do
@moduledoc """
Bulk imports `t:Explorer.Chain.Address.CoinBalancesDaily.t/0`.
"""
require Ecto.Query
import Ecto.Query, only: [from: 2]
alias Ecto.{Changeset, Multi, Repo}
alias Explorer.Chain.Address.CoinBalanceDaily
alias Explorer.Chain.{Hash, Import, Wei}
@behaviour Import.Runner
# milliseconds
@timeout 60_000
@type imported :: [
%{required(:address_hash) => Hash.Address.t(), required(:day) => Date.t()}
]
@impl Import.Runner
def ecto_schema_module, do: CoinBalanceDaily
@impl Import.Runner
def option_key, do: :address_coin_balances_daily
@impl Import.Runner
def imported_table_row do
%{
value_type: "[%{address_hash: Explorer.Chain.Hash.t(), day: Date.t()}]",
value_description: "List of maps of the `t:#{ecto_schema_module()}.t/0` `address_hash` and `day`"
}
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_coin_balances_daily, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
end
@impl Import.Runner
def timeout, do: @timeout
@spec insert(
Repo.t(),
[
%{
required(:address_hash) => Hash.Address.t(),
required(:day) => Date.t(),
required(:value) => Wei.t()
}
],
%{
optional(:on_conflict) => Import.Runner.on_conflict(),
required(:timeout) => timeout,
required(:timestamps) => Import.timestamps()
}
) ::
{:ok, [%{required(:address_hash) => Hash.Address.t(), required(:day) => Date.t()}]}
| {:error, [Changeset.t()]}
defp insert(repo, 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)
combined_changes_list =
changes_list
|> Enum.reduce([], fn change, acc ->
if Enum.empty?(acc) do
[change | acc]
else
target_item =
Enum.find(acc, fn item ->
item.day == change.day && item.address_hash == change.address_hash
end)
if target_item do
if change.value > target_item.value do
acc_updated = List.delete(acc, target_item)
[change | acc_updated]
else
acc
end
else
[change | acc]
end
end
end)
# Enforce CoinBalanceDaily ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(combined_changes_list, &{&1.address_hash, &1.day})
{:ok, _} =
Import.insert_changes_list(
repo,
ordered_changes_list,
conflict_target: [:address_hash, :day],
on_conflict: on_conflict,
for: CoinBalanceDaily,
timeout: timeout,
timestamps: timestamps
)
{:ok, Enum.map(ordered_changes_list, &Map.take(&1, ~w(address_hash day)a))}
end
def default_on_conflict do
from(
balance in CoinBalanceDaily,
update: [
set: [
value:
fragment(
"""
CASE WHEN EXCLUDED.value IS NOT NULL AND EXCLUDED.value > ? THEN
EXCLUDED.value
ELSE
?
END
""",
balance.value,
balance.value
),
inserted_at: fragment("LEAST(EXCLUDED.inserted_at, ?)", balance.inserted_at),
updated_at: fragment("GREATEST(EXCLUDED.updated_at, ?)", balance.updated_at)
]
],
where: fragment("EXCLUDED.value IS NOT NULL")
)
end
end

@ -14,7 +14,8 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do
Runner.Address.CoinBalances,
Runner.Blocks,
Runner.StakingPools,
Runner.StakingPoolsDelegators
Runner.StakingPoolsDelegators,
Runner.Address.CoinBalancesDaily
]
@impl Stage

@ -5,6 +5,7 @@ defmodule Explorer.ChainSpec.Parity.Importer do
require Logger
alias EthereumJSONRPC.Blocks
alias Explorer.{Chain, Repo}
alias Explorer.Chain.Block.{EmissionReward, Range}
alias Explorer.Chain.Hash.Address, as: AddressHash
@ -33,6 +34,21 @@ defmodule Explorer.ChainSpec.Parity.Importer do
end)
|> Enum.to_list()
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
{:ok, %Blocks{blocks_params: [%{timestamp: timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(1..1, json_rpc_named_arguments)
day = DateTime.to_date(timestamp)
balance_daily_params =
chain_spec
|> genesis_accounts()
|> Stream.map(fn balance_map ->
Map.put(balance_map, :day, day)
end)
|> Enum.to_list()
address_params =
balance_params
|> Stream.map(fn %{address_hash: hash} = map ->
@ -40,7 +56,11 @@ defmodule Explorer.ChainSpec.Parity.Importer do
end)
|> Enum.to_list()
params = %{address_coin_balances: %{params: balance_params}, addresses: %{params: address_params}}
params = %{
address_coin_balances: %{params: balance_params},
address_coin_balances_daily: %{params: balance_daily_params},
addresses: %{params: address_params}
}
Chain.import(params)
end

@ -0,0 +1,17 @@
defmodule Explorer.Repo.Migrations.AddressCoinBalancesDaily do
use Ecto.Migration
def change do
create table(:address_coin_balances_daily, primary_key: false) do
add(:address_hash, references(:addresses, column: :hash, type: :bytea), null: false)
add(:day, :date, null: false)
# null until fetched
add(:value, :numeric, precision: 100, default: fragment("NULL"), null: true)
timestamps(null: false, type: :utc_datetime_usec)
end
create(unique_index(:address_coin_balances_daily, [:address_hash, :day]))
end
end

File diff suppressed because one or more lines are too long

@ -4719,6 +4719,8 @@ defmodule Explorer.ChainTest do
block_one_day_ago = insert(:block, timestamp: yesterday, number: 49)
insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block.number)
insert(:fetched_balance, address_hash: address.hash, value: 2000, block_number: block_one_day_ago.number)
insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: noon)
insert(:fetched_balance_daily, address_hash: address.hash, value: 2000, day: yesterday)
balances = Chain.address_to_balances_by_day(address.hash)
@ -4735,6 +4737,7 @@ defmodule Explorer.ChainTest do
yesterday = Timex.shift(noon, days: -1)
block_one_day_ago = insert(:block, timestamp: yesterday)
insert(:fetched_balance, address_hash: address.hash, value: 1000, block_number: block_one_day_ago.number)
insert(:fetched_balance_daily, address_hash: address.hash, value: 1000, day: yesterday)
balances = Chain.address_to_balances_by_day(address.hash)
@ -4754,6 +4757,7 @@ defmodule Explorer.ChainTest do
block_past = insert(:block, timestamp: past, number: 2)
insert(:fetched_balance, address_hash: address.hash, value: 0, block_number: block_past.number)
insert(:fetched_balance_daily, address_hash: address.hash, value: 0, day: today)
[balance] = Chain.address_to_balances_by_day(address.hash)

@ -16,6 +16,7 @@ defmodule Explorer.Factory do
Address.CurrentTokenBalance,
Address.TokenBalance,
Address.CoinBalance,
Address.CoinBalanceDaily,
Block,
ContractMethod,
Data,
@ -56,6 +57,13 @@ defmodule Explorer.Factory do
}
end
def unfetched_balance_daily_factory do
%CoinBalanceDaily{
address_hash: address_hash(),
day: Timex.shift(Timex.now(), days: Enum.random(0..100) * -1)
}
end
def update_balance_value(%CoinBalance{address_hash: address_hash, block_number: block_number}, value) do
Repo.update_all(
from(
@ -71,6 +79,11 @@ defmodule Explorer.Factory do
|> struct!(value: Enum.random(1..100_000))
end
def fetched_balance_daily_factory do
unfetched_balance_daily_factory()
|> struct!(value: Enum.random(1..100_000))
end
def contract_address_factory do
%Address{
hash: address_hash(),

@ -34,6 +34,7 @@ defmodule Indexer.Block.Fetcher do
alias Indexer.Transform.{
AddressCoinBalances,
AddressCoinBalancesDaily,
Addresses,
AddressTokenBalances,
MintTransfers,
@ -55,6 +56,7 @@ defmodule Indexer.Block.Fetcher do
address_hash_to_fetched_balance_block_number: address_hash_to_fetched_balance_block_number,
addresses: Import.Runner.options(),
address_coin_balances: Import.Runner.options(),
address_coin_balances_daily: Import.Runner.options(),
address_token_balances: Import.Runner.options(),
blocks: Import.Runner.options(),
block_second_degree_relations: Import.Runner.options(),
@ -153,6 +155,14 @@ defmodule Indexer.Block.Fetcher do
transactions_params: transactions_with_receipts
}
|> AddressCoinBalances.params_set(),
coin_balances_params_daily_set =
%{
beneficiary_params: MapSet.to_list(beneficiary_params_set),
blocks_params: blocks,
logs_params: logs,
transactions_params: transactions_with_receipts
}
|> AddressCoinBalancesDaily.params_set(),
beneficiaries_with_gas_payment <-
beneficiary_params_set
|> add_gas_payments(transactions_with_receipts, blocks)
@ -164,6 +174,7 @@ defmodule Indexer.Block.Fetcher do
%{
addresses: %{params: addresses},
address_coin_balances: %{params: coin_balances_params_set},
address_coin_balances_daily: %{params: coin_balances_params_daily_set},
address_token_balances: %{params: address_token_balances},
blocks: %{params: blocks},
block_second_degree_relations: %{params: block_second_degree_relations_params},

@ -26,7 +26,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
]
alias Ecto.Changeset
alias EthereumJSONRPC.{FetchedBalances, Subscription}
alias EthereumJSONRPC.{Blocks, FetchedBalances, Subscription}
alias Explorer.Chain
alias Explorer.Chain.Cache.Accounts
alias Explorer.Counters.AverageBlockTime
@ -172,17 +172,25 @@ defmodule Indexer.Block.Realtime.Fetcher do
block_fetcher,
%{
address_coin_balances: %{params: address_coin_balances_params},
address_coin_balances_daily: %{params: address_coin_balances_daily_params},
address_hash_to_fetched_balance_block_number: address_hash_to_block_number,
addresses: %{params: addresses_params},
block_rewards: block_rewards
} = options
) do
with {:balances, {:ok, %{addresses_params: balances_addresses_params, balances_params: balances_params}}} <-
with {:balances,
{:ok,
%{
addresses_params: balances_addresses_params,
balances_params: balances_params,
balances_daily_params: balances_daily_params
}}} <-
{:balances,
balances(block_fetcher, %{
address_hash_to_block_number: address_hash_to_block_number,
addresses_params: addresses_params,
balances_params: address_coin_balances_params
balances_params: address_coin_balances_params,
balances_daily_params: address_coin_balances_daily_params
})},
{block_reward_errors, chain_import_block_rewards} = Map.pop(block_rewards, :errors),
chain_import_options =
@ -191,7 +199,8 @@ defmodule Indexer.Block.Realtime.Fetcher do
|> put_in([:addresses, :params], balances_addresses_params)
|> put_in([:blocks, :params, Access.all(), :consensus], true)
|> put_in([:block_rewards], chain_import_block_rewards)
|> put_in([Access.key(:address_coin_balances, %{}), :params], balances_params),
|> put_in([Access.key(:address_coin_balances, %{}), :params], balances_params)
|> put_in([Access.key(:address_coin_balances_daily, %{}), :params], balances_daily_params),
{:import, {:ok, imported} = ok} <- {:import, Chain.import(chain_import_options)} do
async_import_remaining_block_data(
imported,
@ -381,7 +390,33 @@ defmodule Indexer.Block.Realtime.Fetcher do
importable_balances_params = Enum.map(params_list, &Map.put(&1, :value_fetched_at, value_fetched_at))
{:ok, %{addresses_params: merged_addresses_params, balances_params: importable_balances_params}}
block_numbers =
params_list
|> Enum.map(&Map.get(&1, :block_number))
|> Enum.sort()
|> Enum.dedup()
block_timestamp_map =
Enum.reduce(block_numbers, %{}, fn block_number, map ->
{:ok, %Blocks{blocks_params: [%{timestamp: timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(timestamp)
Map.put(map, "#{block_number}", day)
end)
importable_balances_daily_params =
Enum.map(params_list, fn param ->
day = Map.get(block_timestamp_map, "#{param.block_number}")
Map.put(param, :day, day)
end)
{:ok,
%{
addresses_params: merged_addresses_params,
balances_params: importable_balances_params,
balances_daily_params: importable_balances_daily_params
}}
{:error, _} = error ->
error

@ -21,7 +21,7 @@ defmodule Indexer.Fetcher.BlockReward do
alias Indexer.{BufferedTask, Tracer}
alias Indexer.Fetcher.BlockReward.Supervisor, as: BlockRewardSupervisor
alias Indexer.Fetcher.CoinBalance
alias Indexer.Transform.{AddressCoinBalances, Addresses}
alias Indexer.Transform.{AddressCoinBalances, AddressCoinBalancesDaily, Addresses}
@behaviour BufferedTask
@ -245,9 +245,13 @@ defmodule Indexer.Fetcher.BlockReward do
addresses_params = Addresses.extract_addresses(%{block_reward_contract_beneficiaries: block_rewards_params})
address_coin_balances_params_set = AddressCoinBalances.params_set(%{beneficiary_params: block_rewards_params})
address_coin_balances_daily_params_set =
AddressCoinBalancesDaily.params_set(%{beneficiary_params: block_rewards_params})
Chain.import(%{
addresses: %{params: addresses_params},
address_coin_balances: %{params: address_coin_balances_params_set},
address_coin_balances_daily: %{params: address_coin_balances_daily_params_set},
block_rewards: %{params: block_rewards_params}
})
end

@ -11,7 +11,7 @@ defmodule Indexer.Fetcher.CoinBalance do
import EthereumJSONRPC, only: [integer_to_quantity: 1, quantity_to_integer: 1]
alias EthereumJSONRPC.FetchedBalances
alias EthereumJSONRPC.{Blocks, FetchedBalances}
alias Explorer.Chain
alias Explorer.Chain.{Block, Hash}
alias Explorer.Chain.Cache.Accounts
@ -141,11 +141,88 @@ defmodule Indexer.Fetcher.CoinBalance do
importable_balances_params = Enum.map(params_list, &Map.put(&1, :value_fetched_at, value_fetched_at))
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
block_numbers =
params_list
|> Enum.map(&Map.get(&1, :block_number))
|> Enum.sort()
|> Enum.dedup()
block_timestamp_map =
Enum.reduce(block_numbers, %{}, fn block_number, map ->
{:ok, %Blocks{blocks_params: [%{timestamp: timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(timestamp)
Map.put(map, "#{block_number}", day)
end)
importable_balances_daily_params =
params_list
|> Enum.map(fn balance_param ->
day = Map.get(block_timestamp_map, "#{balance_param.block_number}")
incoming_balance_daily_param = %{
address_hash: balance_param.address_hash,
day: day,
value: balance_param.value
}
incoming_balance_daily_param
end)
addresses_params = balances_params_to_address_params(importable_balances_params)
Chain.import(%{
addresses: %{params: addresses_params, with: :balance_changeset},
address_coin_balances: %{params: importable_balances_params},
address_coin_balances_daily: %{params: importable_balances_daily_params},
broadcast: broadcast_type
})
end
def import_fetched_daily_balances(%FetchedBalances{params_list: params_list}, broadcast_type \\ false) do
value_fetched_at = DateTime.utc_now()
importable_balances_params = Enum.map(params_list, &Map.put(&1, :value_fetched_at, value_fetched_at))
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
block_numbers =
params_list
|> Enum.map(&Map.get(&1, :block_number))
|> Enum.sort()
|> Enum.dedup()
block_timestamp_map =
Enum.reduce(block_numbers, %{}, fn block_number, map ->
{:ok, %Blocks{blocks_params: [%{timestamp: timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(timestamp)
Map.put(map, "#{block_number}", day)
end)
importable_balances_daily_params =
params_list
|> Enum.map(fn balance_param ->
day = Map.get(block_timestamp_map, "#{balance_param.block_number}")
incoming_balance_daily_param = %{
address_hash: balance_param.address_hash,
day: day,
value: balance_param.value
}
incoming_balance_daily_param
end)
addresses_params = balances_params_to_address_params(importable_balances_params)
Chain.import(%{
addresses: %{params: addresses_params, with: :balance_changeset},
address_coin_balances_daily: %{params: importable_balances_daily_params},
broadcast: broadcast_type
})
end

@ -15,10 +15,10 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
import Ecto.Query, only: [from: 2]
import EthereumJSONRPC, only: [integer_to_quantity: 1]
alias EthereumJSONRPC.FetchedBalances
alias EthereumJSONRPC.{FetchedBalances}
alias Explorer.{Chain, Repo}
alias Explorer.Chain.Address
alias Explorer.Chain.Address.CoinBalance
alias Explorer.Chain.Address.{CoinBalance, CoinBalanceDaily}
alias Explorer.Chain.Cache.{Accounts, BlockNumber}
alias Explorer.Counters.AverageBlockTime
alias Indexer.Fetcher.CoinBalance, as: CoinBalanceFetcher
@ -86,6 +86,12 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
{:noreply, state}
end
def handle_cast({:fetch_and_import_daily_balances, block_number, address}, state) do
fetch_and_import_daily_balances(block_number, address, state.json_rpc_named_arguments)
{:noreply, state}
end
## Implementation
defp do_trigger_fetch(%Address{fetched_coin_balance_block_number: nil} = address, latest_block_number, _) do
@ -95,6 +101,14 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
end
defp do_trigger_fetch(address, latest_block_number, stale_balance_window) do
latest_by_day =
from(
cbd in CoinBalanceDaily,
where: cbd.address_hash == ^address.hash,
order_by: [desc: :day],
limit: 1
)
latest =
from(
cb in CoinBalance,
@ -105,15 +119,28 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
limit: 1
)
do_trigger_balance_fetch_query(address, latest_block_number, stale_balance_window, latest, latest_by_day)
end
defp do_trigger_balance_fetch_query(
address,
latest_block_number,
stale_balance_window,
query_balances,
query_balances_daily
) do
if address.fetched_coin_balance_block_number < stale_balance_window do
do_trigger_balance_daily_fetch_query(address, latest_block_number, query_balances_daily)
GenServer.cast(__MODULE__, {:fetch_and_update, latest_block_number, address})
{:stale, latest_block_number}
else
case Repo.one(latest) do
case Repo.one(query_balances) do
nil ->
# There is no recent coin balance to fetch, so we check to see how old the
# balance is on the address. If it is too old, we check again, just to be safe.
do_trigger_balance_daily_fetch_query(address, latest_block_number, query_balances_daily)
:current
%CoinBalance{value_fetched_at: nil, block_number: block_number} ->
@ -122,11 +149,19 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
{:pending, block_number}
%CoinBalance{} ->
do_trigger_balance_daily_fetch_query(address, latest_block_number, query_balances_daily)
:current
end
end
end
defp do_trigger_balance_daily_fetch_query(address, latest_block_number, query) do
if Repo.one(query) == nil do
GenServer.cast(__MODULE__, {:fetch_and_import_daily_balances, latest_block_number, address})
end
end
defp fetch_and_import(block_number, address, json_rpc_named_arguments) do
case fetch_balances(block_number, address, json_rpc_named_arguments) do
{:ok, fetched_balances} -> do_import(fetched_balances)
@ -134,6 +169,13 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
end
end
defp fetch_and_import_daily_balances(block_number, address, json_rpc_named_arguments) do
case fetch_balances(block_number, address, json_rpc_named_arguments) do
{:ok, fetched_balances} -> do_import_daily_balances(fetched_balances)
_ -> :ok
end
end
defp fetch_and_update(block_number, address, json_rpc_named_arguments) do
case fetch_balances(block_number, address, json_rpc_named_arguments) do
{:ok, %{params_list: []}} ->
@ -165,6 +207,13 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
end
end
defp do_import_daily_balances(%FetchedBalances{} = fetched_balances) do
case CoinBalanceFetcher.import_fetched_daily_balances(fetched_balances, :on_demand) do
{:ok, %{addresses: [address]}} -> {:ok, address}
_ -> :error
end
end
defp latest_block_number do
BlockNumber.get_max()
end

@ -0,0 +1,155 @@
defmodule Indexer.Transform.AddressCoinBalancesDaily do
@moduledoc """
Extracts `Explorer.Chain.Address.CoinBalanceDaily` params from other schema's params.
"""
alias EthereumJSONRPC.Blocks
def params_set(%{} = import_options) do
Enum.reduce(import_options, MapSet.new(), &reducer/2)
end
defp reducer({:beneficiary_params, beneficiary_params}, acc) when is_list(beneficiary_params) do
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
block_numbers =
beneficiary_params
|> Enum.map(&Map.get(&1, :block_number))
|> Enum.sort()
|> Enum.dedup()
block_timestamp_map =
Enum.reduce(block_numbers, %{}, fn block_number, map ->
{:ok, %Blocks{blocks_params: [%{timestamp: timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(timestamp)
Map.put(map, "#{block_number}", day)
end)
Enum.into(beneficiary_params, acc, fn %{
address_hash: address_hash,
block_number: block_number
}
when is_binary(address_hash) and is_integer(block_number) ->
day = Map.get(block_timestamp_map, "#{block_number}")
%{address_hash: address_hash, day: day}
end)
end
defp reducer({:blocks_params, blocks_params}, acc) when is_list(blocks_params) do
# a block MUST have a miner_hash and number
Enum.into(blocks_params, acc, fn %{miner_hash: address_hash, number: block_number, timestamp: block_timestamp}
when is_binary(address_hash) and is_integer(block_number) ->
day = DateTime.to_date(block_timestamp)
%{address_hash: address_hash, day: day}
end)
end
defp reducer({:internal_transactions_params, internal_transactions_params}, initial)
when is_list(internal_transactions_params) do
Enum.reduce(internal_transactions_params, initial, &internal_transactions_params_reducer/2)
end
defp reducer({:logs_params, logs_params}, acc) when is_list(logs_params) do
# a log MUST have address_hash and block_number
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
block_numbers =
logs_params
|> Enum.map(&Map.get(&1, :block_number))
|> Enum.sort()
|> Enum.dedup()
block_timestamp_map =
Enum.reduce(block_numbers, %{}, fn block_number, map ->
{:ok, %Blocks{blocks_params: [%{timestamp: timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(timestamp)
Map.put(map, "#{block_number}", day)
end)
logs_params
|> Enum.into(acc, fn
%{address_hash: address_hash, block_number: block_number}
when is_binary(address_hash) and is_integer(block_number) ->
day = Map.get(block_timestamp_map, "#{block_number}")
%{address_hash: address_hash, day: day}
%{type: "pending"} ->
nil
end)
|> Enum.reject(fn val -> is_nil(val) end)
|> MapSet.new()
end
defp reducer({:transactions_params, transactions_params}, initial) when is_list(transactions_params) do
Enum.reduce(transactions_params, initial, &transactions_params_reducer/2)
end
defp reducer({:block_second_degree_relations_params, block_second_degree_relations_params}, initial)
when is_list(block_second_degree_relations_params),
do: initial
defp internal_transactions_params_reducer(
%{block_number: block_number} = internal_transaction_params,
acc
)
when is_integer(block_number) do
case internal_transaction_params do
%{type: "call"} ->
acc
%{type: "create", error: _} ->
acc
%{type: "create", created_contract_address_hash: address_hash} when is_binary(address_hash) ->
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
{:ok, %Blocks{blocks_params: [%{timestamp: block_timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(block_timestamp)
MapSet.put(acc, %{address_hash: address_hash, day: day})
%{type: "selfdestruct", from_address_hash: from_address_hash, to_address_hash: to_address_hash}
when is_binary(from_address_hash) and is_binary(to_address_hash) ->
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
{:ok, %Blocks{blocks_params: [%{timestamp: block_timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(block_timestamp)
acc
|> MapSet.put(%{address_hash: from_address_hash, day: day})
|> MapSet.put(%{address_hash: to_address_hash, day: day})
end
end
defp transactions_params_reducer(
%{block_number: block_number, from_address_hash: from_address_hash} = transaction_params,
initial
)
when is_binary(from_address_hash) do
# a transaction MUST have a `from_address_hash`
json_rpc_named_arguments = Application.get_env(:explorer, :json_rpc_named_arguments)
{:ok, %Blocks{blocks_params: [%{timestamp: block_timestamp}]}} =
EthereumJSONRPC.fetch_blocks_by_range(block_number..block_number, json_rpc_named_arguments)
day = DateTime.to_date(block_timestamp)
acc = MapSet.put(initial, %{address_hash: from_address_hash, day: day})
# `to_address_hash` is optional
case transaction_params do
%{to_address_hash: to_address_hash} when is_binary(to_address_hash) ->
MapSet.put(acc, %{address_hash: to_address_hash, day: day})
_ ->
acc
end
end
end

@ -79,6 +79,8 @@ defmodule Indexer.Block.FetcherTest do
block_quantity = integer_to_quantity(block_number)
miner_hash = "0x0000000000000000000000000000000000000000"
res = eth_block_number_fake_response(block_quantity)
case Keyword.fetch!(json_rpc_named_arguments, :variant) do
EthereumJSONRPC.Parity ->
EthereumJSONRPC.Mox
@ -137,6 +139,17 @@ defmodule Indexer.Block.FetcherTest do
{:ok, [%{id: id, result: []}]}
end
end)
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
EthereumJSONRPC.Geth ->
EthereumJSONRPC.Mox
@ -381,10 +394,78 @@ defmodule Indexer.Block.FetcherTest do
end)
# async requests need to be grouped in one expect because the order is non-deterministic while multiple expect
# calls on the same name/arity are used in order
|> expect(:json_rpc, 5, fn json, _options ->
|> expect(:json_rpc, 11, fn json, _options ->
[request] = json
case request do
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
} ->
{:ok,
[
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"difficulty" => "0xfffffffffffffffffffffffffffffffe",
"extraData" => "0xd5830108048650617269747986312e32322e31826c69",
"gasLimit" => "0x69fe20",
"gasUsed" => "0xc512",
"hash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000200000000000000000000020000000000000000200000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"number" => "0x25",
"parentHash" => "0xc37bbad7057945d1bf128c1ff009fb1ad632110bf6a000aac025a80f7766b66e",
"receiptsRoot" => "0xd300311aab7dcc98c05ac3f1893629b2c9082c189a0a0c76f4f63e292ac419d5",
"sealFields" => [
"0x84120a71de",
"0xb841fcdb570511ec61edda93849bb7c6b3232af60feb2ea74e4035f0143ab66dfdd00f67eb3eda1adddbb6b572db1e0abd39ce00f9b3ccacb9f47973279ff306fe5401"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"fcdb570511ec61edda93849bb7c6b3232af60feb2ea74e4035f0143ab66dfdd00f67eb3eda1adddbb6b572db1e0abd39ce00f9b3ccacb9f47973279ff306fe5401",
"size" => "0x2cf",
"stateRoot" => "0x2cd84079b0d0c267ed387e3895fd1c1dc21ff82717beb1132adac64276886e19",
"step" => "302674398",
"timestamp" => "0x5a343956",
"totalDifficulty" => "0x24ffffffffffffffffffffffffedf78dfd",
"transactions" => [
%{
"blockHash" => "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
"blockNumber" => "0x25",
"chainId" => "0x4d",
"condition" => nil,
"creates" => nil,
"from" => from_address_hash,
"gas" => "0x47b760",
"gasPrice" => "0x174876e800",
"hash" => transaction_hash,
"input" => "0x10855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
"nonce" => "0x4",
"publicKey" =>
"0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
"r" => "0xa7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01",
"raw" =>
"0xf88a0485174876e8008347b760948bf38d4764929064f2d4d3a56520a76ab3df415b80a410855269000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef81bea0a7f8f45cce375bb7af8750416e1b03e0473f93c256da2285d1134fc97a700e01a01f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f",
"s" => "0x1f87a076f13824f4be8963e3dffd7300dae64d5f23c9a062af0c6ead347c135f",
"standardV" => "0x1",
"to" => to_address_hash,
"transactionIndex" => "0x0",
"v" => "0xbe",
"value" => "0x0"
}
],
"transactionsRoot" => "0x68e314a05495f390f9cd0c36267159522e5450d2adf254a74567b452e767bf34",
"uncles" => []
}
}
]}
%{id: id, method: "eth_getBalance", params: [^to_address_hash, ^block_quantity]} ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x1"}]}
@ -662,6 +743,22 @@ defmodule Indexer.Block.FetcherTest do
}
%{id: id, method: "trace_block"} ->
block_quantity = integer_to_quantity(block_number)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
%{
id: id,
result: [
@ -742,4 +839,40 @@ defmodule Indexer.Block.FetcherTest do
counts.buffer == 0 and counts.tasks == 0
end)
end
defp eth_block_number_fake_response(block_quantity) do
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x0000000000000000000000000000000000000000",
"difficulty" => "0x20000",
"extraData" => "0x",
"gasLimit" => "0x663be0",
"gasUsed" => "0x0",
"hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x0000000000000000000000000000000000000000",
"number" => block_quantity,
"parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x80",
"0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"size" => "0x215",
"stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
"step" => "0",
"timestamp" => "0x0",
"totalDifficulty" => "0x20000",
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
}
end
end

@ -205,7 +205,7 @@ defmodule Indexer.Block.Realtime.FetcherTest do
}
]}
end)
|> expect(:json_rpc, 2, fn
|> expect(:json_rpc, 5, fn
[
%{id: 0, jsonrpc: "2.0", method: "trace_block", params: ["0x3C365F"]},
%{id: 1, jsonrpc: "2.0", method: "trace_block", params: ["0x3C3660"]}
@ -217,6 +217,124 @@ defmodule Indexer.Block.Realtime.FetcherTest do
%{id: 1, jsonrpc: "2.0", result: []}
]}
[
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: ["0x3C365F", true]
}
],
_ ->
{:ok,
[
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2",
"difficulty" => "0xfffffffffffffffffffffffffffffffe",
"extraData" => "0xd583010b088650617269747986312e32372e32826c69",
"gasLimit" => "0x7a1200",
"gasUsed" => "0x2886e",
"hash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x5ee341ac44d344ade1ca3a771c59b98eb2a77df2",
"number" => "0x3c365f",
"parentHash" => "0x57f6d66e07488defccd5216c4d2968dd6afd3bd32415e284de3b02af6535e8dc",
"receiptsRoot" => "0x111be72e682cea9c93e02f1ef503fb64aa821b2ef510fd9177c49b37d0af98b5",
"sealFields" => [
"0x841246c63f",
"0xb841ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"ba3d11db672fd7893d1b7906275fa7c4c7f4fbcc8fa29eab0331480332361516545ef10a36d800ad2be2b449dde8d5703125156a9cf8a035f5a8623463e051b700",
"size" => "0x33e",
"stateRoot" => "0x7f73f5fb9f891213b671356126c31e9795d038844392c7aa8800ed4f52307209",
"step" => "306628159",
"timestamp" => "0x5b61df3b",
"totalDifficulty" => "0x3c365effffffffffffffffffffffffed7f0362",
"transactions" => [
%{
"blockHash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc",
"blockNumber" => "0x3c365f",
"chainId" => "0x63",
"condition" => nil,
"creates" => nil,
"from" => "0x40b18103537c0f15d5e137dd8ddd019b84949d16",
"gas" => "0x3d9c5",
"gasPrice" => "0x3b9aca00",
"hash" => "0xd3937e70fab3fb2bfe8feefac36815408bf07de3b9e09fe81114b9a6b17f55c8",
"input" =>
"0x8841ac11000000000000000000000000000000000000000000000000000000000000006c000000000000000000000000000000000000000000000000000000000000000500000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005",
"nonce" => "0x65b",
"publicKey" =>
"0x89c2123ed4b5d141cf1f4b6f5f3d754418f03aea2e870a1c50888d94bf5531f74237e2fea72d0bc198ef213272b62c6869615720757255e6cba087f9db6e759f",
"r" => "0x55a1a93541d7f782f97f6699437bb60fa4606d63760b30c1ee317e648f93995",
"raw" =>
"0xf8f582065b843b9aca008303d9c594698bf6943bab687b2756394624aa183f434f65da8901158e4f216242a000b8848841ac11000000000000000000000000000000000000000000000000000000000000006c00000000000000000000000000000000000000000000000000000000000000050000000000000000000000000000000000000000000000000000000000000005000000000000000000000000000000000000000000000000000000000000000581eaa0055a1a93541d7f782f97f6699437bb60fa4606d63760b30c1ee317e648f93995a06affd4da5eca84fbca2b016c980f861e0af1f8d6535e2fe29d8f96dc0ce358f7",
"s" => "0x6affd4da5eca84fbca2b016c980f861e0af1f8d6535e2fe29d8f96dc0ce358f7",
"standardV" => "0x1",
"to" => "0x698bf6943bab687b2756394624aa183f434f65da",
"transactionIndex" => "0x0",
"v" => "0xea",
"value" => "0x1158e4f216242a000"
}
],
"transactionsRoot" => "0xd7c39a93eafe0bdcbd1324c13dcd674bed8c9fa8adbf8f95bf6a59788985da6f",
"uncles" => ["0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cd"]
}
}
]}
[
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: ["0x3C3660", true]
}
],
_ ->
{:ok,
[
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3",
"difficulty" => "0xfffffffffffffffffffffffffffffffe",
"extraData" => "0xd583010a068650617269747986312e32362e32826c69",
"gasLimit" => "0x7a1200",
"gasUsed" => "0x0",
"hash" => "0xfb483e511d316fa4072694da3f7abc94b06286406af45061e5e681395bdc6815",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x66c9343c7e8ca673a1fedf9dbf2cd7936dbbf7e3",
"number" => "0x3c3660",
"parentHash" => "0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cc",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x841246c640",
"0xb84114db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"14db3fd7526b7ea3635f5c85c30dd8a645453aa2f8afe5fd33fe0ec663c9c7b653b0fb5d8dc7d0b809674fa9dca9887d1636a586bf62191da22255eb068bf20800",
"size" => "0x243",
"stateRoot" => "0x3174c461989e9f99e08fa9b4ffb8bce8d9a281c8fc9f80694bb9d3acd4f15559",
"step" => "306628160",
"timestamp" => "0x5b61df40",
"totalDifficulty" => "0x3c365fffffffffffffffffffffffffed7f0360",
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
}
]}
[
%{
id: 0,

@ -117,6 +117,21 @@ defmodule Indexer.Fetcher.BlockRewardTest do
}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
assert count(Chain.Block.Reward) == 0
parent = self()
@ -190,6 +205,21 @@ defmodule Indexer.Fetcher.BlockRewardTest do
}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
parent = self()
pid =
@ -320,6 +350,21 @@ defmodule Indexer.Fetcher.BlockRewardTest do
}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
assert count(Chain.Block.Reward) == 0
assert count(Chain.Address.CoinBalance) == 0
@ -408,6 +453,21 @@ defmodule Indexer.Fetcher.BlockRewardTest do
}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
assert count(Chain.Block.Reward) == 0
assert count(Chain.Address.CoinBalance) == 0
@ -486,6 +546,21 @@ defmodule Indexer.Fetcher.BlockRewardTest do
}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
assert count(Chain.Block.Reward) == 1
assert count(Chain.Address.CoinBalance) == 1
@ -625,6 +700,21 @@ defmodule Indexer.Fetcher.BlockRewardTest do
}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
assert count(Chain.Block.Reward) == 0
assert count(Chain.Address.CoinBalance) == 0
@ -684,4 +774,40 @@ defmodule Indexer.Fetcher.BlockRewardTest do
do_wait_until(parent, ref, producer)
end
end
defp eth_block_number_fake_response(block_quantity) do
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x0000000000000000000000000000000000000000",
"difficulty" => "0x20000",
"extraData" => "0x",
"gasLimit" => "0x663be0",
"gasUsed" => "0x0",
"hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x0000000000000000000000000000000000000000",
"number" => block_quantity,
"parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x80",
"0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"size" => "0x215",
"stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
"step" => "0",
"timestamp" => "0x0",
"totalDifficulty" => "0x20000",
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
}
end
end

@ -117,6 +117,21 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]}
end)
res = eth_block_number_fake_response("0x65")
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: ["0x65", true]
}
],
_ ->
{:ok, [res]}
end)
assert CoinBalanceOnDemand.trigger_fetch(address) == {:stale, 101}
{:ok, expected_wei} = Wei.cast(2)
@ -144,6 +159,20 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]}
end)
EthereumJSONRPC.Mox
|> expect(:json_rpc, 1, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: ["0x66", true]
}
],
_ ->
res = eth_block_number_fake_response("0x66")
{:ok, [res]}
end)
assert CoinBalanceOnDemand.trigger_fetch(address) == {:pending, 102}
{:ok, expected_wei} = Wei.cast(2)
@ -154,4 +183,40 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemandTest do
)
end
end
defp eth_block_number_fake_response(block_quantity) do
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x0000000000000000000000000000000000000000",
"difficulty" => "0x20000",
"extraData" => "0x",
"gasLimit" => "0x663be0",
"gasUsed" => "0x0",
"hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x0000000000000000000000000000000000000000",
"number" => block_quantity,
"parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x80",
"0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"size" => "0x215",
"stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
"step" => "0",
"timestamp" => "0x0",
"totalDifficulty" => "0x20000",
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
}
end
end

@ -58,6 +58,21 @@ defmodule Indexer.Fetcher.CoinBalanceTest do
_options ->
{:ok, [%{id: id, result: integer_to_quantity(fetched_balance)}]}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
end
{:ok, miner_hash} = Hash.Address.cast(miner_hash_data)
@ -114,6 +129,21 @@ defmodule Indexer.Fetcher.CoinBalanceTest do
_options ->
{:ok, [%{id: id, result: integer_to_quantity(fetched_balance)}]}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
end
{:ok, miner_hash} = Hash.Address.cast(miner_hash_data)
@ -178,6 +208,21 @@ defmodule Indexer.Fetcher.CoinBalanceTest do
_options ->
{:ok, [%{id: id, result: integer_to_quantity(fetched_balance)}]}
end)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
end
CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
@ -254,6 +299,25 @@ defmodule Indexer.Fetcher.CoinBalanceTest do
{:ok, %Hash{bytes: address_hash_bytes}} = Hash.Address.cast(hash_data)
entries = Enum.map(block_quantities, &{address_hash_bytes, quantity_to_integer(&1)})
res1 = eth_block_number_fake_response("0x1")
res2 = eth_block_number_fake_response("0x2")
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x1", true]}
],
_ ->
{:ok, [res1]}
end)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{id: 0, jsonrpc: "2.0", method: "eth_getBlockByNumber", params: ["0x2", true]}
],
_ ->
{:ok, [res2]}
end)
case CoinBalance.run(entries, json_rpc_named_arguments) do
:ok ->
balances = Repo.all(from(balance in Address.CoinBalance, where: balance.address_hash == ^hash_data))
@ -314,16 +378,33 @@ defmodule Indexer.Fetcher.CoinBalanceTest do
test "retries none if all imported and no fetch errors", %{json_rpc_named_arguments: json_rpc_named_arguments} do
%Hash{bytes: address_hash_bytes} = address_hash()
entries = [{address_hash_bytes, block_number()}]
block_number = block_number()
entries = [{address_hash_bytes, block_number}]
expect(EthereumJSONRPC.Mox, :json_rpc, fn [%{id: id, method: "eth_getBalance", params: [_, _]}], _ ->
{:ok, [%{id: id, result: "0x1"}]}
end)
block_quantity = integer_to_quantity(block_number)
res = eth_block_number_fake_response(block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [^block_quantity, true]
}
],
_ ->
{:ok, [res]}
end)
assert :ok = CoinBalance.run(entries, json_rpc_named_arguments)
end
test "retries retries fetch errors if all imported", %{json_rpc_named_arguments: json_rpc_named_arguments} do
test "retries fetch errors if all imported", %{json_rpc_named_arguments: json_rpc_named_arguments} do
%Hash{bytes: address_hash_bytes} = address_hash()
bad_block_number = block_number()
good_block_number = block_number()
@ -359,6 +440,22 @@ defmodule Indexer.Fetcher.CoinBalanceTest do
{:ok, responses}
end)
bad_block_quantity = integer_to_quantity(bad_block_number)
res_bad = eth_block_number_fake_response(bad_block_quantity)
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
%{
id: 0,
jsonrpc: "2.0",
method: "eth_getBlockByNumber",
params: [bad_block_quantity, true]
}
],
[] ->
{:ok, [res_bad]}
end)
assert {:retry, [{^address_hash_bytes, ^bad_block_number}]} =
CoinBalance.run(
[{address_hash_bytes, good_block_number}, {address_hash_bytes, bad_block_number}],
@ -374,4 +471,40 @@ defmodule Indexer.Fetcher.CoinBalanceTest do
Process.sleep(100)
wait(producer)
end
defp eth_block_number_fake_response(block_quantity) do
%{
id: 0,
jsonrpc: "2.0",
result: %{
"author" => "0x0000000000000000000000000000000000000000",
"difficulty" => "0x20000",
"extraData" => "0x",
"gasLimit" => "0x663be0",
"gasUsed" => "0x0",
"hash" => "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
"logsBloom" =>
"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"miner" => "0x0000000000000000000000000000000000000000",
"number" => block_quantity,
"parentHash" => "0x0000000000000000000000000000000000000000000000000000000000000000",
"receiptsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"sealFields" => [
"0x80",
"0xb8410000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"
],
"sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
"signature" =>
"0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"size" => "0x215",
"stateRoot" => "0xfad4af258fd11939fae0c6c6eec9d340b1caac0b0196fd9a1bc3f489c5bf00b3",
"step" => "0",
"timestamp" => "0x0",
"totalDifficulty" => "0x20000",
"transactions" => [],
"transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
"uncles" => []
}
}
end
end

Loading…
Cancel
Save