Merge branch 'master' into pools_fetching

pull/1801/head
saneery 6 years ago
commit b972c91ab5
  1. 4
      .credo.exs
  2. 14
      CHANGELOG.md
  3. 8
      apps/block_scout_web/assets/css/components/_navbar.scss
  4. 7
      apps/block_scout_web/lib/block_scout_web/templates/layout/_topnav.html.eex
  5. 1
      apps/block_scout_web/lib/block_scout_web/views/layout_view.ex
  6. 1
      apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs
  7. 1
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/stats_controller_test.exs
  8. 2
      apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
  9. 14
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  10. 7
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
  11. 15
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block/by_nephew.ex
  12. 3
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex
  13. 17
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncle.ex
  14. 6
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncles.ex
  15. 17
      apps/explorer/lib/explorer/chain.ex
  16. 10
      apps/explorer/lib/explorer/chain/block/second_degree_relation.ex
  17. 25
      apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex
  18. 22
      apps/explorer/lib/explorer/chain/supply/coin_market_cap.ex
  19. 1
      apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
  20. 1
      apps/explorer/lib/explorer/exchange_rates/source/coin_market_cap.ex
  21. 1
      apps/explorer/lib/explorer/exchange_rates/source/transaction_and_log.ex
  22. 15
      apps/explorer/lib/explorer/exchange_rates/token.ex
  23. 10
      apps/explorer/priv/repo/migrations/20190421143300_add_index_to_bsdr.exs
  24. 16
      apps/explorer/test/explorer/chain/block/second_degree_relation_test.exs
  25. 2
      apps/explorer/test/explorer/chain/import_test.exs
  26. 15
      apps/explorer/test/explorer/chain_test.exs
  27. 1
      apps/explorer/test/explorer/exchange_rates/exchange_rates_test.exs
  28. 1
      apps/explorer/test/explorer/exchange_rates/source/coin_gecko_test.exs
  29. 1
      apps/explorer/test/explorer/exchange_rates/source/coin_market_cap_test.exs
  30. 3
      apps/explorer/test/support/factory.ex
  31. 1
      apps/explorer/test/support/fakes/one_coin_source.ex
  32. 1
      apps/indexer/README.md
  33. 4
      apps/indexer/lib/indexer/block/fetcher.ex
  34. 55
      apps/indexer/lib/indexer/fetcher/uncle_block.ex
  35. 7
      apps/indexer/lib/indexer/supervisor.ex
  36. 164
      apps/indexer/lib/indexer/temporary/uncles_without_index.ex
  37. 11
      apps/indexer/test/indexer/block/catchup/fetcher_test.exs
  38. 20
      apps/indexer/test/indexer/fetcher/uncle_block_test.exs

@ -67,6 +67,9 @@
{Credo.Check.Readability.TrailingBlankLine, false}, {Credo.Check.Readability.TrailingBlankLine, false},
{Credo.Check.Readability.TrailingWhiteSpace, false}, {Credo.Check.Readability.TrailingWhiteSpace, false},
# outdated by lazy Logger in Elixir 1.7. See https://elixir-lang.org/blog/2018/07/25/elixir-v1-7-0-released/
{Credo.Check.Warning.LazyLogging, false},
# not handled by formatter # not handled by formatter
{Credo.Check.Consistency.ExceptionNames}, {Credo.Check.Consistency.ExceptionNames},
{Credo.Check.Consistency.ParameterPatternMatching}, {Credo.Check.Consistency.ParameterPatternMatching},
@ -118,7 +121,6 @@
{Credo.Check.Warning.ExpensiveEmptyEnumCheck}, {Credo.Check.Warning.ExpensiveEmptyEnumCheck},
{Credo.Check.Warning.IExPry}, {Credo.Check.Warning.IExPry},
{Credo.Check.Warning.IoInspect}, {Credo.Check.Warning.IoInspect},
{Credo.Check.Warning.LazyLogging},
{Credo.Check.Warning.OperationOnSameValues}, {Credo.Check.Warning.OperationOnSameValues},
{Credo.Check.Warning.OperationWithConstantResult}, {Credo.Check.Warning.OperationWithConstantResult},
{Credo.Check.Warning.UnusedEnumOperation}, {Credo.Check.Warning.UnusedEnumOperation},

@ -2,6 +2,15 @@
### Features ### Features
### Fixes
### Chore
## 1.3.10-beta
### Features
- [#1739](https://github.com/poanetwork/blockscout/pull/1739) - highlight decompiled source code - [#1739](https://github.com/poanetwork/blockscout/pull/1739) - highlight decompiled source code
- [#1696](https://github.com/poanetwork/blockscout/pull/1696) - full-text search by tokens - [#1696](https://github.com/poanetwork/blockscout/pull/1696) - full-text search by tokens
- [#1742](https://github.com/poanetwork/blockscout/pull/1742) - Support RSK - [#1742](https://github.com/poanetwork/blockscout/pull/1742) - Support RSK
@ -23,10 +32,14 @@
- [#1790](https://github.com/poanetwork/blockscout/pull/1790) - fix constructor arguments verification - [#1790](https://github.com/poanetwork/blockscout/pull/1790) - fix constructor arguments verification
- [#1793](https://github.com/poanetwork/blockscout/pull/1793) - fix top nav autocomplete - [#1793](https://github.com/poanetwork/blockscout/pull/1793) - fix top nav autocomplete
- [#1795](https://github.com/poanetwork/blockscout/pull/1795) - fix line numbers for decompiled contracts - [#1795](https://github.com/poanetwork/blockscout/pull/1795) - fix line numbers for decompiled contracts
- [#1803](https://github.com/poanetwork/blockscout/pull/1803) - use coinmarketcap for total_supply by default
- [#1802](https://github.com/poanetwork/blockscout/pull/1802) - make coinmarketcap's number of pages configurable - [#1802](https://github.com/poanetwork/blockscout/pull/1802) - make coinmarketcap's number of pages configurable
- [#1799](https://github.com/poanetwork/blockscout/pull/1799) - Use eth_getUncleByBlockHashAndIndex for uncle block fetching
- [#1531](https://github.com/poanetwork/blockscout/pull/1531) - docker: fix dockerFile for secp256k1 building
### Chore ### Chore
- [#1804](https://github.com/poanetwork/blockscout/pull/1804) - (Chore) Divide chains by Mainnet/Testnet in menu
- [#1783](https://github.com/poanetwork/blockscout/pull/1783) - Update README with the chains that use Blockscout - [#1783](https://github.com/poanetwork/blockscout/pull/1783) - Update README with the chains that use Blockscout
- [#1780](https://github.com/poanetwork/blockscout/pull/1780) - Update link to the Github repo in the footer - [#1780](https://github.com/poanetwork/blockscout/pull/1780) - Update link to the Github repo in the footer
- [#1757](https://github.com/poanetwork/blockscout/pull/1757) - Change twitter acc link to official Blockscout acc twitter - [#1757](https://github.com/poanetwork/blockscout/pull/1757) - Change twitter acc link to official Blockscout acc twitter
@ -35,6 +48,7 @@
- [#1753](https://github.com/poanetwork/blockscout/pull/1753) - Add a check mark to decompiled contract tab - [#1753](https://github.com/poanetwork/blockscout/pull/1753) - Add a check mark to decompiled contract tab
- [#1744](https://github.com/poanetwork/blockscout/pull/1744) - remove `0x0..0` from tests - [#1744](https://github.com/poanetwork/blockscout/pull/1744) - remove `0x0..0` from tests
- [#1763](https://github.com/poanetwork/blockscout/pull/1763) - Describe indexer structure and list existing fetchers - [#1763](https://github.com/poanetwork/blockscout/pull/1763) - Describe indexer structure and list existing fetchers
- [#1800](https://github.com/poanetwork/blockscout/pull/1800) - Disable lazy logging check in Credo
## 1.3.9-beta ## 1.3.9-beta

@ -158,6 +158,14 @@
background-color: $tertiary; background-color: $tertiary;
color: $white; color: $white;
} }
&.header {
font-weight: bold;
&.division {
border-top: 1px solid rgb(183, 185, 184);
}
}
} }
.add-border { .add-border {

@ -120,7 +120,12 @@
<%= subnetwork_title() %> <%= subnetwork_title() %>
</a> </a>
<div class="dropdown-menu" aria-labelledby="navbarDropdown"> <div class="dropdown-menu" aria-labelledby="navbarDropdown">
<%= for %{url: url, title: title} <- other_networks() do %> <a class="dropdown-item header">Mainnet</a>
<%= for %{url: url, title: title} <- main_nets() do %>
<a class="dropdown-item" href="<%= url%>"><%= title %></a>
<% end %>
<a class="dropdown-item header division">Testnet</a>
<%= for %{url: url, title: title} <- test_nets() do %>
<a class="dropdown-item" href="<%= url%>"><%= title %></a> <a class="dropdown-item" href="<%= url%>"><%= title %></a>
<% end %> <% end %>
</div> </div>

@ -95,6 +95,7 @@ defmodule BlockScoutWeb.LayoutView do
|> Enum.reject(fn %{title: title} -> |> Enum.reject(fn %{title: title} ->
title == subnetwork_title() title == subnetwork_title()
end) end)
|> Enum.sort()
end end
def main_nets do def main_nets do

@ -21,6 +21,7 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
token = %Token{ token = %Token{
available_supply: Decimal.new("1000000.0"), available_supply: Decimal.new("1000000.0"),
total_supply: Decimal.new("1000000.0"),
btc_value: Decimal.new("1.000"), btc_value: Decimal.new("1.000"),
id: "test", id: "test",
last_updated: DateTime.utc_now(), last_updated: DateTime.utc_now(),

@ -122,6 +122,7 @@ defmodule BlockScoutWeb.API.RPC.StatsControllerTest do
eth = %Token{ eth = %Token{
available_supply: Decimal.new("1000000.0"), available_supply: Decimal.new("1000000.0"),
total_supply: Decimal.new("1000000.0"),
btc_value: Decimal.new("1.000"), btc_value: Decimal.new("1.000"),
id: "test", id: "test",
last_updated: DateTime.utc_now(), last_updated: DateTime.utc_now(),

@ -134,7 +134,9 @@ defmodule BlockScoutWeb.AddressViewTest do
end end
test "balance_percentage/1" do test "balance_percentage/1" do
Application.put_env(:explorer, :supply, Explorer.Chain.Supply.ProofOfAuthority)
address = insert(:address, fetched_coin_balance: 2_524_608_000_000_000_000_000_000) address = insert(:address, fetched_coin_balance: 2_524_608_000_000_000_000_000_000)
assert "1.0000% Market Cap" = AddressView.balance_percentage(address) assert "1.0000% Market Cap" = AddressView.balance_percentage(address)
end end

@ -52,6 +52,11 @@ defmodule EthereumJSONRPC do
""" """
@type block_number :: non_neg_integer() @type block_number :: non_neg_integer()
@typedoc """
Reference to an uncle block by nephew block's `hash` and `index` in it.
"""
@type nephew_index :: %{required(:nephew_hash) => String.t(), required(:index) => non_neg_integer()}
@typedoc """ @typedoc """
Binary data encoded as a single hexadecimal number in a `String.t` Binary data encoded as a single hexadecimal number in a `String.t`
""" """
@ -235,6 +240,15 @@ defmodule EthereumJSONRPC do
|> fetch_blocks_by_params(&Block.ByNumber.request/1, json_rpc_named_arguments) |> fetch_blocks_by_params(&Block.ByNumber.request/1, json_rpc_named_arguments)
end end
@doc """
Fetches uncle blocks by nephew hashes and indices.
"""
@spec fetch_uncle_blocks([nephew_index()], json_rpc_named_arguments) :: {:ok, Blocks.t()} | {:error, reason :: term}
def fetch_uncle_blocks(blocks, json_rpc_named_arguments) do
blocks
|> fetch_blocks_by_params(&Block.ByNephew.request/1, json_rpc_named_arguments)
end
@doc """ @doc """
Fetches block number by `t:tag/0`. Fetches block number by `t:tag/0`.

@ -356,14 +356,17 @@ defmodule EthereumJSONRPC.Block do
[ [
%{ %{
"hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311", "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
"nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47" "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
"index" => 0
} }
] ]
""" """
@spec elixir_to_uncles(elixir) :: Uncles.elixir() @spec elixir_to_uncles(elixir) :: Uncles.elixir()
def elixir_to_uncles(%{"hash" => nephew_hash, "uncles" => uncles}) do def elixir_to_uncles(%{"hash" => nephew_hash, "uncles" => uncles}) do
Enum.map(uncles, &%{"hash" => &1, "nephewHash" => nephew_hash}) uncles
|> Enum.with_index()
|> Enum.map(fn {uncle_hash, index} -> %{"hash" => uncle_hash, "nephewHash" => nephew_hash, "index" => index} end)
end end
@doc """ @doc """

@ -0,0 +1,15 @@
defmodule EthereumJSONRPC.Block.ByNephew do
@moduledoc """
Block format as returned by [`eth_getUncleByBlockHashAndIndex`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getUncleByBlockHashAndIndex)
"""
import EthereumJSONRPC, only: [integer_to_quantity: 1]
def request(%{id: id, nephew_hash: nephew_hash, index: index}) do
EthereumJSONRPC.request(%{
id: id,
method: "eth_getUncleByBlockHashAndIndex",
params: [nephew_hash, integer_to_quantity(index)]
})
end
end

@ -260,7 +260,8 @@ defmodule EthereumJSONRPC.Blocks do
[ [
%{ %{
"hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311", "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
"nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47" "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
"index" => 0
} }
] ]

@ -6,15 +6,16 @@ defmodule EthereumJSONRPC.Uncle do
chain. chain.
""" """
@type elixir :: %{String.t() => EthereumJSONRPC.hash()} @type elixir :: %{String.t() => EthereumJSONRPC.hash() | non_neg_integer()}
@typedoc """ @typedoc """
* `"hash"` - the hash of the uncle block. * `"hash"` - the hash of the uncle block.
* `"nephewHash"` - the hash of the nephew block that included `"hash` as an uncle. * `"nephewHash"` - the hash of the nephew block that included `"hash` as an uncle.
* `"index"` - the index of the uncle block within the nephew block.
""" """
@type t :: %{String.t() => EthereumJSONRPC.hash()} @type t :: %{String.t() => EthereumJSONRPC.hash()}
@type params :: %{nephew_hash: EthereumJSONRPC.hash(), uncle_hash: EthereumJSONRPC.hash()} @type params :: %{nephew_hash: EthereumJSONRPC.hash(), uncle_hash: EthereumJSONRPC.hash(), index: non_neg_integer()}
@doc """ @doc """
Converts each entry in `t:elixir/0` to `t:params/0` used in `Explorer.Chain.Uncle.changeset/2`. Converts each entry in `t:elixir/0` to `t:params/0` used in `Explorer.Chain.Uncle.changeset/2`.
@ -22,18 +23,20 @@ defmodule EthereumJSONRPC.Uncle do
iex> EthereumJSONRPC.Uncle.elixir_to_params( iex> EthereumJSONRPC.Uncle.elixir_to_params(
...> %{ ...> %{
...> "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311", ...> "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
...> "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47" ...> "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "index" => 0
...> } ...> }
...> ) ...> )
%{ %{
nephew_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47", nephew_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
uncle_hash: "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311" uncle_hash: "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
index: 0
} }
""" """
@spec elixir_to_params(elixir) :: params @spec elixir_to_params(elixir) :: params
def elixir_to_params(%{"hash" => uncle_hash, "nephewHash" => nephew_hash}) def elixir_to_params(%{"hash" => uncle_hash, "nephewHash" => nephew_hash, "index" => index})
when is_binary(uncle_hash) and is_binary(nephew_hash) do when is_binary(uncle_hash) and is_binary(nephew_hash) and is_integer(index) do
%{nephew_hash: nephew_hash, uncle_hash: uncle_hash} %{nephew_hash: nephew_hash, uncle_hash: uncle_hash, index: index}
end end
end end

@ -16,14 +16,16 @@ defmodule EthereumJSONRPC.Uncles do
...> [ ...> [
...> %{ ...> %{
...> "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311", ...> "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
...> "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47" ...> "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "index" => 0
...> } ...> }
...> ] ...> ]
...> ) ...> )
[ [
%{ %{
uncle_hash: "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311", uncle_hash: "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
nephew_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47" nephew_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
index: 0
} }
] ]

@ -1479,24 +1479,23 @@ defmodule Explorer.Chain do
end end
@doc """ @doc """
Returns a stream of all `t:Explorer.Chain.Block.t/0` `hash`es that are marked as unfetched in Returns a stream of all blocks that are marked as unfetched in `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`.
`t:Explorer.Chain.Block.SecondDegreeRelation.t/0`. For each uncle block a `hash` of nephew block and an `index` of the block in it are returned.
When a block is fetched, its uncles are transformed into `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` and can be When a block is fetched, its uncles are transformed into `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` and can be
returned. Once the uncle is imported its corresponding `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` returned. Once the uncle is imported its corresponding `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`
`uncle_fetched_at` will be set and it won't be returned anymore. `uncle_fetched_at` will be set and it won't be returned anymore.
""" """
@spec stream_unfetched_uncle_hashes( @spec stream_unfetched_uncles(
initial :: accumulator, initial :: accumulator,
reducer :: (entry :: Hash.Full.t(), accumulator -> accumulator) reducer :: (entry :: term(), accumulator -> accumulator)
) :: {:ok, accumulator} ) :: {:ok, accumulator}
when accumulator: term() when accumulator: term()
def stream_unfetched_uncle_hashes(initial, reducer) when is_function(reducer, 2) do def stream_unfetched_uncles(initial, reducer) when is_function(reducer, 2) do
query = query =
from(bsdr in Block.SecondDegreeRelation, from(bsdr in Block.SecondDegreeRelation,
where: is_nil(bsdr.uncle_fetched_at), where: is_nil(bsdr.uncle_fetched_at) and not is_nil(bsdr.index),
select: bsdr.uncle_hash, select: [:nephew_hash, :index]
group_by: bsdr.uncle_hash
) )
Repo.stream_reduce(query, initial, reducer) Repo.stream_reduce(query, initial, reducer)
@ -2374,7 +2373,7 @@ defmodule Explorer.Chain do
end end
defp supply_module do defp supply_module do
Application.get_env(:explorer, :supply, Explorer.Chain.Supply.ProofOfAuthority) Application.get_env(:explorer, :supply, Explorer.Chain.Supply.CoinMarketCap)
end end
@doc """ @doc """

@ -17,7 +17,7 @@ defmodule Explorer.Chain.Block.SecondDegreeRelation do
alias Explorer.Chain.{Block, Hash} alias Explorer.Chain.{Block, Hash}
@optional_fields ~w(uncle_fetched_at)a @optional_fields ~w(uncle_fetched_at)a
@required_fields ~w(nephew_hash uncle_hash)a @required_fields ~w(nephew_hash uncle_hash index)a
@allowed_fields @optional_fields ++ @required_fields @allowed_fields @optional_fields ++ @required_fields
@typedoc """ @typedoc """
@ -27,6 +27,7 @@ defmodule Explorer.Chain.Block.SecondDegreeRelation do
`uncle_hash` was fetched for some other reason already. `uncle_hash` was fetched for some other reason already.
* `uncle_fetched_at` - when `t:Explorer.Chain.Block.t/0` for `uncle_hash` was confirmed as fetched. * `uncle_fetched_at` - when `t:Explorer.Chain.Block.t/0` for `uncle_hash` was confirmed as fetched.
* `uncle_hash` - foreign key for `uncle`. * `uncle_hash` - foreign key for `uncle`.
* `index` - index of the uncle within its nephew. Can be `nil` for blocks fetched before this field was added.
""" """
@type t :: @type t ::
%__MODULE__{ %__MODULE__{
@ -34,19 +35,22 @@ defmodule Explorer.Chain.Block.SecondDegreeRelation do
nephew_hash: Hash.Full.t(), nephew_hash: Hash.Full.t(),
uncle: %Ecto.Association.NotLoaded{} | Block.t() | nil, uncle: %Ecto.Association.NotLoaded{} | Block.t() | nil,
uncle_fetched_at: nil, uncle_fetched_at: nil,
uncle_hash: Hash.Full.t() uncle_hash: Hash.Full.t(),
index: non_neg_integer() | nil
} }
| %__MODULE__{ | %__MODULE__{
nephew: %Ecto.Association.NotLoaded{} | Block.t(), nephew: %Ecto.Association.NotLoaded{} | Block.t(),
nephew_hash: Hash.Full.t(), nephew_hash: Hash.Full.t(),
uncle: %Ecto.Association.NotLoaded{} | Block.t(), uncle: %Ecto.Association.NotLoaded{} | Block.t(),
uncle_fetched_at: DateTime.t(), uncle_fetched_at: DateTime.t(),
uncle_hash: Hash.Full.t() uncle_hash: Hash.Full.t(),
index: non_neg_integer() | nil
} }
@primary_key false @primary_key false
schema "block_second_degree_relations" do schema "block_second_degree_relations" do
field(:uncle_fetched_at, :utc_datetime_usec) field(:uncle_fetched_at, :utc_datetime_usec)
field(:index, :integer)
belongs_to(:nephew, Block, foreign_key: :nephew_hash, primary_key: true, references: :hash, type: Hash.Full) belongs_to(:nephew, Block, foreign_key: :nephew_hash, primary_key: true, references: :hash, type: Hash.Full)
belongs_to(:uncle, Block, foreign_key: :uncle_hash, primary_key: true, references: :hash, type: Hash.Full) belongs_to(:uncle, Block, foreign_key: :uncle_hash, primary_key: true, references: :hash, type: Hash.Full)

@ -15,7 +15,11 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations do
@timeout 60_000 @timeout 60_000
@type imported :: [ @type imported :: [
%{required(:nephew_hash) => Hash.Full.t(), required(:uncle_hash) => Hash.Full.t()} %{
required(:nephew_hash) => Hash.Full.t(),
required(:uncle_hash) => Hash.Full.t(),
required(:index) => non_neg_integer()
}
] ]
@impl Import.Runner @impl Import.Runner
@ -27,8 +31,9 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations do
@impl Import.Runner @impl Import.Runner
def imported_table_row do def imported_table_row do
%{ %{
value_type: "[%{uncle_hash: Explorer.Chain.Hash.t(), nephew_hash: Explorer.Chain.Hash.t()]", value_type:
value_description: "List of maps of the `t:#{ecto_schema_module()}.t/0` `uncle_hash` and `nephew_hash`" "[%{uncle_hash: Explorer.Chain.Hash.t(), nephew_hash: Explorer.Chain.Hash.t(), index: non_neg_integer()]",
value_description: "List of maps of the `t:#{ecto_schema_module()}.t/0` `uncle_hash`, `nephew_hash` and `index`"
} }
end end
@ -51,7 +56,9 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations do
@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
}) :: {:ok, %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t()}} | {:error, [Changeset.t()]} }) ::
{:ok, %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t(), index: non_neg_integer()}}
| {:error, [Changeset.t()]}
defp insert(repo, changes_list, %{timeout: timeout} = options) when is_atom(repo) and is_list(changes_list) do defp insert(repo, changes_list, %{timeout: timeout} = options) when is_atom(repo) and is_list(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)
@ -62,7 +69,7 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations do
conflict_target: [:nephew_hash, :uncle_hash], conflict_target: [:nephew_hash, :uncle_hash],
on_conflict: on_conflict, on_conflict: on_conflict,
for: Block.SecondDegreeRelation, for: Block.SecondDegreeRelation,
returning: [:nephew_hash, :uncle_hash], returning: [:nephew_hash, :uncle_hash, :index],
timeout: timeout, timeout: timeout,
# block_second_degree_relations doesn't have timestamps # block_second_degree_relations doesn't have timestamps
timestamps: %{} timestamps: %{}
@ -75,14 +82,16 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations do
update: [ update: [
set: [ set: [
uncle_fetched_at: uncle_fetched_at:
fragment("LEAST(?, EXCLUDED.uncle_fetched_at)", block_second_degree_relation.uncle_fetched_at) fragment("LEAST(?, EXCLUDED.uncle_fetched_at)", block_second_degree_relation.uncle_fetched_at),
index: fragment("EXCLUDED.index")
] ]
], ],
where: where:
fragment( fragment(
"LEAST(?, EXCLUDED.uncle_fetched_at) IS DISTINCT FROM ?", "(LEAST(?, EXCLUDED.uncle_fetched_at), EXCLUDED.index) IS DISTINCT FROM (?, ?)",
block_second_degree_relation.uncle_fetched_at,
block_second_degree_relation.uncle_fetched_at, block_second_degree_relation.uncle_fetched_at,
block_second_degree_relation.uncle_fetched_at block_second_degree_relation.index
) )
) )
end end

@ -0,0 +1,22 @@
defmodule Explorer.Chain.Supply.CoinMarketCap do
@moduledoc """
Defines the supply API for calculating supply for coins from coinmarketcap.
"""
use Explorer.Chain.Supply
alias Explorer.ExchangeRates.Token
alias Explorer.Market
def circulating do
exchange_rate().available_supply
end
def total do
exchange_rate().total_supply
end
def exchange_rate do
Market.get_exchange_rate(Explorer.coin()) || Token.null()
end
end

@ -26,6 +26,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
%Token{ %Token{
available_supply: to_decimal(item["total_supply"]), available_supply: to_decimal(item["total_supply"]),
total_supply: to_decimal(item["total_supply"]),
btc_value: btc_value, btc_value: btc_value,
id: id, id: id,
last_updated: last_updated, last_updated: last_updated,

@ -17,6 +17,7 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCap do
%Token{ %Token{
available_supply: to_decimal(item["available_supply"]), available_supply: to_decimal(item["available_supply"]),
total_supply: to_decimal(item["total_supply"]),
btc_value: to_decimal(item["price_btc"]), btc_value: to_decimal(item["price_btc"]),
id: item["id"], id: item["id"],
last_updated: last_updated, last_updated: last_updated,

@ -26,6 +26,7 @@ defmodule Explorer.ExchangeRates.Source.TransactionAndLog do
defp build_struct(original_token) do defp build_struct(original_token) do
%Token{ %Token{
available_supply: to_decimal(Chain.circulating_supply()), available_supply: to_decimal(Chain.circulating_supply()),
total_supply: 0,
btc_value: original_token.btc_value, btc_value: original_token.btc_value,
id: original_token.id, id: original_token.id,
last_updated: original_token.last_updated, last_updated: original_token.last_updated,

@ -7,6 +7,7 @@ defmodule Explorer.ExchangeRates.Token do
Represents an exchange rate for a given token. Represents an exchange rate for a given token.
* `:available_supply` - Available supply of a token * `:available_supply` - Available supply of a token
* `:total_supply` - Max Supply
* `:btc_value` - The Bitcoin value of the currency * `:btc_value` - The Bitcoin value of the currency
* `:id` - ID of a currency * `:id` - ID of a currency
* `:last_updated` - Timestamp of when the value was last updated * `:last_updated` - Timestamp of when the value was last updated
@ -18,6 +19,7 @@ defmodule Explorer.ExchangeRates.Token do
""" """
@type t :: %__MODULE__{ @type t :: %__MODULE__{
available_supply: Decimal.t(), available_supply: Decimal.t(),
total_supply: Decimal.t(),
btc_value: Decimal.t(), btc_value: Decimal.t(),
id: String.t(), id: String.t(),
last_updated: DateTime.t(), last_updated: DateTime.t(),
@ -28,8 +30,8 @@ defmodule Explorer.ExchangeRates.Token do
volume_24h_usd: Decimal.t() volume_24h_usd: Decimal.t()
} }
@enforce_keys ~w(available_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a @enforce_keys ~w(available_supply total_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a
defstruct ~w(available_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a defstruct ~w(available_supply total_supply btc_value id last_updated market_cap_usd name symbol usd_value volume_24h_usd)a
def null, def null,
do: %__MODULE__{ do: %__MODULE__{
@ -37,6 +39,7 @@ defmodule Explorer.ExchangeRates.Token do
id: nil, id: nil,
name: nil, name: nil,
available_supply: nil, available_supply: nil,
total_supply: nil,
usd_value: nil, usd_value: nil,
volume_24h_usd: nil, volume_24h_usd: nil,
market_cap_usd: nil, market_cap_usd: nil,
@ -51,6 +54,7 @@ defmodule Explorer.ExchangeRates.Token do
id: id, id: id,
name: name, name: name,
available_supply: available_supply, available_supply: available_supply,
total_supply: total_supply,
usd_value: usd_value, usd_value: usd_value,
volume_24h_usd: volume_24h_usd, volume_24h_usd: volume_24h_usd,
market_cap_usd: market_cap_usd, market_cap_usd: market_cap_usd,
@ -58,17 +62,20 @@ defmodule Explorer.ExchangeRates.Token do
last_updated: last_updated last_updated: last_updated
}) do }) do
# symbol is first because it is the key used for lookup in `Explorer.ExchangeRates`'s ETS table # symbol is first because it is the key used for lookup in `Explorer.ExchangeRates`'s ETS table
{symbol, id, name, available_supply, usd_value, volume_24h_usd, market_cap_usd, btc_value, last_updated} {symbol, id, name, available_supply, total_supply, usd_value, volume_24h_usd, market_cap_usd, btc_value,
last_updated}
end end
def from_tuple( def from_tuple(
{symbol, id, name, available_supply, usd_value, volume_24h_usd, market_cap_usd, btc_value, last_updated} {symbol, id, name, available_supply, total_supply, usd_value, volume_24h_usd, market_cap_usd, btc_value,
last_updated}
) do ) do
%__MODULE__{ %__MODULE__{
symbol: symbol, symbol: symbol,
id: id, id: id,
name: name, name: name,
available_supply: available_supply, available_supply: available_supply,
total_supply: total_supply,
usd_value: usd_value, usd_value: usd_value,
volume_24h_usd: volume_24h_usd, volume_24h_usd: volume_24h_usd,
market_cap_usd: market_cap_usd, market_cap_usd: market_cap_usd,

@ -0,0 +1,10 @@
defmodule Explorer.Repo.Migrations.AddIndexToBsdr do
use Ecto.Migration
def change do
alter table(:block_second_degree_relations) do
# Null for old relations without fetched index
add(:index, :integer, null: true)
end
end
end

@ -5,16 +5,21 @@ defmodule Explorer.Chain.Block.SecondDegreeRelationTest do
alias Explorer.Chain.Block alias Explorer.Chain.Block
describe "changeset/2" do describe "changeset/2" do
test "requires hash and nephew_hash" do test "requires hash, nephew_hash and index" do
assert %Changeset{valid?: false} = assert %Changeset{valid?: false} =
changeset = Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{}) changeset = Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{})
assert changeset_errors(changeset) == %{nephew_hash: ["can't be blank"], uncle_hash: ["can't be blank"]} assert changeset_errors(changeset) == %{
nephew_hash: ["can't be blank"],
uncle_hash: ["can't be blank"],
index: ["can't be blank"]
}
assert %Changeset{valid?: true} = assert %Changeset{valid?: true} =
Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{ Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{
nephew_hash: block_hash(), nephew_hash: block_hash(),
uncle_hash: block_hash() uncle_hash: block_hash(),
index: 0
}) })
end end
@ -23,6 +28,7 @@ defmodule Explorer.Chain.Block.SecondDegreeRelationTest do
Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{ Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{
nephew_hash: block_hash(), nephew_hash: block_hash(),
uncle_hash: block_hash(), uncle_hash: block_hash(),
index: 0,
uncle_fetched_at: DateTime.utc_now() uncle_fetched_at: DateTime.utc_now()
}) })
end end
@ -30,7 +36,7 @@ defmodule Explorer.Chain.Block.SecondDegreeRelationTest do
test "enforces foreign key constraint on nephew_hash" do test "enforces foreign key constraint on nephew_hash" do
assert {:error, %Changeset{valid?: false} = changeset} = assert {:error, %Changeset{valid?: false} = changeset} =
%Block.SecondDegreeRelation{} %Block.SecondDegreeRelation{}
|> Block.SecondDegreeRelation.changeset(%{nephew_hash: block_hash(), uncle_hash: block_hash()}) |> Block.SecondDegreeRelation.changeset(%{nephew_hash: block_hash(), uncle_hash: block_hash(), index: 0})
|> Repo.insert() |> Repo.insert()
assert changeset_errors(changeset) == %{nephew_hash: ["does not exist"]} assert changeset_errors(changeset) == %{nephew_hash: ["does not exist"]}
@ -41,7 +47,7 @@ defmodule Explorer.Chain.Block.SecondDegreeRelationTest do
assert {:error, %Changeset{valid?: false} = changeset} = assert {:error, %Changeset{valid?: false} = changeset} =
%Block.SecondDegreeRelation{} %Block.SecondDegreeRelation{}
|> Block.SecondDegreeRelation.changeset(%{nephew_hash: nephew_hash, uncle_hash: hash}) |> Block.SecondDegreeRelation.changeset(%{nephew_hash: nephew_hash, uncle_hash: hash, index: 0})
|> Repo.insert() |> Repo.insert()
assert changeset_errors(changeset) == %{uncle_hash: ["has already been taken"]} assert changeset_errors(changeset) == %{uncle_hash: ["has already been taken"]}

@ -1547,7 +1547,7 @@ defmodule Explorer.Chain.ImportTest do
timeout: 1 timeout: 1
}, },
block_second_degree_relations: %{ block_second_degree_relations: %{
params: [%{nephew_hash: block_hash, uncle_hash: uncle_hash}], params: [%{nephew_hash: block_hash, uncle_hash: uncle_hash, index: 0}],
timeout: 1 timeout: 1
}, },
internal_transactions: %{ internal_transactions: %{

@ -1044,7 +1044,8 @@ defmodule Explorer.ChainTest do
params: [ params: [
%{ %{
nephew_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd", nephew_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
uncle_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471be" uncle_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471be",
index: 0
} }
] ]
}, },
@ -3258,21 +3259,24 @@ defmodule Explorer.ChainTest do
end end
end end
describe "stream_unfetched_uncle_hashes/2" do describe "stream_unfetched_uncles/2" do
test "does not return uncle hashes where t:Explorer.Chain.Block.SecondDegreeRelation.t/0 uncle_fetched_at is not nil" do test "does not return uncle hashes where t:Explorer.Chain.Block.SecondDegreeRelation.t/0 uncle_fetched_at is not nil" do
%Block.SecondDegreeRelation{nephew: %Block{}, uncle_hash: uncle_hash} = insert(:block_second_degree_relation) %Block.SecondDegreeRelation{nephew: %Block{}, nephew_hash: nephew_hash, index: index, uncle_hash: uncle_hash} =
insert(:block_second_degree_relation)
assert {:ok, [^uncle_hash]} = Explorer.Chain.stream_unfetched_uncle_hashes([], &[&1 | &2]) assert {:ok, [%{nephew_hash: ^nephew_hash, index: ^index}]} =
Explorer.Chain.stream_unfetched_uncles([], &[&1 | &2])
query = from(bsdr in Block.SecondDegreeRelation, where: bsdr.uncle_hash == ^uncle_hash) query = from(bsdr in Block.SecondDegreeRelation, where: bsdr.uncle_hash == ^uncle_hash)
assert {1, _} = Repo.update_all(query, set: [uncle_fetched_at: DateTime.utc_now()]) assert {1, _} = Repo.update_all(query, set: [uncle_fetched_at: DateTime.utc_now()])
assert {:ok, []} = Explorer.Chain.stream_unfetched_uncle_hashes([], &[&1 | &2]) assert {:ok, []} = Explorer.Chain.stream_unfetched_uncles([], &[&1 | &2])
end end
end end
test "total_supply/0" do test "total_supply/0" do
Application.put_env(:explorer, :supply, Explorer.Chain.Supply.ProofOfAuthority)
height = 2_000_000 height = 2_000_000
insert(:block, number: height) insert(:block, number: height)
expected = ProofOfAuthority.initial_supply() + height expected = ProofOfAuthority.initial_supply() + height
@ -3281,6 +3285,7 @@ defmodule Explorer.ChainTest do
end end
test "circulating_supply/0" do test "circulating_supply/0" do
Application.put_env(:explorer, :supply, Explorer.Chain.Supply.ProofOfAuthority)
assert Chain.circulating_supply() == ProofOfAuthority.circulating() assert Chain.circulating_supply() == ProofOfAuthority.circulating()
end end

@ -68,6 +68,7 @@ defmodule Explorer.ExchangeRatesTest do
test "with successful fetch" do test "with successful fetch" do
expected_token = %Token{ expected_token = %Token{
available_supply: Decimal.new("1000000.0"), available_supply: Decimal.new("1000000.0"),
total_supply: Decimal.new("1000000.0"),
btc_value: Decimal.new("1.000"), btc_value: Decimal.new("1.000"),
id: "test_id", id: "test_id",
last_updated: DateTime.utc_now(), last_updated: DateTime.utc_now(),

@ -64,6 +64,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGeckoTest do
expected = [ expected = [
%Token{ %Token{
available_supply: Decimal.new("252193195"), available_supply: Decimal.new("252193195"),
total_supply: Decimal.new("252193195"),
btc_value: Decimal.new("0.00001753101509231471092879666458"), btc_value: Decimal.new("0.00001753101509231471092879666458"),
id: "poa-network", id: "poa-network",
last_updated: expected_date, last_updated: expected_date,

@ -33,6 +33,7 @@ defmodule Explorer.ExchangeRates.Source.CoinMarketCapTest do
expected = [ expected = [
%Token{ %Token{
available_supply: Decimal.new("203981804.0"), available_supply: Decimal.new("203981804.0"),
total_supply: Decimal.new("254473964.0"),
btc_value: Decimal.new("0.00007032"), btc_value: Decimal.new("0.00007032"),
id: "poa-network", id: "poa-network",
last_updated: expected_date, last_updated: expected_date,

@ -181,7 +181,8 @@ defmodule Explorer.Factory do
def block_second_degree_relation_factory do def block_second_degree_relation_factory do
%Block.SecondDegreeRelation{ %Block.SecondDegreeRelation{
uncle_hash: block_hash(), uncle_hash: block_hash(),
nephew: build(:block) nephew: build(:block),
index: 0
} }
end end

@ -10,6 +10,7 @@ defmodule Explorer.ExchangeRates.Source.OneCoinSource do
def format_data(_) do def format_data(_) do
pseudo_token = %Token{ pseudo_token = %Token{
available_supply: Decimal.new(10_000_000), available_supply: Decimal.new(10_000_000),
total_supply: Decimal.new(10_000_000_000),
btc_value: Decimal.new(1), btc_value: Decimal.new(1),
id: "", id: "",
last_updated: Timex.now(), last_updated: Timex.now(),

@ -93,6 +93,7 @@ After all deployed instances get all needed data, these fetchers should be depre
- `uncataloged_token_transfers`: extracts token transfers from logs, which previously weren't parsed due to unknown format - `uncataloged_token_transfers`: extracts token transfers from logs, which previously weren't parsed due to unknown format
- `addresses_without_codes`: forces complete refetch of blocks, which have created contract addresses without contract code - `addresses_without_codes`: forces complete refetch of blocks, which have created contract addresses without contract code
- `failed_created_addresses`: forces refetch of contract code for failed transactions, which previously got incorrectly overwritten - `failed_created_addresses`: forces refetch of contract code for failed transactions, which previously got incorrectly overwritten
- `uncles_without_index`: adds previously unfetched `index` field for unfetched blocks in `block_second_degree_relations`
## Memory Usage ## Memory Usage

@ -286,9 +286,7 @@ defmodule Indexer.Block.Fetcher do
end end
def async_import_uncles(%{block_second_degree_relations: block_second_degree_relations}) do def async_import_uncles(%{block_second_degree_relations: block_second_degree_relations}) do
block_second_degree_relations UncleBlock.async_fetch_blocks(block_second_degree_relations)
|> Enum.map(& &1.uncle_hash)
|> UncleBlock.async_fetch_blocks()
end end
def async_import_uncles(_), do: :ok def async_import_uncles(_), do: :ok

@ -29,17 +29,13 @@ defmodule Indexer.Fetcher.UncleBlock do
] ]
@doc """ @doc """
Asynchronously fetches `t:Explorer.Chain.Block.t/0` for the given `hashes` and updates Asynchronously fetches `t:Explorer.Chain.Block.t/0` for the given `nephew_hash` and `index`
`t:Explorer.Chain.Block.SecondDegreeRelation.t/0` `block_fetched_at`. and updates `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` `block_fetched_at`.
""" """
@spec async_fetch_blocks([Hash.Full.t()]) :: :ok @spec async_fetch_blocks([%{required(:nephew_hash) => Hash.Full.t(), required(:index) => non_neg_integer()}]) :: :ok
def async_fetch_blocks(block_hashes) when is_list(block_hashes) do def async_fetch_blocks(relations) when is_list(relations) do
BufferedTask.buffer( entries = Enum.map(relations, &entry/1)
__MODULE__, BufferedTask.buffer(__MODULE__, entries)
block_hashes
|> Enum.map(&to_string/1)
|> Enum.uniq()
)
end end
@doc false @doc false
@ -63,9 +59,9 @@ defmodule Indexer.Fetcher.UncleBlock do
@impl BufferedTask @impl BufferedTask
def init(initial, reducer, _) do def init(initial, reducer, _) do
{:ok, final} = {:ok, final} =
Chain.stream_unfetched_uncle_hashes(initial, fn uncle_hash, acc -> Chain.stream_unfetched_uncles(initial, fn uncle, acc ->
uncle_hash uncle
|> to_string() |> entry()
|> reducer.(acc) |> reducer.(acc)
end) end)
@ -74,29 +70,38 @@ defmodule Indexer.Fetcher.UncleBlock do
@impl BufferedTask @impl BufferedTask
@decorate trace(name: "fetch", resource: "Indexer.Fetcher.UncleBlock.run/2", service: :indexer, tracer: Tracer) @decorate trace(name: "fetch", resource: "Indexer.Fetcher.UncleBlock.run/2", service: :indexer, tracer: Tracer)
def run(hashes, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} = block_fetcher) do def run(entries, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} = block_fetcher) do
# the same block could be included as an uncle on multiple blocks, but we only want to fetch it once entry_count = Enum.count(entries)
unique_hashes = Enum.uniq(hashes) Logger.metadata(count: entry_count)
unique_hash_count = Enum.count(unique_hashes)
Logger.metadata(count: unique_hash_count)
Logger.debug("fetching") Logger.debug("fetching")
case EthereumJSONRPC.fetch_blocks_by_hash(unique_hashes, json_rpc_named_arguments) do entries
|> Enum.map(&entry_to_params/1)
|> EthereumJSONRPC.fetch_uncle_blocks(json_rpc_named_arguments)
|> case do
{:ok, blocks} -> {:ok, blocks} ->
run_blocks(blocks, block_fetcher, unique_hashes) run_blocks(blocks, block_fetcher, entries)
{:error, reason} -> {:error, reason} ->
Logger.error( Logger.error(
fn -> fn ->
["failed to fetch: ", inspect(reason)] ["failed to fetch: ", inspect(reason)]
end, end,
error_count: unique_hash_count error_count: entry_count
) )
{:retry, unique_hashes} {:retry, entries}
end
end
defp entry_to_params({nephew_hash_bytes, index}) when is_integer(index) do
{:ok, nephew_hash} = Hash.Full.cast(nephew_hash_bytes)
%{nephew_hash: to_string(nephew_hash), index: index}
end end
defp entry(%{nephew_hash: %Hash{bytes: nephew_hash_bytes}, index: index}) do
{nephew_hash_bytes, index}
end end
defp run_blocks(%Blocks{blocks_params: []}, _, original_entries), do: {:retry, original_entries} defp run_blocks(%Blocks{blocks_params: []}, _, original_entries), do: {:retry, original_entries}
@ -158,9 +163,7 @@ defmodule Indexer.Fetcher.UncleBlock do
# * Token.async_fetch is not called because the tokens only matter on consensus blocks # * Token.async_fetch is not called because the tokens only matter on consensus blocks
# * TokenBalance.async_fetch is not called because it uses block numbers from consensus, not uncles # * TokenBalance.async_fetch is not called because it uses block numbers from consensus, not uncles
block_second_degree_relations UncleBlock.async_fetch_blocks(block_second_degree_relations)
|> Enum.map(& &1.uncle_hash)
|> UncleBlock.async_fetch_blocks()
ok ok
end end

@ -26,7 +26,8 @@ defmodule Indexer.Supervisor do
alias Indexer.Temporary.{ alias Indexer.Temporary.{
AddressesWithoutCode, AddressesWithoutCode,
FailedCreatedAddresses, FailedCreatedAddresses,
UncatalogedTokenTransfers UncatalogedTokenTransfers,
UnclesWithoutIndex
} }
def child_spec([]) do def child_spec([]) do
@ -131,7 +132,9 @@ defmodule Indexer.Supervisor do
# Temporary workers # Temporary workers
{AddressesWithoutCode.Supervisor, [fixing_realtime_fetcher]}, {AddressesWithoutCode.Supervisor, [fixing_realtime_fetcher]},
{FailedCreatedAddresses.Supervisor, [json_rpc_named_arguments]}, {FailedCreatedAddresses.Supervisor, [json_rpc_named_arguments]},
{UncatalogedTokenTransfers.Supervisor, [[]]} {UncatalogedTokenTransfers.Supervisor, [[]]},
{UnclesWithoutIndex.Supervisor,
[[json_rpc_named_arguments: json_rpc_named_arguments, memory_monitor: memory_monitor]]}
], ],
strategy: :one_for_one strategy: :one_for_one
) )

@ -0,0 +1,164 @@
defmodule Indexer.Temporary.UnclesWithoutIndex do
@moduledoc """
Fetches `index`es for unfetched `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`.
As we don't explicitly store uncle block lists for nephew blocks, we need to refetch
them completely.
"""
use Indexer.Fetcher
use Spandex.Decorators
require Logger
import Ecto.Query
alias EthereumJSONRPC.Blocks
alias Explorer.{Chain, Repo}
alias Explorer.Chain.Block.SecondDegreeRelation
alias Indexer.{BufferedTask, Tracer}
alias Indexer.Fetcher.UncleBlock
@behaviour BufferedTask
@defaults [
flush_interval: :timer.seconds(3),
max_batch_size: 100,
max_concurrency: 10,
task_supervisor: Indexer.Temporary.UnclesWithoutIndex.TaskSupervisor,
metadata: [fetcher: :uncles_without_index]
]
@doc false
def child_spec([init_options, gen_server_options]) when is_list(init_options) do
{state, mergeable_init_options} = Keyword.pop(init_options, :json_rpc_named_arguments)
unless state do
raise ArgumentError,
":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <>
"to allow for json_rpc calls when running."
end
merged_init_options =
@defaults
|> Keyword.merge(mergeable_init_options)
|> Keyword.put(:state, state)
Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__)
end
@impl BufferedTask
def init(initial, reducer, _) do
query =
from(bsdr in SecondDegreeRelation,
join: b in assoc(bsdr, :nephew),
where: is_nil(bsdr.index) and is_nil(bsdr.uncle_fetched_at) and b.consensus,
select: bsdr.nephew_hash,
group_by: bsdr.nephew_hash
)
{:ok, final} =
Repo.stream_reduce(query, initial, fn nephew_hash, acc ->
nephew_hash
|> to_string()
|> reducer.(acc)
end)
final
end
@impl BufferedTask
@decorate trace(name: "fetch", resource: "Indexer.Fetcher.UncleBlock.run/2", service: :indexer, tracer: Tracer)
def run(hashes, json_rpc_named_arguments) do
hash_count = Enum.count(hashes)
Logger.metadata(count: hash_count)
Logger.debug("fetching")
case EthereumJSONRPC.fetch_blocks_by_hash(hashes, json_rpc_named_arguments) do
{:ok, blocks} ->
run_blocks(blocks, hashes)
{:error, reason} ->
Logger.error(
fn ->
["failed to fetch: ", inspect(reason)]
end,
error_count: hash_count
)
{:retry, hashes}
end
end
defp run_blocks(%Blocks{blocks_params: []}, original_entries), do: {:retry, original_entries}
defp run_blocks(
%Blocks{block_second_degree_relations_params: block_second_degree_relations_params, errors: errors},
original_entries
) do
case Chain.import(%{block_second_degree_relations: %{params: block_second_degree_relations_params}}) do
{:ok, %{block_second_degree_relations: block_second_degree_relations}} ->
UncleBlock.async_fetch_blocks(block_second_degree_relations)
retry(errors)
{:error, step, failed_value, _changes_so_far} ->
Logger.error(fn -> ["failed to import: ", inspect(failed_value)] end,
step: step,
error_count: Enum.count(original_entries)
)
{:retry, original_entries}
end
end
defp retry([]), do: :ok
defp retry(errors) when is_list(errors) do
retried_entries = errors_to_entries(errors)
loggable_errors = loggable_errors(errors)
loggable_error_count = Enum.count(loggable_errors)
unless loggable_error_count == 0 do
Logger.error(
fn ->
[
"failed to fetch: ",
errors_to_iodata(loggable_errors)
]
end,
error_count: loggable_error_count
)
end
{:retry, retried_entries}
end
defp loggable_errors(errors) when is_list(errors) do
Enum.filter(errors, fn
%{code: 404, message: "Not Found"} -> false
_ -> true
end)
end
defp errors_to_entries(errors) when is_list(errors) do
Enum.map(errors, &error_to_entry/1)
end
defp error_to_entry(%{data: %{hash: hash}}) when is_binary(hash), do: hash
defp errors_to_iodata(errors) when is_list(errors) do
errors_to_iodata(errors, [])
end
defp errors_to_iodata([], iodata), do: iodata
defp errors_to_iodata([error | errors], iodata) do
errors_to_iodata(errors, [iodata | error_to_iodata(error)])
end
defp error_to_iodata(%{code: code, message: message, data: %{hash: hash}})
when is_integer(code) and is_binary(message) and is_binary(hash) do
[hash, ": (", to_string(code), ") ", message, ?\n]
end
end

@ -7,6 +7,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.Block.Reward alias Explorer.Chain.Block.Reward
alias Explorer.Chain.Hash
alias Indexer.Block alias Indexer.Block
alias Indexer.Block.Catchup.Fetcher alias Indexer.Block.Catchup.Fetcher
alias Indexer.Fetcher.{BlockReward, CoinBalance, InternalTransaction, Token, TokenBalance, UncleBlock} alias Indexer.Fetcher.{BlockReward, CoinBalance, InternalTransaction, Token, TokenBalance, UncleBlock}
@ -51,7 +52,10 @@ defmodule Indexer.Block.Catchup.FetcherTest do
Process.register(pid, UncleBlock) Process.register(pid, UncleBlock)
nephew_hash = block_hash() |> to_string() nephew_hash_data = block_hash()
%Hash{bytes: nephew_hash_bytes} = nephew_hash_data
nephew_hash = nephew_hash_data |> to_string()
nephew_index = 0
uncle_hash = block_hash() |> to_string() uncle_hash = block_hash() |> to_string()
miner_hash = address_hash() |> to_string() miner_hash = address_hash() |> to_string()
block_number = 0 block_number = 0
@ -96,7 +100,8 @@ defmodule Indexer.Block.Catchup.FetcherTest do
params: [ params: [
%{ %{
nephew_hash: nephew_hash, nephew_hash: nephew_hash,
uncle_hash: uncle_hash uncle_hash: uncle_hash,
index: nephew_index
} }
] ]
}, },
@ -113,7 +118,7 @@ defmodule Indexer.Block.Catchup.FetcherTest do
} }
}) })
assert_receive {:uncles, [^uncle_hash]} assert_receive {:uncles, [{^nephew_hash_bytes, ^nephew_index}]}
end end
end end

@ -4,6 +4,8 @@ defmodule Indexer.Fetcher.UncleBlockTest do
use EthereumJSONRPC.Case, async: false use EthereumJSONRPC.Case, async: false
use Explorer.DataCase use Explorer.DataCase
import EthereumJSONRPC, only: [integer_to_quantity: 1]
alias Explorer.Chain alias Explorer.Chain
alias Indexer.Block alias Indexer.Block
alias Indexer.Fetcher.UncleBlock alias Indexer.Fetcher.UncleBlock
@ -43,16 +45,30 @@ defmodule Indexer.Fetcher.UncleBlockTest do
describe "init/1" do describe "init/1" do
test "fetched unfetched uncle hashes", %{json_rpc_named_arguments: json_rpc_named_arguments} do test "fetched unfetched uncle hashes", %{json_rpc_named_arguments: json_rpc_named_arguments} do
assert %Chain.Block.SecondDegreeRelation{nephew_hash: nephew_hash, uncle_hash: uncle_hash, uncle: nil} = assert %Chain.Block.SecondDegreeRelation{
nephew_hash: nephew_hash,
uncle_hash: uncle_hash,
index: index,
uncle: nil
} =
:block_second_degree_relation :block_second_degree_relation
|> insert() |> insert()
|> Repo.preload([:nephew, :uncle]) |> Repo.preload([:nephew, :uncle])
nephew_hash_data = to_string(nephew_hash)
uncle_hash_data = to_string(uncle_hash) uncle_hash_data = to_string(uncle_hash)
uncle_uncle_hash_data = to_string(block_hash()) uncle_uncle_hash_data = to_string(block_hash())
index_data = integer_to_quantity(index)
EthereumJSONRPC.Mox EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [%{id: id, method: "eth_getBlockByHash", params: [^uncle_hash_data, true]}], _ -> |> expect(:json_rpc, fn [
%{
id: id,
method: "eth_getUncleByBlockHashAndIndex",
params: [^nephew_hash_data, ^index_data]
}
],
_ ->
number_quantity = "0x0" number_quantity = "0x0"
{:ok, {:ok,

Loading…
Cancel
Save