Merge branch 'master' into mobile-menu-fix

pull/2737/head
Victor Baranov 5 years ago committed by GitHub
commit 934251ad7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 15
      .circleci/config.yml
  2. 25
      CHANGELOG.md
  3. 4
      apps/block_scout_web/assets/css/components/_dashboard-banner.scss
  4. 6
      apps/block_scout_web/assets/js/lib/try_api.js
  5. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex
  6. 2
      apps/block_scout_web/lib/block_scout_web/controllers/chain_controller.ex
  7. 8
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/holder_controller.ex
  8. 7
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/inventory_controller.ex
  9. 8
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/read_contract_controller.ex
  10. 34
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/token_controller.ex
  11. 7
      apps/block_scout_web/lib/block_scout_web/controllers/tokens/transfer_controller.ex
  12. 14
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  13. 3
      apps/block_scout_web/lib/block_scout_web/templates/block/overview.html.eex
  14. 2
      apps/block_scout_web/lib/block_scout_web/templates/smart_contract/_functions.html.eex
  15. 8
      apps/block_scout_web/lib/block_scout_web/templates/tokens/overview/_details.html.eex
  16. 8
      apps/block_scout_web/lib/block_scout_web/templates/transaction_log/_logs.html.eex
  17. 2
      apps/block_scout_web/mix.exs
  18. 47
      apps/block_scout_web/priv/gettext/default.pot
  19. 47
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  20. 16
      apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs
  21. 4
      apps/block_scout_web/test/block_scout_web/controllers/address_coin_balance_by_day_controller_test.exs
  22. 10
      apps/block_scout_web/test/block_scout_web/controllers/address_controller_test.exs
  23. 4
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/address_controller_test.exs
  24. 4
      apps/block_scout_web/test/block_scout_web/controllers/api/rpc/eth_controller_test.exs
  25. 8
      apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
  26. 6
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  27. 6
      apps/block_scout_web/test/block_scout_web/features/viewing_app_test.exs
  28. 34
      apps/block_scout_web/test/block_scout_web/features/viewing_chain_test.exs
  29. 2
      apps/block_scout_web/test/support/conn_case.ex
  30. 2
      apps/block_scout_web/test/support/feature_case.ex
  31. 4
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/receipts.ex
  32. 13
      apps/explorer/config/config.exs
  33. 2
      apps/explorer/config/test.exs
  34. 5
      apps/explorer/lib/explorer/application.ex
  35. 149
      apps/explorer/lib/explorer/chain.ex
  36. 10
      apps/explorer/lib/explorer/chain/address.ex
  37. 2
      apps/explorer/lib/explorer/chain/address/coin_balance.ex
  38. 73
      apps/explorer/lib/explorer/chain/cache/accounts.ex
  39. 5
      apps/explorer/lib/explorer/chain/contract_method.ex
  40. 4
      apps/explorer/lib/explorer/chain/import.ex
  41. 2
      apps/explorer/lib/explorer/chain/import/runner/address/coin_balances.ex
  42. 18
      apps/explorer/lib/explorer/chain/import/runner/address/current_token_balances.ex
  43. 2
      apps/explorer/lib/explorer/chain/import/runner/address/token_balances.ex
  44. 21
      apps/explorer/lib/explorer/chain/import/runner/addresses.ex
  45. 2
      apps/explorer/lib/explorer/chain/import/runner/block/rewards.ex
  46. 2
      apps/explorer/lib/explorer/chain/import/runner/block/second_degree_relations.ex
  47. 584
      apps/explorer/lib/explorer/chain/import/runner/blocks.ex
  48. 47
      apps/explorer/lib/explorer/chain/import/runner/internal_transactions.ex
  49. 30
      apps/explorer/lib/explorer/chain/import/runner/internal_transactions_indexed_at_blocks.ex
  50. 2
      apps/explorer/lib/explorer/chain/import/runner/logs.ex
  51. 37
      apps/explorer/lib/explorer/chain/import/runner/staking_pools.ex
  52. 5
      apps/explorer/lib/explorer/chain/import/runner/staking_pools_delegators.ex
  53. 2
      apps/explorer/lib/explorer/chain/import/runner/token_transfers.ex
  54. 125
      apps/explorer/lib/explorer/chain/import/runner/tokens.ex
  55. 4
      apps/explorer/lib/explorer/chain/import/runner/transaction/forks.ex
  56. 70
      apps/explorer/lib/explorer/chain/import/runner/transactions.ex
  57. 20
      apps/explorer/lib/explorer/chain/import/stage.ex
  58. 28
      apps/explorer/lib/explorer/chain/import/stage/address_referencing.ex
  59. 30
      apps/explorer/lib/explorer/chain/import/stage/block_following.ex
  60. 30
      apps/explorer/lib/explorer/chain/import/stage/block_referencing.ex
  61. 25
      apps/explorer/lib/explorer/chain_spec/parity/importer.ex
  62. 25
      apps/explorer/lib/explorer/chain_spec/poa/importer.ex
  63. 125
      apps/explorer/lib/explorer/counters/addresses_counter.ex
  64. 34
      apps/explorer/lib/explorer/exchange_rates/source/coin_gecko.ex
  65. 5
      apps/explorer/lib/explorer/market/market.ex
  66. 15
      apps/explorer/lib/explorer/smart_contract/verifier.ex
  67. 5
      apps/explorer/lib/explorer/validator/metadata_importer.ex
  68. 7
      apps/explorer/priv/repo/migrations/20190910170703_create_indexes_for_block_number_in_token_transfers_and_transactions.exs
  69. 42
      apps/explorer/test/explorer/chain/cache/accounts_test.exs
  70. 69
      apps/explorer/test/explorer/chain/import/runner/addresses_test.exs
  71. 104
      apps/explorer/test/explorer/chain/import/runner/blocks_test.exs
  72. 37
      apps/explorer/test/explorer/chain_test.exs
  73. 16
      apps/explorer/test/explorer/counters/addresses_counter_test.exs
  74. 101
      apps/explorer/test/explorer/exchange_rates/source/coin_gecko_test.exs
  75. 28
      apps/explorer/test/explorer/smart_contract/verifier_test.exs
  76. 2
      apps/explorer/test/support/data_case.ex
  77. 7
      apps/indexer/lib/indexer/block/fetcher.ex
  78. 3
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  79. 5
      apps/indexer/lib/indexer/fetcher/block_reward.ex
  80. 5
      apps/indexer/lib/indexer/fetcher/coin_balance.ex
  81. 8
      apps/indexer/lib/indexer/fetcher/coin_balance_on_demand.ex
  82. 4
      apps/indexer/lib/indexer/fetcher/contract_code.ex
  83. 3
      apps/indexer/lib/indexer/fetcher/internal_transaction.ex
  84. 4
      apps/indexer/lib/indexer/fetcher/pending_transaction.ex
  85. 4
      apps/indexer/lib/indexer/fetcher/uncle_block.ex
  86. 31
      apps/indexer/lib/indexer/temporary/blocks_transactions_mismatch.ex
  87. 14
      apps/indexer/test/indexer/block/fetcher/receipts_test.exs
  88. 3
      docs/env-variables.md
  89. 137
      docs/sharelocks.md
  90. 2
      mix.lock

@ -33,12 +33,15 @@ jobs:
- restore_cache:
keys:
- v7-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }}
- v7-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }}
- v7-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}
- v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }}
- v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }}
- v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}
- run: mix deps.get
- run:
command: sed -i '68,68 s/^/%/' ./deps/hackney/src/hackney_ssl.erl
- restore_cache:
keys:
- v7-npm-install-{{ .Branch }}-{{ checksum "apps/block_scout_web/assets/package-lock.json" }}
@ -83,17 +86,17 @@ jobs:
# `deps` needs to be cached with `_build` because `_build` will symlink into `deps`
- save_cache:
key: v7-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }}
key: v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.lock" }}
paths:
- deps
- _build
- save_cache:
key: v7-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }}
key: v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}-{{ checksum "mix.exs" }}
paths:
- deps
- _build
- save_cache:
key: v7-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}
key: v8-mix-compile-{{ checksum "OTP_VERSION.lock" }}-{{ checksum "ELIXIR_VERSION.lock" }}
paths:
- deps
- _build

@ -1,17 +1,39 @@
## Current
### Features
- [#2665](https://github.com/poanetwork/blockscout/pull/2665) - new menu layout for mobile devices
- [#2717](https://github.com/poanetwork/blockscout/pull/2717) - Improve speed of nonconsensus data removal
- [#2679](https://github.com/poanetwork/blockscout/pull/2679) - added fixed height for card chain blocks and card chain transactions
- [#2678](https://github.com/poanetwork/blockscout/pull/2678) - fixed dashboard banner height bug
- [#2672](https://github.com/poanetwork/blockscout/pull/2672) - added new theme for xUSDT
- [#2667](https://github.com/poanetwork/blockscout/pull/2667) - Add ETS-based cache for accounts page
- [#2666](https://github.com/poanetwork/blockscout/pull/2666) - fetch token counters in parallel
- [#2665](https://github.com/poanetwork/blockscout/pull/2665) - new menu layout for mobile devices
- [#2663](https://github.com/poanetwork/blockscout/pull/2663) - Fetch address counters in parallel
### Fixes
- [#2737](https://github.com/poanetwork/blockscout/pull/2737) - switched hardcoded subnetwork value to elixir expression for mobile menu
- [#2736](https://github.com/poanetwork/blockscout/pull/2736) - do not update cache if no blocks were inserted
- [#2731](https://github.com/poanetwork/blockscout/pull/2731) - fix library verification
- [#2718](https://github.com/poanetwork/blockscout/pull/2718) - Include all addresses taking part in transactions in wallets' addresses counter
- [#2709](https://github.com/poanetwork/blockscout/pull/2709) - Fix stuck label and value for uncle block height
- [#2707](https://github.com/poanetwork/blockscout/pull/2707) - fix for dashboard banner chart legend items
- [#2706](https://github.com/poanetwork/blockscout/pull/2706) - fix empty total_supply in coin gecko response
- [#2701](https://github.com/poanetwork/blockscout/pull/2701) - Exclude nonconsensus blocks from avg block time calculation by default
- [#2696](https://github.com/poanetwork/blockscout/pull/2696) - do not update fetched_coin_balance with nil
- [#2693](https://github.com/poanetwork/blockscout/pull/2693) - remove non consensus internal transactions
- [#2691](https://github.com/poanetwork/blockscout/pull/2691) - fix exchange rate websocket update for Rootstock
- [#2688](https://github.com/poanetwork/blockscout/pull/2688) - fix try it out section
- [#2687](https://github.com/poanetwork/blockscout/pull/2687) - remove non-consensus token transfers, logs when inserting new consensus blocks
- [#2684](https://github.com/poanetwork/blockscout/pull/2684) - do not filter pending logs
- [#2682](https://github.com/poanetwork/blockscout/pull/2682) - Use Task.start instead of Task.async in caches
- [#2671](https://github.com/poanetwork/blockscout/pull/2671) - fixed buttons color at smart contract section
- [#2660](https://github.com/poanetwork/blockscout/pull/2660) - set correct last value for coin balances chart data
- [#2619](https://github.com/poanetwork/blockscout/pull/2619) - Enforce DB transaction's order to prevent deadlocks
### Chore
- [#2724](https://github.com/poanetwork/blockscout/pull/2724) - fix ci by commenting a line in hackney library
- [#2708](https://github.com/poanetwork/blockscout/pull/2708) - add log index to logs view
- [#2723](https://github.com/poanetwork/blockscout/pull/2723) - get rid of ex_json_schema warnings
## 2.0.4-beta
@ -49,6 +71,7 @@
- [#2468](https://github.com/poanetwork/blockscout/pull/2468) - fix confirmations for non consensus blocks
### Chore
- [#2662](https://github.com/poanetwork/blockscout/pull/2662) - fetch coin gecko id based on the coin symbol
- [#2646](https://github.com/poanetwork/blockscout/pull/2646) - Added Xerom to list of Additional Chains using BlockScout
- [#2634](https://github.com/poanetwork/blockscout/pull/2634) - add Lukso to networks dropdown
- [#2617](https://github.com/poanetwork/blockscout/pull/2617) - skip cache update if there are no blocks inserted

@ -72,7 +72,7 @@ $dashboard-banner-chart-axis-font-color: $dashboard-stats-item-value-color !defa
}
.dashboard-banner-chart-legend {
display: flex;
display: grid;
grid-template-columns: 1fr 1fr;
padding-bottom: 12px;
@ -81,7 +81,7 @@ $dashboard-banner-chart-axis-font-color: $dashboard-stats-item-value-color !defa
padding-left: 12px;
padding-top: 3px;
position: relative;
padding-right: 60px;
padding-right: 12px;
@include media-breakpoint-down(md) {
display: flex;

@ -55,10 +55,6 @@ function handleSuccess (query, xhr, clickedButton) {
clickedButton.prop('disabled', false)
}
function dropDomain (url) {
return new URL(url).pathname
}
// Show 'Try it out' UI for a module/action.
$('button[data-selector*="btn-try-api"]').click(event => {
const clickedButton = $(event.target)
@ -128,7 +124,7 @@ $('button[data-try-api-ui-button-type="execute"]').click(event => {
}
$.ajax({
url: dropDomain(composeRequestUrl(query)),
url: composeRequestUrl(query),
success: (_data, _status, xhr) => {
handleSuccess(query, xhr, clickedButton)
},

@ -61,7 +61,7 @@ defmodule BlockScoutWeb.AddressController do
def index(conn, _params) do
render(conn, "index.html",
current_path: current_path(conn),
address_count: Chain.count_addresses_with_balance_from_cache()
address_count: Chain.count_addresses_from_cache()
)
end

@ -28,7 +28,7 @@ defmodule BlockScoutWeb.ChainController do
render(
conn,
"show.html",
address_count: Chain.count_addresses_with_balance_from_cache(),
address_count: Chain.count_addresses_from_cache(),
average_block_time: AverageBlockTime.average_block_time(),
exchange_rate: exchange_rate,
chart_data_path: market_history_chart_path(conn, :show),

@ -12,6 +12,8 @@ defmodule BlockScoutWeb.Tokens.HolderController do
next_page_params: 3
]
import BlockScoutWeb.Tokens.TokenController, only: [fetch_token_counters: 2]
def index(conn, %{"token_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash),
@ -47,13 +49,15 @@ defmodule BlockScoutWeb.Tokens.HolderController do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash, options) do
{total_token_transfers, total_token_holders} = fetch_token_counters(token, address_hash)
render(
conn,
"index.html",
current_path: current_path(conn),
token: Market.add_price(token),
total_token_holders: token.holder_count || Chain.count_token_holders_from_token_hash(address_hash),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash)
total_token_holders: total_token_holders,
total_token_transfers: total_token_transfers
)
else
:error ->

@ -7,6 +7,7 @@ defmodule BlockScoutWeb.Tokens.InventoryController do
alias Phoenix.View
import BlockScoutWeb.Chain, only: [split_list_by_page: 1, default_paging_options: 0]
import BlockScoutWeb.Tokens.TokenController, only: [fetch_token_counters: 2]
def index(conn, %{"token_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
@ -64,13 +65,15 @@ defmodule BlockScoutWeb.Tokens.InventoryController do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash, options) do
{total_token_transfers, total_token_holders} = fetch_token_counters(token, address_hash)
render(
conn,
"index.html",
current_path: current_path(conn),
token: Market.add_price(token),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
total_token_holders: token.holder_count || Chain.count_token_holders_from_token_hash(address_hash)
total_token_transfers: total_token_transfers,
total_token_holders: total_token_holders
)
else
:error ->

@ -3,17 +3,21 @@ defmodule BlockScoutWeb.Tokens.ReadContractController do
alias Explorer.{Chain, Market}
import BlockScoutWeb.Tokens.TokenController, only: [fetch_token_counters: 2]
def index(conn, %{"token_id" => address_hash_string}) do
options = [necessity_by_association: %{[contract_address: :smart_contract] => :optional}]
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash, options) do
{total_token_transfers, total_token_holders} = fetch_token_counters(token, address_hash)
render(
conn,
"index.html",
token: Market.add_price(token),
total_token_transfers: token.holder_count || Chain.count_token_transfers_from_token_hash(address_hash),
total_token_holders: Chain.count_token_holders_from_token_hash(address_hash)
total_token_transfers: total_token_transfers,
total_token_holders: total_token_holders
)
else
:error ->

@ -1,7 +1,41 @@
defmodule BlockScoutWeb.Tokens.TokenController do
use BlockScoutWeb, :controller
require Logger
alias Explorer.Chain
def show(conn, %{"id" => address_hash_string}) do
redirect(conn, to: token_transfer_path(conn, :index, address_hash_string))
end
def fetch_token_counters(token, address_hash) do
total_token_transfers_task =
Task.async(fn ->
Chain.count_token_transfers_from_token_hash(address_hash)
end)
total_token_holders_task =
Task.async(fn ->
token.holder_count || Chain.count_token_holders_from_token_hash(address_hash)
end)
[total_token_transfers_task, total_token_holders_task]
|> Task.yield_many(:timer.seconds(40))
|> Enum.map(fn {_task, res} ->
case res do
{:ok, result} ->
result
{:exit, reason} ->
Logger.warn("Query fetching token counters terminated: #{inspect(reason)}")
0
nil ->
Logger.warn("Query fetching token counters timed out.")
0
end
end)
|> List.to_tuple()
end
end

@ -6,6 +6,7 @@ defmodule BlockScoutWeb.Tokens.TransferController do
alias Phoenix.View
import BlockScoutWeb.Chain, only: [split_list_by_page: 1, paging_options: 1, next_page_params: 3]
import BlockScoutWeb.Tokens.TokenController, only: [fetch_token_counters: 2]
def index(conn, %{"token_id" => address_hash_string, "type" => "JSON"} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
@ -48,13 +49,15 @@ defmodule BlockScoutWeb.Tokens.TransferController do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, token} <- Chain.token_from_address_hash(address_hash, options) do
{total_token_transfers, total_token_holders} = fetch_token_counters(token, address_hash)
render(
conn,
"index.html",
current_path: current_path(conn),
token: Market.add_price(token),
total_token_transfers: Chain.count_token_transfers_from_token_hash(address_hash),
total_token_holders: token.holder_count || Chain.count_token_holders_from_token_hash(address_hash)
total_token_transfers: total_token_transfers,
total_token_holders: total_token_holders
)
else
:error ->

@ -7,13 +7,14 @@ defmodule BlockScoutWeb.Notifier do
alias BlockScoutWeb.{AddressContractVerificationView, Endpoint}
alias Explorer.{Chain, Market, Repo}
alias Explorer.Chain.{Address, InternalTransaction, Transaction}
alias Explorer.Chain.Supply.RSK
alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token
alias Explorer.SmartContract.{Solidity.CodeCompiler, Solidity.CompilerVersion}
alias Phoenix.View
def handle_event({:chain_event, :addresses, type, addresses}) when type in [:realtime, :on_demand] do
Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.count_addresses_with_balance_from_cache()})
Endpoint.broadcast("addresses:new_address", "count", %{count: Chain.count_addresses_from_cache()})
addresses
|> Stream.reject(fn %Address{fetched_coin_balance: fetched_coin_balance} -> is_nil(fetched_coin_balance) end)
@ -76,8 +77,17 @@ defmodule BlockScoutWeb.Notifier do
data -> data
end
exchange_rate_with_available_supply =
case Application.get_env(:explorer, :supply) do
RSK ->
%{exchange_rate | available_supply: nil, market_cap_usd: RSK.market_cap(exchange_rate)}
_ ->
exchange_rate
end
Endpoint.broadcast("exchange_rate:new_rate", "new_rate", %{
exchange_rate: exchange_rate,
exchange_rate: exchange_rate_with_available_supply,
market_history_data: Enum.map(market_history_data, fn day -> Map.take(day, [:closing_price, :date]) end)
})
end

@ -12,8 +12,7 @@
<%= if block_type(@block) == "Block" do %>
<%= gettext("Block Height: %{height}", height: @block.number) %> <%= if @block.number == 0, do: "- " <> gettext("Genesis Block")%>
<% else %>
<%= gettext("%{block_type} Height:", block_type: block_type(@block)) %>
<%= link(@block, to: block_path(BlockScoutWeb.Endpoint, :show, @block.number)) %>
<%= gettext("%{block_type} Height:", block_type: block_type(@block)) %> <%= link(@block, to: block_path(BlockScoutWeb.Endpoint, :show, @block.number)) %>
<% end %>
</h3>
<div class="d-flex justify-content-start text-muted block-details-row">

@ -19,7 +19,7 @@
</div>
<% end %>
<input type="submit" value='<%= gettext("Query")%>' class="button button-secondary button-xs py-0 mt-2" />
<input type="submit" value='<%= gettext("Query")%>' class="button btn-line button-xs py-0 mt-2" style="padding: 6px 8px!important;height: 26px;font-size: 11px;" />
</form>
<div class='p-2 text-muted <%= if (queryable?(function["inputs"]) == true), do: "w-100" %>'>

@ -57,8 +57,12 @@
</span>
<div class="d-flex flex-row justify-content-start text-muted">
<span class="mr-4"> <%= @token.type %> </span>
<span class="mr-4"><%= @total_token_holders %> <%= gettext "Addresses" %></span>
<span class="mr-4"><%= @total_token_transfers %> <%= gettext "Transfers" %></span>
<%= if @total_token_holders > 0 do %>
<span class="mr-4"><%= @total_token_holders %> <%= gettext "Addresses" %></span>
<% end %>
<%= if @total_token_transfers > 0 do %>
<span class="mr-4"><%= @total_token_transfers %> <%= gettext "Transfers" %></span>
<% end %>
<%= if decimals?(@token) do %>
<span class="mr-4"><%= @token.decimals %> <%= gettext "Decimals" %></span>
<% end %>

@ -185,5 +185,13 @@
</div>
<% end %>
</dd>
<dt class="col-md-2">
<%= gettext "Log Index" %>
</dt>
<dd class="col-md-10">
<div class="text-dark raw-transaction-log-index">
<%= @log.index %>
</div>
</dd>
</dl>
</div>

@ -131,7 +131,7 @@ defmodule BlockScoutWeb.Mixfile do
# `:cowboy` `~> 2.0` and Phoenix 1.4 compatibility
{:wobserver, "~> 0.2.0", github: "poanetwork/wobserver", branch: "support-https"},
{:phoenix_form_awesomplete, "~> 0.1.4"},
{:ex_json_schema, "~> 0.6.1"}
{:ex_json_schema, "~> 0.6.2"}
]
end

@ -43,7 +43,7 @@ msgid "%{block_type}s"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:21
#: lib/block_scout_web/templates/block/overview.html.eex:20
#: lib/block_scout_web/templates/chain/_block.html.eex:11
msgid "%{count} Transactions"
msgstr ""
@ -135,7 +135,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/index.html.eex:4
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:60
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:61
msgid "Addresses"
msgstr ""
@ -304,8 +304,8 @@ msgstr ""
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:145
#: lib/block_scout_web/templates/address/overview.html.eex:153
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:106
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:114
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:110
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
msgid "Close"
msgstr ""
@ -480,7 +480,7 @@ msgid "Copy Txn Hash"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:59
#: lib/block_scout_web/templates/block/overview.html.eex:58
msgid "Difficulty"
msgstr ""
@ -526,7 +526,7 @@ msgid "Data"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:63
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
msgid "Decimals"
msgstr ""
@ -562,15 +562,15 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:56
#: lib/block_scout_web/templates/block/overview.html.eex:109
#: lib/block_scout_web/templates/block/overview.html.eex:159
#: lib/block_scout_web/templates/block/overview.html.eex:108
#: lib/block_scout_web/templates/block/overview.html.eex:158
msgid "Gas Limit"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:61
#: lib/block_scout_web/templates/block/overview.html.eex:102
#: lib/block_scout_web/templates/block/overview.html.eex:153
#: lib/block_scout_web/templates/block/overview.html.eex:101
#: lib/block_scout_web/templates/block/overview.html.eex:152
msgid "Gas Used"
msgstr ""
@ -657,7 +657,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:38
#: lib/block_scout_web/templates/block/overview.html.eex:125
#: lib/block_scout_web/templates/block/overview.html.eex:124
#: lib/block_scout_web/templates/chain/_block.html.eex:15
msgid "Miner"
msgstr ""
@ -712,7 +712,7 @@ msgid "Execute"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:74
#: lib/block_scout_web/templates/block/overview.html.eex:73
#: lib/block_scout_web/templates/transaction/overview.html.eex:79
msgid "Nonce"
msgstr ""
@ -767,7 +767,7 @@ msgid "GET"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:85
#: lib/block_scout_web/templates/block/overview.html.eex:84
msgid "Position %{index}"
msgstr ""
@ -798,7 +798,7 @@ msgid "Gwei"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:37
#: lib/block_scout_web/templates/block/overview.html.eex:36
msgid "Hash"
msgstr ""
@ -964,7 +964,7 @@ msgid "Top Accounts - %{subnetwork} Explorer"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:68
#: lib/block_scout_web/templates/block/overview.html.eex:67
msgid "Total Difficulty"
msgstr ""
@ -1246,7 +1246,7 @@ msgid "Parameters"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:44
#: lib/block_scout_web/templates/block/overview.html.eex:43
msgid "Parent Hash"
msgstr ""
@ -1277,7 +1277,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/overview.html.eex:33
#: lib/block_scout_web/templates/address/overview.html.eex:144
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:36
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:105
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:109
msgid "QR Code"
msgstr ""
@ -1404,7 +1404,7 @@ msgid "Show Validator Info"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:146
#: lib/block_scout_web/templates/block/overview.html.eex:145
msgid "Block Rewards"
msgstr ""
@ -1554,7 +1554,7 @@ msgid "Topics"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:75
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:79
msgid "Total Supply"
msgstr ""
@ -1611,7 +1611,7 @@ msgid "Transactions sent"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:61
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:64
msgid "Transfers"
msgstr ""
@ -1646,7 +1646,7 @@ msgid "Uncle Reward"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:81
#: lib/block_scout_web/templates/block/overview.html.eex:80
#: lib/block_scout_web/templates/layout/_topnav.html.eex:52
msgid "Uncles"
msgstr ""
@ -1841,3 +1841,8 @@ msgstr ""
#: lib/block_scout_web/views/address_contract_view.ex:22
msgid "true"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:189
msgid "Log Index"
msgstr ""

@ -43,7 +43,7 @@ msgid "%{block_type}s"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:21
#: lib/block_scout_web/templates/block/overview.html.eex:20
#: lib/block_scout_web/templates/chain/_block.html.eex:11
msgid "%{count} Transactions"
msgstr ""
@ -135,7 +135,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/index.html.eex:4
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:60
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:61
msgid "Addresses"
msgstr ""
@ -304,8 +304,8 @@ msgstr ""
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:145
#: lib/block_scout_web/templates/address/overview.html.eex:153
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:106
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:114
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:110
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:118
msgid "Close"
msgstr ""
@ -480,7 +480,7 @@ msgid "Copy Txn Hash"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:59
#: lib/block_scout_web/templates/block/overview.html.eex:58
msgid "Difficulty"
msgstr ""
@ -526,7 +526,7 @@ msgid "Data"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:63
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:67
msgid "Decimals"
msgstr ""
@ -562,15 +562,15 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:56
#: lib/block_scout_web/templates/block/overview.html.eex:109
#: lib/block_scout_web/templates/block/overview.html.eex:159
#: lib/block_scout_web/templates/block/overview.html.eex:108
#: lib/block_scout_web/templates/block/overview.html.eex:158
msgid "Gas Limit"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:61
#: lib/block_scout_web/templates/block/overview.html.eex:102
#: lib/block_scout_web/templates/block/overview.html.eex:153
#: lib/block_scout_web/templates/block/overview.html.eex:101
#: lib/block_scout_web/templates/block/overview.html.eex:152
msgid "Gas Used"
msgstr ""
@ -657,7 +657,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/_tile.html.eex:38
#: lib/block_scout_web/templates/block/overview.html.eex:125
#: lib/block_scout_web/templates/block/overview.html.eex:124
#: lib/block_scout_web/templates/chain/_block.html.eex:15
msgid "Miner"
msgstr ""
@ -712,7 +712,7 @@ msgid "Execute"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:74
#: lib/block_scout_web/templates/block/overview.html.eex:73
#: lib/block_scout_web/templates/transaction/overview.html.eex:79
msgid "Nonce"
msgstr ""
@ -767,7 +767,7 @@ msgid "GET"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:85
#: lib/block_scout_web/templates/block/overview.html.eex:84
msgid "Position %{index}"
msgstr ""
@ -798,7 +798,7 @@ msgid "Gwei"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:37
#: lib/block_scout_web/templates/block/overview.html.eex:36
msgid "Hash"
msgstr ""
@ -964,7 +964,7 @@ msgid "Top Accounts - %{subnetwork} Explorer"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:68
#: lib/block_scout_web/templates/block/overview.html.eex:67
msgid "Total Difficulty"
msgstr ""
@ -1246,7 +1246,7 @@ msgid "Parameters"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:44
#: lib/block_scout_web/templates/block/overview.html.eex:43
msgid "Parent Hash"
msgstr ""
@ -1277,7 +1277,7 @@ msgstr ""
#: lib/block_scout_web/templates/address/overview.html.eex:33
#: lib/block_scout_web/templates/address/overview.html.eex:144
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:36
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:105
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:109
msgid "QR Code"
msgstr ""
@ -1404,7 +1404,7 @@ msgid "Show Validator Info"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:146
#: lib/block_scout_web/templates/block/overview.html.eex:145
msgid "Block Rewards"
msgstr ""
@ -1554,7 +1554,7 @@ msgid "Topics"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:75
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:79
msgid "Total Supply"
msgstr ""
@ -1611,7 +1611,7 @@ msgid "Transactions sent"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:61
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:64
msgid "Transfers"
msgstr ""
@ -1646,7 +1646,7 @@ msgid "Uncle Reward"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/block/overview.html.eex:81
#: lib/block_scout_web/templates/block/overview.html.eex:80
#: lib/block_scout_web/templates/layout/_topnav.html.eex:52
msgid "Uncles"
msgstr ""
@ -1841,3 +1841,8 @@ msgstr ""
#: lib/block_scout_web/views/address_contract_view.ex:22
msgid "true"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction_log/_logs.html.eex:189
msgid "Log Index"
msgstr ""

@ -1,11 +1,11 @@
defmodule BlockScoutWeb.AddressChannelTest do
use BlockScoutWeb.ChannelCase,
# ETS tables are shared in `Explorer.Counters.AddressesWithBalanceCounter`
# ETS tables are shared in `Explorer.Counters.AddressesCounter`
async: false
alias BlockScoutWeb.UserSocket
alias BlockScoutWeb.Notifier
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.AddressesCounter
test "subscribed user is notified of new_address count event" do
topic = "addresses:new_address"
@ -13,8 +13,8 @@ defmodule BlockScoutWeb.AddressChannelTest do
address = insert(:address)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
Notifier.handle_event({:chain_event, :addresses, :realtime, [address]})
@ -55,8 +55,8 @@ defmodule BlockScoutWeb.AddressChannelTest do
test "notified of balance_update for matching address", %{address: address, topic: topic} do
address_with_balance = %{address | fetched_coin_balance: 1}
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
Notifier.handle_event({:chain_event, :addresses, :realtime, [address_with_balance]})
@ -67,8 +67,8 @@ defmodule BlockScoutWeb.AddressChannelTest do
end
test "not notified of balance_update if fetched_coin_balance is nil", %{address: address} do
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
Notifier.handle_event({:chain_event, :addresses, :realtime, [address]})

@ -5,8 +5,8 @@ defmodule BlockScoutWeb.AddressCoinBalanceByDayControllerTest do
test "returns the coin balance history grouped by date", %{conn: conn} do
address = insert(:address)
noon = Timex.now() |> Timex.beginning_of_day() |> Timex.set(hour: 12)
block = insert(:block, timestamp: noon)
block_one_day_ago = insert(:block, timestamp: Timex.shift(noon, days: -1))
block = insert(:block, timestamp: noon, number: 2)
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)

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.AddressControllerTest do
# ETS tables are shared in `Explorer.Counters.*`
async: false
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.AddressesCounter
describe "GET index/2" do
test "returns top addresses", %{conn: conn} do
@ -12,8 +12,8 @@ defmodule BlockScoutWeb.AddressControllerTest do
|> Enum.map(&insert(:address, fetched_coin_balance: &1))
|> Enum.map(& &1.hash)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
conn = get(conn, address_path(conn, :index, %{type: "JSON"}))
{:ok, %{"items" => items}} = Poison.decode(conn.resp_body)
@ -25,8 +25,8 @@ defmodule BlockScoutWeb.AddressControllerTest do
address = insert(:address, fetched_coin_balance: 1)
insert(:address_name, address: address, primary: true, name: "POA Wallet")
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
conn = get(conn, address_path(conn, :index, %{type: "JSON"}))

@ -6,7 +6,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
alias BlockScoutWeb.API.RPC.AddressController
alias Explorer.Chain
alias Explorer.Chain.{Events.Subscriber, Transaction, Wei}
alias Explorer.Counters.{AddressesWithBalanceCounter, AverageBlockTime}
alias Explorer.Counters.{AddressesCounter, AverageBlockTime}
alias Indexer.Fetcher.CoinBalanceOnDemand
alias Explorer.Repo
@ -22,7 +22,7 @@ defmodule BlockScoutWeb.API.RPC.AddressControllerTest do
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
start_supervised!(AverageBlockTime)
start_supervised!({CoinBalanceOnDemand, [mocked_json_rpc_named_arguments, [name: CoinBalanceOnDemand]]})
start_supervised!(AddressesWithBalanceCounter)
start_supervised!(AddressesCounter)
Application.put_env(:explorer, AverageBlockTime, enabled: true)

@ -1,7 +1,7 @@
defmodule BlockScoutWeb.API.RPC.EthControllerTest do
use BlockScoutWeb.ConnCase, async: false
alias Explorer.Counters.{AddressesWithBalanceCounter, AverageBlockTime}
alias Explorer.Counters.{AddressesCounter, AverageBlockTime}
alias Explorer.Repo
alias Indexer.Fetcher.CoinBalanceOnDemand
@ -14,7 +14,7 @@ defmodule BlockScoutWeb.API.RPC.EthControllerTest do
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
start_supervised!(AverageBlockTime)
start_supervised!({CoinBalanceOnDemand, [mocked_json_rpc_named_arguments, [name: CoinBalanceOnDemand]]})
start_supervised!(AddressesWithBalanceCounter)
start_supervised!(AddressesCounter)
Application.put_env(:explorer, AverageBlockTime, enabled: true)

@ -1,18 +1,18 @@
defmodule BlockScoutWeb.ChainControllerTest do
use BlockScoutWeb.ConnCase,
# ETS table is shared in `Explorer.Counters.AddressesWithBalanceCounter`
# ETS table is shared in `Explorer.Counters.AddressesCounter`
async: false
import BlockScoutWeb.WebRouter.Helpers, only: [chain_path: 2, block_path: 3, transaction_path: 3, address_path: 3]
alias Explorer.Chain.Block
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.AddressesCounter
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
:ok
end

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
# Because ETS tables is shared for `Explorer.Counters.*`
async: false
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.AddressesCounter
alias BlockScoutWeb.{AddressPage, AddressView, Notifier}
setup do
@ -58,8 +58,8 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
[first_address | _] = addresses
[last_address | _] = Enum.reverse(addresses)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> AddressPage.visit_page()

@ -5,11 +5,11 @@ defmodule BlockScoutWeb.ViewingAppTest do
alias BlockScoutWeb.AppPage
alias BlockScoutWeb.Counters.BlocksIndexedCounter
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.AddressesCounter
setup do
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
:ok
end

@ -7,7 +7,7 @@ defmodule BlockScoutWeb.ViewingChainTest do
alias BlockScoutWeb.{AddressPage, BlockPage, ChainPage, TransactionPage}
alias Explorer.Chain.Block
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.AddressesCounter
setup do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
@ -35,8 +35,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
test "search for address", %{session: session} do
address = insert(:address)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()
@ -49,8 +49,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
test "search for blocks from chain page", %{session: session} do
block = insert(:block, number: 6)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()
@ -59,8 +59,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
end
test "blocks list", %{session: session} do
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()
@ -70,8 +70,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
test "inserts place holder blocks on render for out of order blocks", %{session: session} do
insert(:block, number: 409)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()
@ -84,8 +84,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
test "search for transactions", %{session: session} do
transaction = insert(:transaction)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()
@ -94,8 +94,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
end
test "transactions list", %{session: session} do
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()
@ -111,8 +111,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
|> with_contract_creation(contract_address)
|> with_block(block)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()
@ -138,8 +138,8 @@ defmodule BlockScoutWeb.ViewingChainTest do
token_contract_address: contract_token_address
)
start_supervised!(AddressesWithBalanceCounter)
AddressesWithBalanceCounter.consolidate()
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
session
|> ChainPage.visit_page()

@ -42,6 +42,8 @@ defmodule BlockScoutWeb.ConnCase do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
{:ok, conn: Phoenix.ConnTest.build_conn()}
end

@ -29,6 +29,8 @@ defmodule BlockScoutWeb.FeatureCase do
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
metadata = Phoenix.Ecto.SQL.Sandbox.metadata_for(Explorer.Repo, self())
{:ok, session} = Wallaby.start_session(metadata: metadata)

@ -61,9 +61,7 @@ defmodule EthereumJSONRPC.Receipts do
"""
@spec elixir_to_logs(elixir) :: Logs.elixir()
def elixir_to_logs(elixir) when is_list(elixir) do
elixir
|> Enum.flat_map(&Receipt.elixir_to_logs/1)
|> Enum.filter(&(Map.get(&1, "type") != "pending"))
Enum.flat_map(elixir, &Receipt.elixir_to_logs/1)
end
@doc """

@ -14,7 +14,7 @@ config :explorer,
System.get_env("ALLOWED_EVM_VERSIONS") ||
"homestead,tangerineWhistle,spuriousDragon,byzantium,constantinople,petersburg,default",
include_uncles_in_average_block_time:
if(System.get_env("UNCLES_IN_AVERAGE_BLOCK_TIME") == "false", do: false, else: true),
if(System.get_env("UNCLES_IN_AVERAGE_BLOCK_TIME") == "true", do: true, else: false),
healthy_blocks_period: System.get_env("HEALTHY_BLOCKS_PERIOD") || :timer.minutes(5)
average_block_period =
@ -38,8 +38,6 @@ config :explorer, Explorer.Chain.Cache.BlockNumber,
ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false),
global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5))
config :explorer, Explorer.ExchangeRates.Source.CoinGecko, coin_id: System.get_env("COIN_GECKO_ID", "poa-network")
balances_update_interval =
if System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL") do
case Integer.parse(System.get_env("ADDRESS_WITH_BALANCES_UPDATE_INTERVAL")) do
@ -49,6 +47,11 @@ balances_update_interval =
end
config :explorer, Explorer.Counters.AddressesWithBalanceCounter,
enabled: false,
enable_consolidation: true,
update_interval_in_seconds: balances_update_interval || 30 * 60
config :explorer, Explorer.Counters.AddressesCounter,
enabled: true,
enable_consolidation: true,
update_interval_in_seconds: balances_update_interval || 30 * 60
@ -138,6 +141,10 @@ config :explorer, Explorer.Chain.Cache.Transactions,
ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false),
global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5))
config :explorer, Explorer.Chain.Cache.Accounts,
ttl_check_interval: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(1), else: false),
global_ttl: if(System.get_env("DISABLE_INDEXER") == "true", do: :timer.seconds(5))
# Import environment specific config. This must remain at the bottom
# of this file so it overrides the configuration defined above.
import_config "#{Mix.env()}.exs"

@ -21,6 +21,8 @@ config :explorer, Explorer.Counters.AverageBlockTime, enabled: false
config :explorer, Explorer.Counters.AddressesWithBalanceCounter, enabled: false, enable_consolidation: false
config :explorer, Explorer.Counters.AddressesCounter, enabled: false, enable_consolidation: false
config :explorer, Explorer.Market.History.Cataloger, enabled: false
config :explorer, Explorer.Tracer, disabled?: false

@ -8,6 +8,7 @@ defmodule Explorer.Application do
alias Explorer.Admin
alias Explorer.Chain.Cache.{
Accounts,
BlockCount,
BlockNumber,
Blocks,
@ -49,7 +50,8 @@ defmodule Explorer.Application do
BlockNumber,
con_cache_child_spec(MarketHistoryCache.cache_name()),
con_cache_child_spec(RSK.cache_name(), ttl_check_interval: :timer.minutes(1), global_ttl: :timer.minutes(30)),
Transactions
Transactions,
Accounts
]
children = base_children ++ configurable_children()
@ -66,6 +68,7 @@ defmodule Explorer.Application do
configure(Explorer.KnownTokens),
configure(Explorer.Market.History.Cataloger),
configure(Explorer.Counters.AddressesWithBalanceCounter),
configure(Explorer.Counters.AddressesCounter),
configure(Explorer.Counters.AverageBlockTime),
configure(Explorer.Validator.MetadataProcessor),
configure(Explorer.Staking.EpochCounter)

@ -8,6 +8,7 @@ defmodule Explorer.Chain do
from: 2,
join: 4,
limit: 2,
lock: 2,
order_by: 2,
order_by: 3,
offset: 2,
@ -48,6 +49,7 @@ defmodule Explorer.Chain do
alias Explorer.Chain.Block.{EmissionReward, Reward}
alias Explorer.Chain.Cache.{
Accounts,
BlockCount,
BlockNumber,
Blocks,
@ -56,7 +58,7 @@ defmodule Explorer.Chain do
}
alias Explorer.Chain.Import.Runner
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.{AddressesCounter, AddressesWithBalanceCounter}
alias Explorer.Market.MarketHistoryCache
alias Explorer.{PagingOptions, Repo}
@ -116,6 +118,14 @@ defmodule Explorer.Chain do
AddressesWithBalanceCounter.fetch()
end
@doc """
Gets from the cache the count of all `t:Explorer.Chain.Address.t/0`'s
"""
@spec count_addresses_from_cache :: non_neg_integer()
def count_addresses_from_cache do
AddressesCounter.fetch()
end
@doc """
Counts the number of addresses with fetched coin balance > 0.
@ -129,6 +139,19 @@ defmodule Explorer.Chain do
)
end
@doc """
Counts the number of all addresses.
This function should be used with caution. In larger databases, it may take a
while to have the return back.
"""
def count_addresses do
Repo.one(
Address.count(),
timeout: :infinity
)
end
@doc """
`t:Explorer.Chain.InternalTransaction/0`s from the address with the given `hash`.
@ -594,12 +617,15 @@ defmodule Explorer.Chain do
def create_decompiled_smart_contract(attrs) do
changeset = DecompiledSmartContract.changeset(%DecompiledSmartContract{}, attrs)
# Enforce ShareLocks tables order (see docs: sharelocks.md)
Multi.new()
|> Multi.run(:set_address_decompiled, fn repo, _ ->
set_address_decompiled(repo, Changeset.get_field(changeset, :address_hash))
end)
|> Multi.insert(:decompiled_smart_contract, changeset,
on_conflict: :replace_all,
conflict_target: [:decompiler_version, :address_hash]
)
|> Multi.run(:set_address_decompiled, &set_address_decompiled/2)
|> Repo.transaction()
|> case do
{:ok, %{decompiled_smart_contract: decompiled_smart_contract}} -> {:ok, decompiled_smart_contract}
@ -1375,6 +1401,36 @@ defmodule Explorer.Chain do
def list_top_addresses(options \\ []) do
paging_options = Keyword.get(options, :paging_options, @default_paging_options)
if is_nil(paging_options.key) do
paging_options.page_size
|> Accounts.take_enough()
|> case do
nil ->
accounts_with_n = fetch_top_addresses(paging_options)
accounts_with_n
|> Enum.map(fn {address, _n} -> address end)
|> Accounts.update()
accounts_with_n
accounts ->
Enum.map(
accounts,
&{&1,
if is_nil(&1.nonce) do
0
else
&1.nonce + 1
end}
)
end
else
fetch_top_addresses(paging_options)
end
end
defp fetch_top_addresses(paging_options) do
base_query =
from(a in Address,
where: a.fetched_coin_balance > ^0,
@ -2518,12 +2574,18 @@ defmodule Explorer.Chain do
|> SmartContract.changeset(attrs)
|> Changeset.put_change(:external_libraries, external_libraries)
address_hash = Changeset.get_field(smart_contract_changeset, :address_hash)
# Enforce ShareLocks tables order (see docs: sharelocks.md)
insert_result =
Multi.new()
|> Multi.run(:set_address_verified, fn repo, _ -> set_address_verified(repo, address_hash) end)
|> Multi.run(:clear_primary_address_names, fn repo, _ -> clear_primary_address_names(repo, address_hash) end)
|> Multi.run(:insert_address_name, fn repo, _ ->
name = Changeset.get_field(smart_contract_changeset, :name)
create_address_name(repo, name, address_hash)
end)
|> Multi.insert(:smart_contract, smart_contract_changeset)
|> Multi.run(:clear_primary_address_names, &clear_primary_address_names/2)
|> Multi.run(:insert_address_name, &create_address_name/2)
|> Multi.run(:set_address_verified, &set_address_verified/2)
|> Repo.transaction()
case insert_result do
@ -2538,7 +2600,7 @@ defmodule Explorer.Chain do
end
end
defp set_address_verified(repo, %{smart_contract: %SmartContract{address_hash: address_hash}}) do
defp set_address_verified(repo, address_hash) do
query =
from(
address in Address,
@ -2551,7 +2613,7 @@ defmodule Explorer.Chain do
end
end
defp set_address_decompiled(repo, %{decompiled_smart_contract: %DecompiledSmartContract{address_hash: address_hash}}) do
defp set_address_decompiled(repo, address_hash) do
query =
from(
address in Address,
@ -2560,24 +2622,29 @@ defmodule Explorer.Chain do
case repo.update_all(query, set: [decompiled: true]) do
{1, _} -> {:ok, []}
_ -> {:error, "There was an error annotating that the address has been verified."}
_ -> {:error, "There was an error annotating that the address has been decompiled."}
end
end
defp clear_primary_address_names(repo, %{smart_contract: %SmartContract{address_hash: address_hash}}) do
clear_primary_query =
defp clear_primary_address_names(repo, address_hash) do
query =
from(
address_name in Address.Name,
where: address_name.address_hash == ^address_hash,
update: [set: [primary: false]]
# Enforce Name ShareLocks order (see docs: sharelocks.md)
order_by: [asc: :address_hash, asc: :name],
lock: "FOR UPDATE"
)
repo.update_all(clear_primary_query, [])
repo.update_all(
from(n in Address.Name, join: s in subquery(query), on: n.address_hash == s.address_hash),
set: [primary: false]
)
{:ok, []}
end
defp create_address_name(repo, %{smart_contract: %SmartContract{name: name, address_hash: address_hash}}) do
defp create_address_name(repo, name, address_hash) do
params = %{
address_hash: address_hash,
name: name,
@ -2924,14 +2991,20 @@ defmodule Explorer.Chain do
]) :: {integer(), nil | [term()]}
def find_and_update_replaced_transactions(transactions, timeout \\ :infinity) do
query =
Enum.reduce(transactions, Transaction, fn %{hash: hash, nonce: nonce, from_address_hash: from_address_hash},
query ->
from(t in query,
or_where:
t.nonce == ^nonce and t.from_address_hash == ^from_address_hash and t.hash != ^hash and
not is_nil(t.block_number)
)
end)
transactions
|> Enum.reduce(
Transaction,
fn %{hash: hash, nonce: nonce, from_address_hash: from_address_hash}, query ->
from(t in query,
or_where:
t.nonce == ^nonce and t.from_address_hash == ^from_address_hash and t.hash != ^hash and
not is_nil(t.block_number)
)
end
)
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
|> order_by(asc: :hash)
|> lock("FOR UPDATE")
hashes = Enum.map(transactions, & &1.hash)
@ -2974,10 +3047,15 @@ defmodule Explorer.Chain do
or_where: t.nonce == ^nonce and t.from_address_hash == ^from_address and is_nil(t.block_hash)
)
end)
update_query = from(t in query, update: [set: [status: ^:error, error: "dropped/replaced"]])
Repo.update_all(update_query, [], timeout: timeout)
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
|> order_by(asc: :hash)
|> lock("FOR UPDATE")
Repo.update_all(
from(t in Transaction, join: s in subquery(query), on: t.hash == s.hash),
[set: [error: "dropped/replaced", status: :error]],
timeout: timeout
)
end
end
@ -3004,8 +3082,15 @@ defmodule Explorer.Chain do
address_name_opts = [on_conflict: :nothing, conflict_target: [:address_hash, :name]]
# Enforce ShareLocks tables order (see docs: sharelocks.md)
insert_result =
Multi.new()
|> Multi.run(
:address_name,
fn repo, _ ->
{:ok, repo.insert(address_name_changeset, address_name_opts)}
end
)
|> Multi.run(:token, fn repo, _ ->
with {:error, %Changeset{errors: [{^stale_error_field, {^stale_error_message, []}}]}} <-
repo.insert(token_changeset, token_opts) do
@ -3013,12 +3098,6 @@ defmodule Explorer.Chain do
{:ok, token}
end
end)
|> Multi.run(
:address_name,
fn repo, _ ->
{:ok, repo.insert(address_name_changeset, address_name_opts)}
end
)
|> Repo.transaction()
case insert_result do
@ -3070,9 +3149,17 @@ defmodule Explorer.Chain do
address_hash
|> CoinBalance.balances_by_day(latest_block_timestamp)
|> Repo.all()
|> replace_last_value(latest_block_timestamp)
|> normalize_balances_by_day()
end
# https://github.com/poanetwork/blockscout/issues/2658
defp replace_last_value(items, %{value: value, timestamp: timestamp}) do
List.replace_at(items, -1, %{date: Date.convert!(timestamp, Calendar.ISO), value: value})
end
defp replace_last_value(items, _), do: items
defp normalize_balances_by_day(balances_by_day) do
result =
balances_by_day

@ -237,6 +237,16 @@ defmodule Explorer.Chain.Address do
)
end
@doc """
Counts all the addresses.
"""
def count do
from(
a in Address,
select: fragment("COUNT(*)")
)
end
defimpl String.Chars do
@doc """
Uses `hash` as string representation, formatting it according to the eip-55 specification

@ -112,7 +112,7 @@ defmodule Explorer.Chain.Address.CoinBalance do
|> join(:inner, [cb], b in Block, on: cb.block_number == b.number)
|> where([cb], cb.address_hash == ^address_hash)
|> last(:block_number)
|> select([cb, b], %{timestamp: b.timestamp})
|> select([cb, b], %{timestamp: b.timestamp, value: cb.value})
end
def changeset(%__MODULE__{} = balance, params) do

@ -0,0 +1,73 @@
defmodule Explorer.Chain.Cache.Accounts do
@moduledoc """
Caches the top Addresses
"""
alias Explorer.Chain.Address
use Explorer.Chain.OrderedCache,
name: :accounts,
max_size: 51,
preload: :names,
ttl_check_interval: Application.get_env(:explorer, __MODULE__)[:ttl_check_interval],
global_ttl: Application.get_env(:explorer, __MODULE__)[:global_ttl]
@type element :: Address.t()
@type id :: {non_neg_integer(), non_neg_integer()}
def element_to_id(%Address{fetched_coin_balance: fetched_coin_balance, hash: hash}) do
{fetched_coin_balance, hash}
end
def prevails?({fetched_coin_balance_a, hash_a}, {fetched_coin_balance_b, hash_b}) do
# same as a query's `order_by: [desc: :fetched_coin_balance, asc: :hash]`
if fetched_coin_balance_a == fetched_coin_balance_b do
hash_a < hash_b
else
fetched_coin_balance_a > fetched_coin_balance_b
end
end
def drop(nil), do: :ok
def drop([]), do: :ok
def drop(addresses) when is_list(addresses) do
# This has to be used by the Indexer insead of `update`.
# The reason being that addresses already in the cache can change their balance
# value and removing or updating them will result into a potentially invalid
# cache status, that would not even get corrected with time.
# The only thing we can safely do when an address in the cache changes its
# `fetched_coin_balance` is to invalidate the whole cache and wait for it
# to be filled again (by the query that it takes the place of when full).
ConCache.update(cache_name(), ids_list_key(), fn ids ->
if drop_needed?(ids, addresses) do
# Remove the addresses immediately
Enum.each(ids, &ConCache.delete(cache_name(), &1))
{:ok, []}
else
{:ok, ids}
end
end)
end
def drop(address), do: drop([address])
defp drop_needed?(ids, _addresses) when is_nil(ids), do: false
defp drop_needed?([], _addresses), do: false
defp drop_needed?(ids, addresses) do
ids_map = Map.new(ids, fn {balance, hash} -> {hash, balance} end)
# Result it `true` only when the address is present in the cache already,
# but with a different `fetched_coin_balance`
Enum.find_value(addresses, false, fn address ->
stored_address_balance = Map.get(ids_map, address.hash)
stored_address_balance && stored_address_balance != address.fetched_coin_balance
end)
end
end

@ -46,7 +46,10 @@ defmodule Explorer.Chain.ContractMethod do
end)
end
Repo.insert_all(__MODULE__, successes, on_conflict: :nothing, conflict_target: [:identifier, :abi])
# Enforce ContractMethod ShareLocks order (see docs: sharelocks.md)
ordered_successes = Enum.sort_by(successes, &{&1.identifier, &1.abi})
Repo.insert_all(__MODULE__, ordered_successes, on_conflict: :nothing, conflict_target: [:identifier, :abi])
end
def import_all do

@ -10,7 +10,9 @@ defmodule Explorer.Chain.Import do
@stages [
Import.Stage.Addresses,
Import.Stage.AddressReferencing
Import.Stage.AddressReferencing,
Import.Stage.BlockReferencing,
Import.Stage.BlockFollowing
]
# in order so that foreign keys are inserted before being referenced

@ -71,7 +71,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CoinBalances do
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)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce CoinBalance ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.address_hash, &1.block_number})
{:ok, _} =

@ -106,12 +106,14 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.address_hash, &1.token_contract_address_hash})
# Enforce ShareLocks tables order (see docs: sharelocks.md)
multi
|> Multi.run(:acquire_contract_address_tokens, fn repo, _ ->
contract_address_hashes = changes_list |> Enum.map(& &1.token_contract_address_hash) |> Enum.uniq()
Tokens.acquire_contract_address_tokens(repo, contract_address_hashes)
end)
|> Multi.run(:address_current_token_balances, fn repo, _ ->
insert(repo, ordered_changes_list, insert_options)
insert(repo, changes_list, insert_options)
end)
|> Multi.run(:address_current_token_balances_update_token_holder_counts, fn repo,
%{
@ -120,6 +122,7 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
} ->
token_holder_count_deltas = upserted_balances_to_holder_count_deltas(upserted_balances)
# ShareLocks order already enforced by `acquire_contract_address_tokens` (see docs: sharelocks.md)
Tokens.update_holder_counts_with_deltas(
repo,
token_holder_count_deltas,
@ -193,10 +196,13 @@ defmodule Explorer.Chain.Import.Runner.Address.CurrentTokenBalances do
}) ::
{:ok, [CurrentTokenBalance.t()]}
| {:error, [Changeset.t()]}
defp insert(repo, ordered_changes_list, %{timeout: timeout, timestamps: timestamps} = options)
when is_atom(repo) and is_list(ordered_changes_list) do
defp insert(repo, changes_list, %{timeout: timeout, timestamps: timestamps} = options)
when is_atom(repo) and is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.address_hash, &1.token_contract_address_hash})
Import.insert_changes_list(
repo,
ordered_changes_list,

@ -59,7 +59,7 @@ defmodule Explorer.Chain.Import.Runner.Address.TokenBalances do
def 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)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce TokenBalance ShareLocks order (see docs: sharelocks.md)
ordered_changes_list =
Enum.sort_by(changes_list, &{&1.address_hash, &1.token_contract_address_hash, &1.block_number})

@ -80,7 +80,7 @@ defmodule Explorer.Chain.Import.Runner.Addresses do
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)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce Address ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = sort_changes_list(changes_list)
Import.insert_changes_list(
@ -104,13 +104,15 @@ defmodule Explorer.Chain.Import.Runner.Addresses do
fetched_coin_balance:
fragment(
"""
CASE WHEN EXCLUDED.fetched_coin_balance_block_number IS NOT NULL AND
(? IS NULL OR
CASE WHEN EXCLUDED.fetched_coin_balance_block_number IS NOT NULL
AND EXCLUDED.fetched_coin_balance IS NOT NULL AND
(? IS NULL OR ? IS NULL OR
EXCLUDED.fetched_coin_balance_block_number >= ?) THEN
EXCLUDED.fetched_coin_balance
ELSE ?
END
""",
address.fetched_coin_balance,
address.fetched_coin_balance_block_number,
address.fetched_coin_balance_block_number,
address.fetched_coin_balance
@ -153,13 +155,18 @@ defmodule Explorer.Chain.Import.Runner.Addresses do
query =
from(t in Transaction,
where: t.created_contract_address_hash in ^ordered_created_contract_hashes,
update: [
set: [created_contract_code_indexed_at: ^timestamps.updated_at]
]
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
order_by: t.hash,
lock: "FOR UPDATE"
)
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
{_, result} =
repo.update_all(
from(t in Transaction, join: s in subquery(query), on: t.hash == s.hash),
[set: [created_contract_code_indexed_at: timestamps.updated_at]],
timeout: timeout
)
{:ok, result}
rescue

@ -52,7 +52,7 @@ defmodule Explorer.Chain.Import.Runner.Block.Rewards do
when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce Reward ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.address_hash, &1.address_type, &1.block_hash})
Import.insert_changes_list(

@ -62,7 +62,7 @@ defmodule Explorer.Chain.Import.Runner.Block.SecondDegreeRelations 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)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce SeconDegreeRelation ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.nephew_hash, &1.uncle_hash})
Import.insert_changes_list(repo, ordered_changes_list,

@ -5,11 +5,10 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
require Ecto.Query
import Ecto.Query, only: [from: 2, select: 2, subquery: 1, update: 2]
import Ecto.Query, only: [from: 2, subquery: 1]
alias Ecto.Adapters.SQL
alias Ecto.{Changeset, Multi, Repo}
alias Explorer.Chain.{Address, Block, Hash, Import, InternalTransaction, Transaction}
alias Explorer.Chain.{Address, Block, Import, InternalTransaction, Log, TokenTransfer, Transaction}
alias Explorer.Chain.Block.Reward
alias Explorer.Chain.Import.Runner
alias Explorer.Chain.Import.Runner.Address.CurrentTokenBalances
@ -45,39 +44,68 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
ordered_consensus_block_numbers = ordered_consensus_block_numbers(changes_list)
where_invalid_neighbour = where_invalid_neighbour(changes_list)
where_forked = where_forked(changes_list)
hashes = Enum.map(changes_list, & &1.hash)
consensus_block_numbers = consensus_block_numbers(changes_list)
# Enforce ShareLocks tables order (see docs: sharelocks.md)
multi
|> Multi.run(:derive_transaction_forks, fn repo, _ ->
derive_transaction_forks(%{
repo: repo,
timeout: options[Runner.Transaction.Forks.option_key()][:timeout] || Runner.Transaction.Forks.timeout(),
timestamps: timestamps,
where_forked: where_forked
|> Multi.run(:lose_consensus, fn repo, _ ->
lose_consensus(repo, hashes, consensus_block_numbers, changes_list, insert_options)
end)
|> Multi.run(:blocks, fn repo, _ ->
# Note, needs to be executed after `lose_consensus` for lock acquisition
insert(repo, changes_list, insert_options)
end)
|> Multi.run(:uncle_fetched_block_second_degree_relations, fn repo, %{blocks: blocks} when is_list(blocks) ->
update_block_second_degree_relations(repo, hashes, %{
timeout:
options[Runner.Block.SecondDegreeRelations.option_key()][:timeout] ||
Runner.Block.SecondDegreeRelations.timeout(),
timestamps: timestamps
})
end)
# MUST be after `:derive_transaction_forks`, which depends on values in `transactions` table
|> Multi.run(:delete_rewards, fn repo, _ ->
delete_rewards(repo, changes_list, insert_options)
end)
|> Multi.run(:fork_transactions, fn repo, _ ->
fork_transactions(%{
repo: repo,
timeout: options[Runner.Transactions.option_key()][:timeout] || Runner.Transactions.timeout(),
timestamps: timestamps,
where_forked: where_forked
blocks_changes: changes_list
})
end)
|> Multi.run(:lose_consensus, fn repo, _ ->
lose_consensus(repo, ordered_consensus_block_numbers, insert_options)
|> Multi.run(:derive_transaction_forks, fn repo, %{fork_transactions: transactions} ->
derive_transaction_forks(%{
repo: repo,
timeout: options[Runner.Transaction.Forks.option_key()][:timeout] || Runner.Transaction.Forks.timeout(),
timestamps: timestamps,
transactions: transactions
})
end)
|> Multi.run(:remove_nonconsensus_logs, fn repo, %{derive_transaction_forks: transactions} ->
remove_nonconsensus_logs(repo, transactions, insert_options)
end)
|> Multi.run(:acquire_internal_transactions, fn repo, %{derive_transaction_forks: transactions} ->
acquire_internal_transactions(repo, hashes, transactions)
end)
|> Multi.run(:lose_invalid_neighbour_consensus, fn repo, _ ->
lose_invalid_neighbour_consensus(repo, where_invalid_neighbour, insert_options)
|> Multi.run(:remove_nonconsensus_internal_transactions, fn repo, %{derive_transaction_forks: transactions} ->
remove_nonconsensus_internal_transactions(repo, transactions, insert_options)
end)
|> Multi.run(:internal_transaction_transaction_block_number, fn repo, _ ->
update_internal_transaction_block_number(repo, hashes)
end)
|> Multi.run(:acquire_contract_address_tokens, fn repo, _ ->
acquire_contract_address_tokens(repo, consensus_block_numbers)
end)
|> Multi.run(:remove_nonconsensus_token_transfers, fn repo, %{derive_transaction_forks: transactions} ->
remove_nonconsensus_token_transfers(repo, transactions, insert_options)
end)
|> Multi.run(:delete_address_token_balances, fn repo, _ ->
delete_address_token_balances(repo, ordered_consensus_block_numbers, insert_options)
delete_address_token_balances(repo, consensus_block_numbers, insert_options)
end)
|> Multi.run(:delete_address_current_token_balances, fn repo, _ ->
delete_address_current_token_balances(repo, ordered_consensus_block_numbers, insert_options)
delete_address_current_token_balances(repo, consensus_block_numbers, insert_options)
end)
|> Multi.run(:derive_address_current_token_balances, fn repo,
%{
@ -94,130 +122,131 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
deltas = CurrentTokenBalances.token_holder_count_deltas(%{deleted: deleted, inserted: inserted})
Tokens.update_holder_counts_with_deltas(repo, deltas, insert_options)
end)
|> Multi.run(:delete_rewards, fn repo, _ ->
delete_rewards(repo, changes_list, insert_options)
end)
|> Multi.run(:blocks, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
|> Multi.run(:uncle_fetched_block_second_degree_relations, fn repo, %{blocks: blocks} when is_list(blocks) ->
update_block_second_degree_relations(
repo,
blocks,
%{
timeout:
options[Runner.Block.SecondDegreeRelations.option_key()][:timeout] ||
Runner.Block.SecondDegreeRelations.timeout(),
timestamps: timestamps
}
)
end)
|> Multi.run(
:internal_transaction_transaction_block_number,
fn repo, %{blocks: blocks} ->
blocks_hashes = Enum.map(blocks, & &1.hash)
query =
from(
internal_transaction in InternalTransaction,
join: transaction in Transaction,
on: internal_transaction.transaction_hash == transaction.hash,
join: block in Block,
on: block.hash == transaction.block_hash,
where: block.hash in ^blocks_hashes,
update: [
set: [
block_number: block.number
]
]
)
{total, _} = repo.update_all(query, [])
{:ok, total}
end
)
end
@impl Runner
def timeout, do: @timeout
# sobelow_skip ["SQL.Query"]
defp derive_transaction_forks(%{
repo: repo,
timeout: timeout,
timestamps: %{inserted_at: inserted_at, updated_at: updated_at},
where_forked: where_forked
}) do
defp acquire_contract_address_tokens(repo, consensus_block_numbers) do
query =
from(transaction in where_forked,
select: [
transaction.block_hash,
transaction.index,
transaction.hash,
type(^inserted_at, transaction.inserted_at),
type(^updated_at, transaction.updated_at)
from(address_current_token_balance in Address.CurrentTokenBalance,
where: address_current_token_balance.block_number in ^consensus_block_numbers,
select: address_current_token_balance.token_contract_address_hash
)
contract_address_hashes = repo.all(query)
Tokens.acquire_contract_address_tokens(repo, contract_address_hashes)
end
defp acquire_internal_transactions(repo, hashes, forked_transaction_hashes) do
query =
from(internal_transaction in InternalTransaction,
join: transaction in Transaction,
on: internal_transaction.transaction_hash == transaction.hash,
where: transaction.block_hash in ^hashes,
or_where: transaction.hash in ^forked_transaction_hashes,
select: {internal_transaction.transaction_hash, internal_transaction.index},
# Enforce InternalTransaction ShareLocks order (see docs: sharelocks.md)
order_by: [
internal_transaction.transaction_hash,
internal_transaction.index
],
# order so that row ShareLocks are grabbed in a consistent order with
# `Explorer.Chain.Import.Runner.Transactions.insert`
order_by: transaction.hash
# NOTE: find a better way to know the alias that ecto gives to token
lock: "FOR UPDATE OF i0"
)
{select_sql, parameters} = SQL.to_sql(:all, repo, query)
insert_sql = """
INSERT INTO transaction_forks (uncle_hash, index, hash, inserted_at, updated_at)
#{select_sql}
ON CONFLICT (uncle_hash, index)
DO UPDATE SET hash = EXCLUDED.hash
WHERE EXCLUDED.hash <> transaction_forks.hash
RETURNING uncle_hash, hash
"""
with {:ok, %Postgrex.Result{columns: ["uncle_hash", "hash"], command: :insert, rows: rows}} <-
SQL.query(
repo,
insert_sql,
parameters,
timeout: timeout
) do
derived_transaction_forks = Enum.map(rows, fn [uncle_hash, hash] -> %{uncle_hash: uncle_hash, hash: hash} end)
{:ok, derived_transaction_forks}
end
{:ok, repo.all(query)}
end
defp fork_transactions(%{
repo: repo,
timeout: timeout,
timestamps: %{updated_at: updated_at},
where_forked: where_forked
blocks_changes: blocks_changes
}) do
query =
where_forked
|> update(
set: [
block_hash: nil,
block_number: nil,
gas_used: nil,
cumulative_gas_used: nil,
index: nil,
internal_transactions_indexed_at: nil,
status: nil,
error: nil,
updated_at: ^updated_at
]
from(
transaction in where_forked(blocks_changes),
select: %{
block_hash: transaction.block_hash,
index: transaction.index,
hash: transaction.hash
},
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
order_by: [asc: :hash],
lock: "FOR UPDATE"
)
|> select([:hash])
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
update_query =
from(
t in Transaction,
join: s in subquery(query),
on: t.hash == s.hash,
update: [
set: [
block_hash: nil,
block_number: nil,
gas_used: nil,
cumulative_gas_used: nil,
index: nil,
internal_transactions_indexed_at: nil,
status: nil,
error: nil,
updated_at: ^updated_at
]
],
select: %{
block_hash: s.block_hash,
index: s.index,
hash: s.hash
}
)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error}}
end
{_num, transactions} = repo.update_all(update_query, [], timeout: timeout)
{:ok, transactions}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error}}
end
defp derive_transaction_forks(%{
repo: repo,
timeout: timeout,
timestamps: %{inserted_at: inserted_at, updated_at: updated_at},
transactions: transactions
}) do
transaction_forks =
transactions
|> Enum.map(fn transaction ->
%{
uncle_hash: transaction.block_hash,
index: transaction.index,
hash: transaction.hash,
inserted_at: inserted_at,
updated_at: updated_at
}
end)
# Enforce Fork ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(&{&1.uncle_hash, &1.index})
{_total, forked_transaction} =
repo.insert_all(
Transaction.Fork,
transaction_forks,
conflict_target: [:uncle_hash, :index],
on_conflict:
from(
transaction_fork in Transaction.Fork,
update: [set: [hash: fragment("EXCLUDED.hash")]],
where: fragment("EXCLUDED.hash <> ?", transaction_fork.hash)
),
returning: [:hash],
timeout: timeout
)
{:ok, Enum.map(forked_transaction, & &1.hash)}
end
@spec insert(Repo.t(), [map()], %{
@ -228,8 +257,8 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
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)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.number, &1.hash})
# Enforce Block ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, & &1.hash)
Import.insert_changes_list(
repo,
@ -277,85 +306,135 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
)
end
defp ordered_consensus_block_numbers(blocks_changes) when is_list(blocks_changes) do
defp consensus_block_numbers(blocks_changes) when is_list(blocks_changes) do
blocks_changes
|> Enum.reduce(MapSet.new(), fn
%{consensus: true, number: number}, acc ->
MapSet.put(acc, number)
%{consensus: false}, acc ->
acc
end)
|> Enum.sort()
|> Enum.filter(& &1.consensus)
|> Enum.map(& &1.number)
end
defp lose_consensus(_, [], _), do: {:ok, []}
defp lose_consensus(repo, hashes, consensus_block_numbers, changes_list, %{
timeout: timeout,
timestamps: %{updated_at: updated_at}
}) do
acquire_query =
from(
block in where_invalid_neighbour(changes_list),
or_where: block.number in ^consensus_block_numbers,
# we also need to acquire blocks that will be upserted here, for ordering
or_where: block.hash in ^hashes,
select: block.hash,
# Enforce Block ShareLocks order (see docs: sharelocks.md)
order_by: [asc: block.hash],
lock: "FOR UPDATE"
)
defp lose_consensus(repo, ordered_consensus_block_number, %{timeout: timeout, timestamps: %{updated_at: updated_at}})
when is_list(ordered_consensus_block_number) do
query =
{_, removed_consensus_block_hashes} =
repo.update_all(
from(
block in Block,
join: s in subquery(acquire_query),
on: block.hash == s.hash,
# we don't want to remove consensus from blocks that will be upserted
where: block.hash not in ^hashes,
select: block.hash
),
[set: [consensus: false, updated_at: updated_at]],
timeout: timeout
)
{:ok, removed_consensus_block_hashes}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, consensus_block_numbers: consensus_block_numbers}}
end
defp remove_nonconsensus_token_transfers(repo, forked_transaction_hashes, %{timeout: timeout}) do
ordered_token_transfers =
from(
block in Block,
where: block.number in ^ordered_consensus_block_number,
update: [
set: [
consensus: false,
updated_at: ^updated_at
]
token_transfer in TokenTransfer,
where: token_transfer.transaction_hash in ^forked_transaction_hashes,
select: token_transfer.transaction_hash,
# Enforce TokenTransfer ShareLocks order (see docs: sharelocks.md)
order_by: [
token_transfer.transaction_hash,
token_transfer.log_index
],
select: [:hash, :number]
lock: "FOR UPDATE"
)
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
query =
from(token_transfer in TokenTransfer,
select: map(token_transfer, [:transaction_hash, :log_index]),
inner_join: ordered_token_transfer in subquery(ordered_token_transfers),
on: ordered_token_transfer.transaction_hash == token_transfer.transaction_hash
)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, consensus_block_numbers: ordered_consensus_block_number}}
end
{_count, deleted_token_transfers} = repo.delete_all(query, timeout: timeout)
{:ok, deleted_token_transfers}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, transactions: forked_transaction_hashes}}
end
defp lose_invalid_neighbour_consensus(repo, where_invalid_neighbour, %{
timeout: timeout,
timestamps: %{updated_at: updated_at}
}) do
defp remove_nonconsensus_internal_transactions(repo, forked_transaction_hashes, %{timeout: timeout}) do
query =
from(internal_transaction in InternalTransaction,
where: internal_transaction.transaction_hash in ^forked_transaction_hashes,
select: map(internal_transaction, [:transaction_hash, :index])
)
# ShareLocks order already enforced by `acquire_internal_transactions` (see docs: sharelocks.md)
{_count, deleted_internal_transactions} = repo.delete_all(query, timeout: timeout)
{:ok, deleted_internal_transactions}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, transactions: forked_transaction_hashes}}
end
defp remove_nonconsensus_logs(repo, forked_transaction_hashes, %{timeout: timeout}) do
ordered_logs =
from(
block in where_invalid_neighbour,
update: [
set: [
consensus: false,
updated_at: ^updated_at
]
log in Log,
where: log.transaction_hash in ^forked_transaction_hashes,
select: log.transaction_hash,
# Enforce Log ShareLocks order (see docs: sharelocks.md)
order_by: [
log.transaction_hash,
log.index
],
select: [:hash, :number]
lock: "FOR UPDATE"
)
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
query =
from(log in Log,
select: map(log, [:transaction_hash, :index]),
inner_join: ordered_log in subquery(ordered_logs),
on: ordered_log.transaction_hash == log.transaction_hash
)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, where_invalid_neighbour: where_invalid_neighbour}}
end
{_count, deleted_logs} = repo.delete_all(query, timeout: timeout)
{:ok, deleted_logs}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, transactions: forked_transaction_hashes}}
end
defp delete_address_token_balances(_, [], _), do: {:ok, []}
defp delete_address_token_balances(repo, ordered_consensus_block_numbers, %{timeout: timeout}) do
defp delete_address_token_balances(repo, consensus_block_numbers, %{timeout: timeout}) do
ordered_query =
from(address_token_balance in Address.TokenBalance,
where: address_token_balance.block_number in ^ordered_consensus_block_numbers,
where: address_token_balance.block_number in ^consensus_block_numbers,
select: map(address_token_balance, [:address_hash, :token_contract_address_hash, :block_number]),
# MUST match order in `Explorer.Chain.Import.Runner.Address.TokenBalances.insert` to prevent ShareLock ordering deadlocks.
# Enforce TokenBalance ShareLocks order (see docs: sharelocks.md)
order_by: [
address_token_balance.address_hash,
address_token_balance.token_contract_address_hash,
address_token_balance.block_number
],
# ensures rows remains locked while outer query is joining to it
lock: "FOR UPDATE"
)
@ -376,23 +455,22 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
{:ok, deleted_address_token_balances}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, block_numbers: ordered_consensus_block_numbers}}
{:error, %{exception: postgrex_error, block_numbers: consensus_block_numbers}}
end
end
defp delete_address_current_token_balances(_, [], _), do: {:ok, []}
defp delete_address_current_token_balances(repo, ordered_consensus_block_numbers, %{timeout: timeout}) do
defp delete_address_current_token_balances(repo, consensus_block_numbers, %{timeout: timeout}) do
ordered_query =
from(address_current_token_balance in Address.CurrentTokenBalance,
where: address_current_token_balance.block_number in ^ordered_consensus_block_numbers,
where: address_current_token_balance.block_number in ^consensus_block_numbers,
select: map(address_current_token_balance, [:address_hash, :token_contract_address_hash]),
# MUST match order in `Explorer.Chain.Import.Runner.Address.CurrentTokenBalances.insert` to prevent ShareLock ordering deadlocks.
# Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
order_by: [
address_current_token_balance.address_hash,
address_current_token_balance.token_contract_address_hash
],
# ensures row remains locked while outer query is joining to it
lock: "FOR UPDATE"
)
@ -420,13 +498,12 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
{:ok, deleted_address_current_token_balances}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, block_numbers: ordered_consensus_block_numbers}}
{:error, %{exception: postgrex_error, block_numbers: consensus_block_numbers}}
end
end
defp derive_address_current_token_balances(_, [], _), do: {:ok, []}
# sobelow_skip ["SQL.Query"]
defp derive_address_current_token_balances(repo, deleted_address_current_token_balances, %{timeout: timeout})
when is_list(deleted_address_current_token_balances) do
initial_query =
@ -460,57 +537,39 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
address_token_balance.address_hash == new_current_token_balance.address_hash and
address_token_balance.token_contract_address_hash == new_current_token_balance.token_contract_address_hash and
address_token_balance.block_number == new_current_token_balance.block_number,
select: {
new_current_token_balance.address_hash,
new_current_token_balance.token_contract_address_hash,
new_current_token_balance.block_number,
address_token_balance.value,
over(min(address_token_balance.inserted_at), :w),
over(max(address_token_balance.updated_at), :w)
select: %{
address_hash: new_current_token_balance.address_hash,
token_contract_address_hash: new_current_token_balance.token_contract_address_hash,
block_number: new_current_token_balance.block_number,
value: address_token_balance.value,
inserted_at: over(min(address_token_balance.inserted_at), :w),
updated_at: over(max(address_token_balance.updated_at), :w)
},
# Prevent ShareLock deadlock by matching order of `Explorer.Chain.Import.Runner.Address.CurrentTokenBalances.insert`
order_by: [new_current_token_balance.address_hash, new_current_token_balance.token_contract_address_hash],
windows: [
w: [partition_by: [address_token_balance.address_hash, address_token_balance.token_contract_address_hash]]
]
)
{select_sql, parameters} = SQL.to_sql(:all, repo, new_current_token_balance_query)
# No `ON CONFLICT` because `delete_address_current_token_balances` should have removed any conflicts.
insert_sql = """
INSERT INTO address_current_token_balances (address_hash, token_contract_address_hash, block_number, value, inserted_at, updated_at)
#{select_sql}
RETURNING address_hash, token_contract_address_hash, block_number, value
"""
with {:ok,
%Postgrex.Result{
columns: [
"address_hash",
"token_contract_address_hash",
"block_number",
# needed for `update_tokens_holder_count`
"value"
],
command: :insert,
rows: rows
}} <- SQL.query(repo, insert_sql, parameters, timeout: timeout) do
derived_address_current_token_balances =
Enum.map(rows, fn [address_hash_bytes, token_contract_address_hash_bytes, block_number, value] ->
{:ok, address_hash} = Hash.Address.load(address_hash_bytes)
{:ok, token_contract_address_hash} = Hash.Address.load(token_contract_address_hash_bytes)
%{
address_hash: address_hash,
token_contract_address_hash: token_contract_address_hash,
block_number: block_number,
value: value
}
end)
{:ok, derived_address_current_token_balances}
end
ordered_current_token_balance =
new_current_token_balance_query
|> repo.all()
# Enforce CurrentTokenBalance ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(&{&1.address_hash, &1.token_contract_address_hash})
{_total, result} =
repo.insert_all(
Address.CurrentTokenBalance,
ordered_current_token_balance,
# No `ON CONFLICT` because `delete_address_current_token_balances`
# should have removed any conflicts.
returning: [:address_hash, :token_contract_address_hash, :block_number, :value],
timeout: timeout
)
derived_address_current_token_balances =
Enum.map(result, &Map.take(&1, [:address_hash, :token_contract_address_hash, :block_number, :value]))
{:ok, derived_address_current_token_balances}
end
# `block_rewards` are linked to `blocks.hash`, but fetched by `blocks.number`, so when a block with the same number is
@ -528,11 +587,24 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
query =
from(reward in Reward,
inner_join: block in assoc(reward, :block),
where: block.hash in ^hashes or block.number in ^numbers
where: block.hash in ^hashes or block.number in ^numbers,
# Enforce Reward ShareLocks order (see docs: sharelocks.md)
order_by: [asc: :address_hash, asc: :address_type, asc: :block_hash],
# NOTE: find a better way to know the alias that ecto gives to token
lock: "FOR UPDATE OF b0"
)
delete_query =
from(r in Reward,
join: s in subquery(query),
on:
r.address_hash == s.address_hash and
r.address_type == s.address_type and
r.block_hash == s.block_hash
)
try do
{count, nil} = repo.delete_all(query, timeout: timeout)
{count, nil} = repo.delete_all(delete_query, timeout: timeout)
{:ok, count}
rescue
@ -541,34 +613,56 @@ defmodule Explorer.Chain.Import.Runner.Blocks do
end
end
defp update_block_second_degree_relations(repo, blocks, %{timeout: timeout, timestamps: %{updated_at: updated_at}})
when is_list(blocks) do
ordered_uncle_hashes =
blocks
|> MapSet.new(& &1.hash)
|> Enum.sort()
defp update_block_second_degree_relations(repo, uncle_hashes, %{
timeout: timeout,
timestamps: %{updated_at: updated_at}
})
when is_list(uncle_hashes) do
query =
from(
bsdr in Block.SecondDegreeRelation,
where: bsdr.uncle_hash in ^ordered_uncle_hashes,
update: [
set: [
uncle_fetched_at: ^updated_at
]
]
where: bsdr.uncle_hash in ^uncle_hashes,
# Enforce SeconDegreeRelation ShareLocks order (see docs: sharelocks.md)
order_by: [asc: :nephew_hash, asc: :uncle_hash],
lock: "FOR UPDATE"
)
update_query =
from(
b in Block.SecondDegreeRelation,
join: s in subquery(query),
on: b.nephew_hash == s.nephew_hash and b.uncle_hash == s.uncle_hash,
update: [set: [uncle_fetched_at: ^updated_at]]
)
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
{_, result} = repo.update_all(update_query, [], timeout: timeout)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, uncle_hashes: ordered_uncle_hashes}}
{:error, %{exception: postgrex_error, uncle_hashes: uncle_hashes}}
end
end
defp update_internal_transaction_block_number(repo, blocks_hashes) when is_list(blocks_hashes) do
query =
from(
internal_transaction in InternalTransaction,
join: transaction in Transaction,
on: internal_transaction.transaction_hash == transaction.hash,
join: block in Block,
on: block.hash == transaction.block_hash,
where: block.hash in ^blocks_hashes,
update: [set: [block_number: block.number]]
)
# ShareLocks order already enforced by `acquire_internal_transactions` (see docs: sharelocks.md)
{total, _} = repo.update_all(query, [])
{:ok, total}
end
defp where_forked(blocks_changes) when is_list(blocks_changes) do
initial = from(t in Transaction, where: false)

@ -47,12 +47,16 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
update_transactions_options = %{timeout: transactions_timeout, timestamps: timestamps}
# Enforce ShareLocks tables order (see docs: sharelocks.md)
multi
|> Multi.run(:acquire_transactions, fn repo, _ ->
acquire_transactions(repo, changes_list)
end)
|> Multi.run(:internal_transactions, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
|> Multi.run(:internal_transactions_indexed_at_transactions, fn repo, _ ->
update_transactions(repo, changes_list, update_transactions_options)
|> Multi.run(:internal_transactions_indexed_at_transactions, fn repo, %{acquire_transactions: transaction_hashes} ->
update_transactions(repo, transaction_hashes, update_transactions_options)
end)
end
@ -70,7 +74,7 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
when is_list(changes_list) do
on_conflict = Map.get_lazy(options, :on_conflict, &default_on_conflict/0)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce InternalTransaction ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.index})
final_changes_list = reject_pending_transactions(ordered_changes_list, repo)
@ -144,21 +148,38 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
)
end
defp update_transactions(repo, internal_transactions, %{
timeout: timeout,
timestamps: timestamps
})
when is_list(internal_transactions) do
ordered_transaction_hashes =
defp acquire_transactions(repo, internal_transactions) do
transaction_hashes =
internal_transactions
|> MapSet.new(& &1.transaction_hash)
|> Enum.sort()
|> MapSet.to_list()
query =
from(
t in Transaction,
where: t.hash in ^ordered_transaction_hashes,
where: t.hash in ^transaction_hashes,
where: not is_nil(t.block_hash),
select: t.hash,
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
order_by: t.hash,
lock: "FOR UPDATE"
)
hashes = repo.all(query)
{:ok, hashes}
end
defp update_transactions(repo, transaction_hashes, %{
timeout: timeout,
timestamps: timestamps
})
when is_list(transaction_hashes) do
update_query =
from(
t in Transaction,
where: t.hash in ^transaction_hashes,
# ShareLocks order already enforced by `acquire_transactions` (see docs: sharelocks.md)
update: [
set: [
internal_transactions_indexed_at: ^timestamps.updated_at,
@ -184,12 +205,12 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactions do
)
try do
{_transaction_count, result} = repo.update_all(query, [], timeout: timeout)
{_transaction_count, result} = repo.update_all(update_query, [], timeout: timeout)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, transaction_hashes: ordered_transaction_hashes}}
{:error, %{exception: postgrex_error, transaction_hashes: transaction_hashes}}
end
end

@ -49,36 +49,36 @@ defmodule Explorer.Chain.Import.Runner.InternalTransactionsIndexedAtBlocks do
defp update_blocks(_repo, [], %{}), do: {:ok, []}
defp update_blocks(repo, block_numbers, %{
defp update_blocks(repo, changes_list, %{
timeout: timeout,
timestamps: timestamps
})
when is_list(block_numbers) do
ordered_block_numbers =
block_numbers
|> Enum.map(fn %{number: number} -> number end)
|> Enum.sort()
when is_list(changes_list) do
block_numbers = Enum.map(changes_list, fn %{number: number} -> number end)
query =
from(
b in Block,
where: b.number in ^ordered_block_numbers and b.consensus,
update: [
set: [
internal_transactions_indexed_at: ^timestamps.updated_at
]
]
where: b.number in ^block_numbers and b.consensus,
# Enforce Block ShareLocks order (see docs: sharelocks.md)
order_by: [asc: b.hash],
lock: "FOR UPDATE"
)
block_count = Enum.count(ordered_block_numbers)
block_count = Enum.count(block_numbers)
try do
{^block_count, result} = repo.update_all(query, [], timeout: timeout)
{^block_count, result} =
repo.update_all(
from(b in Block, join: s in subquery(query), on: b.hash == s.hash),
[set: [internal_transactions_indexed_at: timestamps.updated_at]],
timeout: timeout
)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, block_numbers: ordered_block_numbers}}
{:error, %{exception: postgrex_error, block_numbers: block_numbers}}
end
end
end

@ -58,7 +58,7 @@ defmodule Explorer.Chain.Import.Runner.Logs do
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)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce Log ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.index})
{:ok, _} =

@ -40,7 +40,11 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
|> Map.put_new(:timeout, @timeout)
|> Map.put(:timestamps, timestamps)
# Enforce ShareLocks tables order (see docs: sharelocks.md)
multi
|> Multi.run(:acquire_all_staking_pools, fn repo, _ ->
acquire_all_staking_pools(repo)
end)
|> Multi.run(:mark_as_deleted, fn repo, _ ->
mark_as_deleted(repo, changes_list, insert_options)
end)
@ -55,6 +59,20 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
@impl Import.Runner
def timeout, do: @timeout
defp acquire_all_staking_pools(repo) do
query =
from(
pool in StakingPool,
# Enforce StackingPool ShareLocks order (see docs: sharelocks.md)
order_by: pool.staking_address_hash,
lock: "FOR UPDATE"
)
pools = repo.all(query)
{:ok, pools}
end
defp mark_as_deleted(repo, changes_list, %{timeout: timeout}) when is_list(changes_list) do
addresses = Enum.map(changes_list, & &1.staking_address_hash)
@ -62,12 +80,8 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
from(
pool in StakingPool,
where: pool.staking_address_hash not in ^addresses,
update: [
set: [
is_deleted: true,
is_active: false
]
]
# ShareLocks order already enforced by `acquire_all_staking_pools` (see docs: sharelocks.md)
update: [set: [is_deleted: true, is_active: false]]
)
try do
@ -90,10 +104,13 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
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)
# Enforce StackingPool ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, & &1.staking_address_hash)
{:ok, _} =
Import.insert_changes_list(
repo,
changes_list,
ordered_changes_list,
conflict_target: :staking_address_hash,
on_conflict: on_conflict,
for: StakingPool,
@ -138,10 +155,11 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
total = repo.one!(total_query)
if total > Decimal.new(0) do
query =
update_query =
from(
p in StakingPool,
where: p.is_active == true,
# ShareLocks order already enforced by `acquire_all_staking_pools` (see docs: sharelocks.md)
update: [
set: [
staked_ratio: p.staked_amount / ^total * 100,
@ -150,7 +168,8 @@ defmodule Explorer.Chain.Import.Runner.StakingPools do
]
)
{count, _} = repo.update_all(query, [], timeout: timeout)
{count, _} = repo.update_all(update_query, [], timeout: timeout)
{:ok, count}
else
{:ok, 1}

@ -59,10 +59,13 @@ defmodule Explorer.Chain.Import.Runner.StakingPoolsDelegators do
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)
# Enforce StackingPoolDelegator ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.delegator_address_hash, &1.pool_address_hash})
{:ok, _} =
Import.insert_changes_list(
repo,
changes_list,
ordered_changes_list,
conflict_target: [:pool_address_hash, :delegator_address_hash],
on_conflict: on_conflict,
for: StakingPoolsDelegator,

@ -54,7 +54,7 @@ defmodule Explorer.Chain.Import.Runner.TokenTransfers do
def 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)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce TokenTransfer ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.transaction_hash, &1.log_index})
{:ok, _} =

@ -7,7 +7,6 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
import Ecto.Query, only: [from: 2]
alias Ecto.Adapters.SQL
alias Ecto.{Multi, Repo}
alias Explorer.Chain.{Hash, Import, Token}
@ -22,10 +21,61 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
@type holder_count :: non_neg_integer()
@type token_holder_count :: %{contract_address_hash: Hash.Address.t(), count: holder_count()}
def update_holder_counts_with_deltas(repo, token_holder_count_deltas, options) do
parameters = token_holder_count_deltas_to_parameters(token_holder_count_deltas)
def acquire_contract_address_tokens(repo, contract_address_hashes) do
token_query =
from(
token in Token,
where: token.contract_address_hash in ^contract_address_hashes,
# Enforce Token ShareLocks order (see docs: sharelocks.md)
order_by: token.contract_address_hash,
lock: "FOR UPDATE"
)
tokens = repo.all(token_query)
{:ok, tokens}
end
def update_holder_counts_with_deltas(repo, token_holder_count_deltas, %{
timeout: timeout,
timestamps: %{updated_at: updated_at}
}) do
# NOTE that acquire_contract_address_tokens needs to be called before this
{hashes, deltas} =
token_holder_count_deltas
|> Enum.map(fn %{contract_address_hash: contract_address_hash, delta: delta} ->
{:ok, contract_address_hash_bytes} = Hash.Address.dump(contract_address_hash)
{contract_address_hash_bytes, delta}
end)
|> Enum.unzip()
query =
from(
token in Token,
join:
deltas in fragment(
"(SELECT unnest(?::bytea[]) as contract_address_hash, unnest(?::bigint[]) as delta)",
^hashes,
^deltas
),
on: token.contract_address_hash == deltas.contract_address_hash,
where: not is_nil(token.holder_count),
# ShareLocks order already enforced by `acquire_contract_address_tokens` (see docs: sharelocks.md)
update: [
set: [
holder_count: token.holder_count + deltas.delta,
updated_at: ^updated_at
]
],
select: %{
contract_address_hash: token.contract_address_hash,
holder_count: token.holder_count
}
)
{_total, result} = repo.update_all(query, [], timeout: timeout)
update_holder_counts_with_parameters(repo, parameters, options)
{:ok, result}
end
@impl Import.Runner
@ -71,7 +121,7 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
changes_list
# brand new tokens start with no holders
|> Stream.map(&Map.put_new(&1, :holder_count, 0))
# order so that row ShareLocks are grabbed in a consistent order
# Enforce Token ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(& &1.contract_address_hash)
{:ok, _} =
@ -117,69 +167,4 @@ defmodule Explorer.Chain.Import.Runner.Tokens do
)
)
end
defp token_holder_count_deltas_to_parameters(token_holder_count_deltas) when is_list(token_holder_count_deltas) do
Enum.flat_map(token_holder_count_deltas, fn
%{contract_address_hash: contract_address_hash, delta: delta} ->
{:ok, contract_address_hash_bytes} = Hash.Address.dump(contract_address_hash)
[contract_address_hash_bytes, delta]
end)
end
defp update_holder_counts_with_parameters(_, [], _), do: {:ok, []}
# sobelow_skip ["SQL.Query"]
defp update_holder_counts_with_parameters(repo, parameters, %{timeout: timeout, timestamps: %{updated_at: updated_at}})
when is_list(parameters) do
update_sql = update_holder_counts_sql(parameters)
with {:ok, %Postgrex.Result{columns: ["contract_address_hash", "holder_count"], command: :update, rows: rows}} <-
SQL.query(repo, update_sql, [updated_at | parameters], timeout: timeout) do
update_token_holder_counts =
Enum.map(rows, fn [contract_address_hash_bytes, holder_count] ->
{:ok, contract_address_hash} = Hash.Address.cast(contract_address_hash_bytes)
%{contract_address_hash: contract_address_hash, holder_count: holder_count}
end)
{:ok, update_token_holder_counts}
end
end
defp update_holder_counts_sql(parameters) when is_list(parameters) do
parameters
|> Enum.count()
|> div(2)
|> update_holder_counts_sql()
end
defp update_holder_counts_sql(row_count) when is_integer(row_count) do
parameters_sql =
update_holder_counts_parameters_sql(
row_count,
# skip $1 as it is used for the common `updated_at` timestamp
2
)
"""
UPDATE tokens
SET holder_count = holder_count + holder_counts.delta,
updated_at = $1
FROM (
VALUES
#{parameters_sql}
) AS holder_counts(contract_address_hash, delta)
WHERE tokens.contract_address_hash = holder_counts.contract_address_hash AND
holder_count IS NOT NULL
RETURNING tokens.contract_address_hash, tokens.holder_count
"""
end
defp update_holder_counts_parameters_sql(row_count, start) when is_integer(row_count) do
Enum.map_join(0..(row_count - 1), ",\n ", fn i ->
contract_address_hash_parameter_number = 2 * i + start
holder_count_number = contract_address_hash_parameter_number + 1
"($#{contract_address_hash_parameter_number}::bytea, $#{holder_count_number}::bigint)"
end)
end
end

@ -58,8 +58,8 @@ defmodule Explorer.Chain.Import.Runner.Transaction.Forks do
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)
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.uncle_hash, &1.hash})
# Enforce Fork ShareLocks order (see docs: sharelocks.md)
ordered_changes_list = Enum.sort_by(changes_list, &{&1.uncle_hash, &1.index})
Import.insert_changes_list(
repo,

@ -42,13 +42,14 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
|> Map.put(:timestamps, timestamps)
|> Map.put(:token_transfer_transaction_hash_set, token_transfer_transaction_hash_set(options))
# Enforce ShareLocks tables order (see docs: sharelocks.md)
multi
|> Multi.run(:recollated_transactions, fn repo, _ ->
discard_blocks_for_recollated_transactions(repo, changes_list, insert_options)
end)
|> Multi.run(:transactions, fn repo, _ ->
insert(repo, changes_list, insert_options)
end)
|> Multi.run(:recollated_transactions, fn repo, %{transactions: transactions} ->
discard_blocks_for_recollated_transactions(repo, transactions, insert_options)
end)
end
@impl Import.Runner
@ -81,7 +82,7 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
ordered_changes_list =
changes_list
|> put_internal_transactions_indexed_at(inserted_at, token_transfer_transaction_hash_set)
# order so that row ShareLocks are grabbed in a consistent order
# Enforce Transaction ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(& &1.hash)
Import.insert_changes_list(
@ -186,41 +187,64 @@ defmodule Explorer.Chain.Import.Runner.Transactions do
defp put_internal_transactions_indexed_at?(_, _), do: false
defp discard_blocks_for_recollated_transactions(repo, transactions, %{
defp discard_blocks_for_recollated_transactions(repo, changes_list, %{
timeout: timeout,
timestamps: %{updated_at: updated_at}
})
when is_list(transactions) do
ordered_block_hashes =
transactions
|> Enum.filter(fn %{block_hash: block_hash, old_block_hash: old_block_hash} ->
not is_nil(old_block_hash) and block_hash != old_block_hash
when is_list(changes_list) do
{transactions_hashes, transactions_block_hashes} =
changes_list
|> Enum.filter(&Map.has_key?(&1, :block_hash))
|> Enum.map(fn %{hash: hash, block_hash: block_hash} ->
{:ok, hash_bytes} = Hash.Full.dump(hash)
{:ok, block_hash_bytes} = Hash.Full.dump(block_hash)
{hash_bytes, block_hash_bytes}
end)
|> MapSet.new(& &1.old_block_hash)
|> Enum.sort()
if Enum.empty?(ordered_block_hashes) do
|> Enum.unzip()
blocks_with_recollated_transactions =
from(
transaction in Transaction,
join:
new_transaction in fragment(
"(SELECT unnest(?::bytea[]) as hash, unnest(?::bytea[]) as block_hash)",
^transactions_hashes,
^transactions_block_hashes
),
on: transaction.hash == new_transaction.hash,
where: transaction.block_hash != new_transaction.block_hash,
select: transaction.block_hash
)
block_hashes =
blocks_with_recollated_transactions
|> repo.all()
|> Enum.uniq()
if Enum.empty?(block_hashes) do
{:ok, []}
else
query =
from(
block in Block,
where: block.hash in ^ordered_block_hashes,
update: [
set: [
consensus: false,
updated_at: ^updated_at
]
]
where: block.hash in ^block_hashes,
# Enforce Block ShareLocks order (see docs: sharelocks.md)
order_by: [asc: block.hash],
lock: "FOR UPDATE"
)
try do
{_, result} = repo.update_all(query, [], timeout: timeout)
{_, result} =
repo.update_all(
from(b in Block, join: s in subquery(query), on: b.hash == s.hash),
[set: [consensus: false, updated_at: updated_at]],
timeout: timeout
)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, block_hashes: ordered_block_hashes}}
{:error, %{exception: postgrex_error, block_hashes: block_hashes}}
end
end
end

@ -47,4 +47,24 @@ defmodule Explorer.Chain.Import.Stage do
runner.run(Multi.new(), changes_chunk, options)
end)
end
@spec single_multi([Runner.t()], runner_to_changes_list, %{optional(atom()) => term()}) ::
{Multi.t(), runner_to_changes_list}
def single_multi(runners, runner_to_changes_list, options) do
runners
|> Enum.reduce({Multi.new(), runner_to_changes_list}, fn runner, {multi, remaining_runner_to_changes_list} ->
{changes_list, new_remaining_runner_to_changes_list} = Map.pop(remaining_runner_to_changes_list, runner)
new_multi =
case changes_list do
nil ->
multi
_ ->
runner.run(multi, changes_list, options)
end
{new_multi, new_remaining_runner_to_changes_list}
end)
end
end

@ -4,7 +4,6 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do
`Explorer.Chain.Import.Stage.Addresses`.
"""
alias Ecto.Multi
alias Explorer.Chain.Import.{Runner, Stage}
@behaviour Stage
@ -14,17 +13,6 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do
do: [
Runner.Address.CoinBalances,
Runner.Blocks,
Runner.Block.Rewards,
Runner.Block.SecondDegreeRelations,
Runner.Transactions,
Runner.Transaction.Forks,
Runner.InternalTransactions,
Runner.InternalTransactionsIndexedAtBlocks,
Runner.Logs,
Runner.Tokens,
Runner.TokenTransfers,
Runner.Address.CurrentTokenBalances,
Runner.Address.TokenBalances,
Runner.StakingPools,
Runner.StakingPoolsDelegators
]
@ -32,21 +20,7 @@ defmodule Explorer.Chain.Import.Stage.AddressReferencing do
@impl Stage
def multis(runner_to_changes_list, options) do
{final_multi, final_remaining_runner_to_changes_list} =
runners()
|> Enum.reduce({Multi.new(), runner_to_changes_list}, fn runner, {multi, remaining_runner_to_changes_list} ->
{changes_list, new_remaining_runner_to_changes_list} = Map.pop(remaining_runner_to_changes_list, runner)
new_multi =
case changes_list do
nil ->
multi
_ ->
runner.run(multi, changes_list, options)
end
{new_multi, new_remaining_runner_to_changes_list}
end)
Stage.single_multi(runners(), runner_to_changes_list, options)
{[final_multi], final_remaining_runner_to_changes_list}
end

@ -0,0 +1,30 @@
defmodule Explorer.Chain.Import.Stage.BlockFollowing do
@moduledoc """
Imports any tables that follows and cannot be imported at the same time as
those imported by `Explorer.Chain.Import.Stage.Addresses`,
`Explorer.Chain.Import.Stage.AddressReferencing` and
`Explorer.Chain.Import.Stage.BlockReferencing`
"""
alias Explorer.Chain.Import.{Runner, Stage}
@behaviour Stage
@impl Stage
def runners,
do: [
Runner.InternalTransactionsIndexedAtBlocks,
Runner.Block.SecondDegreeRelations,
Runner.Block.Rewards,
Runner.InternalTransactions,
Runner.Address.CurrentTokenBalances
]
@impl Stage
def multis(runner_to_changes_list, options) do
{final_multi, final_remaining_runner_to_changes_list} =
Stage.single_multi(runners(), runner_to_changes_list, options)
{[final_multi], final_remaining_runner_to_changes_list}
end
end

@ -0,0 +1,30 @@
defmodule Explorer.Chain.Import.Stage.BlockReferencing do
@moduledoc """
Imports any tables that reference `t:Explorer.Chain.Block.t/0` and that were
imported by `Explorer.Chain.Import.Stage.Addresses` and
`Explorer.Chain.Import.Stage.AddressReferencing`.
"""
alias Explorer.Chain.Import.{Runner, Stage}
@behaviour Stage
@impl Stage
def runners,
do: [
Runner.Transactions,
Runner.Transaction.Forks,
Runner.Logs,
Runner.Tokens,
Runner.TokenTransfers,
Runner.Address.TokenBalances
]
@impl Stage
def multis(runner_to_changes_list, options) do
{final_multi, final_remaining_runner_to_changes_list} =
Stage.single_multi(runners(), runner_to_changes_list, options)
{[final_multi], final_remaining_runner_to_changes_list}
end
end

@ -12,6 +12,8 @@ defmodule Explorer.ChainSpec.Parity.Importer do
alias Explorer.ChainSpec.GenesisData
alias Explorer.ChainSpec.POA.Importer, as: PoaEmissionImporter
import Ecto.Query
@max_block_number :infinity
def import_emission_rewards(chain_spec) do
@ -46,8 +48,27 @@ defmodule Explorer.ChainSpec.Parity.Importer do
defp import_rewards_from_chain_spec(chain_spec) do
rewards = emission_rewards(chain_spec)
{_, nil} = Repo.delete_all(EmissionReward)
{_, nil} = Repo.insert_all(EmissionReward, rewards)
inner_delete_query =
from(
emission_reward in EmissionReward,
# Enforce EmissionReward ShareLocks order (see docs: sharelocks.md)
order_by: emission_reward.block_range,
lock: "FOR UPDATE"
)
delete_query =
from(
e in EmissionReward,
join: s in subquery(inner_delete_query),
# we join on reward because it's faster and we have to delete them all anyway
on: e.reward == s.reward
)
# Enforce EmissionReward ShareLocks order (see docs: sharelocks.md)
ordered_rewards = Enum.sort_by(rewards, & &1.block_range)
{_, nil} = Repo.delete_all(delete_query)
{_, nil} = Repo.insert_all(EmissionReward, ordered_rewards)
end
def genesis_coin_balances(chain_spec) do

@ -11,6 +11,8 @@ defmodule Explorer.ChainSpec.POA.Importer do
alias Explorer.Chain.Block.{EmissionReward, Range}
alias Explorer.ChainSpec.GenesisData
import Ecto.Query
@block_reward_amount_abi %{
"type" => "function",
"stateMutability" => "view",
@ -51,8 +53,27 @@ defmodule Explorer.ChainSpec.POA.Importer do
}
]
{_, nil} = Repo.delete_all(EmissionReward)
{_, nil} = Repo.insert_all(EmissionReward, rewards)
inner_delete_query =
from(
emission_reward in EmissionReward,
# Enforce EmissionReward ShareLocks order (see docs: sharelocks.md)
order_by: emission_reward.block_range,
lock: "FOR UPDATE"
)
delete_query =
from(
e in EmissionReward,
join: s in subquery(inner_delete_query),
# we join on reward because it's faster and we have to delete them all anyway
on: e.reward == s.reward
)
# Enforce EmissionReward ShareLocks order (see docs: sharelocks.md)
ordered_rewards = Enum.sort_by(rewards, & &1.block_range)
{_, nil} = Repo.delete_all(delete_query)
{_, nil} = Repo.insert_all(EmissionReward, ordered_rewards)
end
end

@ -0,0 +1,125 @@
defmodule Explorer.Counters.AddressesCounter do
@moduledoc """
Caches the number of all addresses.
It loads the count asynchronously and in a time interval of 30 minutes.
"""
use GenServer
alias Explorer.Chain
@table :addresses_counter
@cache_key "addresses"
def table_name do
@table
end
def cache_key do
@cache_key
end
# It is undesirable to automatically start the consolidation in all environments.
# Consider the test environment: if the consolidation initiates but does not
# finish before a test ends, that test will fail. This way, hundreds of
# tests were failing before disabling the consolidation and the scheduler in
# the test env.
config = Application.get_env(:explorer, Explorer.Counters.AddressesCounter)
@enable_consolidation Keyword.get(config, :enable_consolidation)
@update_interval_in_seconds Keyword.get(config, :update_interval_in_seconds)
@doc """
Starts a process to periodically update the counter of the token holders.
"""
@spec start_link(term()) :: GenServer.on_start()
def start_link(_) do
GenServer.start_link(__MODULE__, :ok, name: __MODULE__)
end
@impl true
def init(_args) do
create_table()
{:ok, %{consolidate?: enable_consolidation?()}, {:continue, :ok}}
end
def create_table do
opts = [
:set,
:named_table,
:public,
read_concurrency: true
]
:ets.new(table_name(), opts)
end
defp schedule_next_consolidation do
Process.send_after(self(), :consolidate, :timer.seconds(@update_interval_in_seconds))
end
@doc """
Inserts new items into the `:ets` table.
"""
def insert_counter({key, info}) do
:ets.insert(table_name(), {key, info})
end
@impl true
def handle_continue(:ok, %{consolidate?: true} = state) do
consolidate()
schedule_next_consolidation()
{:noreply, state}
end
@impl true
def handle_continue(:ok, state) do
{:noreply, state}
end
@impl true
def handle_info(:consolidate, state) do
consolidate()
schedule_next_consolidation()
{:noreply, state}
end
@doc """
Fetches the info for a specific item from the `:ets` table.
"""
def fetch do
do_fetch(:ets.lookup(table_name(), cache_key()))
end
defp do_fetch([{_, result}]), do: result
defp do_fetch([]), do: 0
@doc """
Consolidates the info by populating the `:ets` table with the current database information.
"""
def consolidate do
counter = Chain.count_addresses()
insert_counter({cache_key(), counter})
end
@doc """
Returns a boolean that indicates whether consolidation is enabled
In order to choose whether or not to enable the scheduler and the initial
consolidation, change the following Explorer config:
`config :explorer, Explorer.Counters.AddressesCounter, enable_consolidation: true`
to:
`config :explorer, Explorer.Counters.AddressesCounter, enable_consolidation: false`
"""
def enable_consolidation?, do: @enable_consolidation
end

@ -26,7 +26,7 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
[
%Token{
available_supply: to_decimal(market_data["circulating_supply"]),
total_supply: to_decimal(market_data["total_supply"]),
total_supply: to_decimal(market_data["total_supply"]) || to_decimal(market_data["circulating_supply"]),
btc_value: btc_value,
id: json_data["id"],
last_updated: last_updated,
@ -44,15 +44,41 @@ defmodule Explorer.ExchangeRates.Source.CoinGecko do
@impl Source
def source_url do
"#{base_url()}/coins/#{coin_id()}"
{:ok, id} = coin_id()
"#{base_url()}/coins/#{id}"
end
defp base_url do
config(:base_url) || "https://api.coingecko.com/api/v3"
end
defp coin_id do
Application.get_env(:explorer, __MODULE__)[:coin_id]
def coin_id do
url = "#{base_url()}/coins/list"
symbol = String.downcase(Explorer.coin())
case HTTPoison.get(url, headers()) do
{:ok, %Response{body: body, status_code: 200}} ->
data = decode_json(body)
symbol_data =
Enum.find(data, fn item ->
item["symbol"] == symbol
end)
if symbol_data do
{:ok, symbol_data["id"]}
else
{:error, :not_found}
end
{:ok, %Response{body: body, status_code: status_code}} when status_code in 400..499 ->
{:error, decode_json(body)["error"]}
{:error, %Error{reason: reason}} ->
{:error, reason}
end
end
defp get_btc_price(currency \\ "usd") do

@ -41,9 +41,12 @@ defmodule Explorer.Market do
@doc false
def bulk_insert_history(records) do
records_without_zeroes =
Enum.reject(records, fn item ->
records
|> Enum.reject(fn item ->
Decimal.equal?(item.closing_price, 0) && Decimal.equal?(item.opening_price, 0)
end)
# Enforce MarketHistory ShareLocks order (see docs: sharelocks.md)
|> Enum.sort_by(& &1.date)
Repo.insert_all(MarketHistory, records_without_zeroes, on_conflict: :nothing, conflict_target: [:date])
end

@ -69,7 +69,8 @@ defmodule Explorer.SmartContract.Verifier do
blockchain_bytecode_without_whisper = extract_bytecode(blockchain_bytecode)
cond do
generated_bytecode != blockchain_bytecode_without_whisper ->
generated_bytecode != blockchain_bytecode_without_whisper &&
!try_library_verification(generated_bytecode, blockchain_bytecode_without_whisper) ->
{:error, :generated_bytecode}
has_constructor_with_params?(abi) &&
@ -81,6 +82,18 @@ defmodule Explorer.SmartContract.Verifier do
end
end
# 730000000000000000000000000000000000000000 - default library address that returned by the compiler
defp try_library_verification(
"730000000000000000000000000000000000000000" <> bytecode,
<<_address::binary-size(42)>> <> bytecode
) do
true
end
defp try_library_verification(_, _) do
false
end
@doc """
In order to discover the bytecode we need to remove the `swarm source` from
the hash.

@ -8,7 +8,10 @@ defmodule Explorer.Validator.MetadataImporter do
import Ecto.Query, only: [from: 2]
def import_metadata(metadata_maps) do
Repo.transaction(fn -> Enum.each(metadata_maps, &upsert_validator_metadata(&1)) end)
# Enforce Name ShareLocks order (see docs: sharelocks.md)
ordered_metadata_maps = Enum.sort_by(metadata_maps, &{&1.address_hash, &1.name})
Repo.transaction(fn -> Enum.each(ordered_metadata_maps, &upsert_validator_metadata(&1)) end)
end
defp upsert_validator_metadata(validator_changeset) do

@ -0,0 +1,7 @@
defmodule Explorer.Repo.Migrations.CreateIndexesForBlockNumberInTokenTransfersAndTransactions do
use Ecto.Migration
def change do
create_if_not_exists(index(:token_transfers, [:block_number]))
end
end

@ -0,0 +1,42 @@
defmodule Explorer.Chain.Cache.AccountsTest do
use Explorer.DataCase
alias Explorer.Chain.Cache.Accounts
alias Explorer.Repo
describe "drop/1" do
test "does not drop the cache if the address fetched_coin_balance has not changed" do
address =
insert(:address, fetched_coin_balance: 100_000, fetched_coin_balance_block_number: 1)
|> preload_names()
Accounts.update(address)
assert Accounts.take(1) == [address]
Accounts.drop(address)
assert Accounts.take(1) == [address]
end
test "drops the cache if an address was in the cache with a different fetched_coin_balance" do
address =
insert(:address, fetched_coin_balance: 100_000, fetched_coin_balance_block_number: 1)
|> preload_names()
Accounts.update(address)
assert Accounts.take(1) == [address]
updated_address = %{address | fetched_coin_balance: 100_001}
Accounts.drop(updated_address)
assert Accounts.take(1) == []
end
end
defp preload_names(address) do
Repo.preload(address, [:names])
end
end

@ -0,0 +1,69 @@
defmodule Explorer.Chain.Import.Runner.AddressesTest do
use Explorer.DataCase
alias Ecto.Multi
alias Explorer.Chain.{Address, Wei}
alias Explorer.Chain.Import.Runner.Addresses
alias Explorer.Repo
describe "run/1" do
test "does not update fetched_coin_balance if original value is not nil but new value is nil" do
block_number = 5
original_address = insert(:address, fetched_coin_balance: 5, fetched_coin_balance_block_number: block_number)
new_params = %{
fetched_coin_balance: nil,
fetched_coin_balance_block_number: block_number,
hash: to_string(original_address.hash)
}
changeset = Address.balance_changeset(%Address{}, new_params)
wei = original_address.fetched_coin_balance
assert {:ok,
%{
addresses: [
%Address{
fetched_coin_balance: ^wei,
fetched_coin_balance_block_number: 5
}
]
}} = run([changeset.changes])
end
test "updates fetched_coin_balance if original value is nil and new value is not nil" do
block_number = 5
original_address = insert(:address, fetched_coin_balance: nil, fetched_coin_balance_block_number: block_number)
new_params = %{
fetched_coin_balance: 5,
fetched_coin_balance_block_number: block_number,
hash: to_string(original_address.hash)
}
changeset = Address.balance_changeset(%Address{}, new_params)
wei = %Wei{value: Decimal.new(new_params.fetched_coin_balance)}
assert {:ok,
%{
addresses: [
%Address{
fetched_coin_balance: ^wei,
fetched_coin_balance_block_number: 5
}
]
}} = run([changeset.changes])
end
end
defp run(changes) do
timestamp = DateTime.utc_now()
options = %{timestamps: %{inserted_at: timestamp, updated_at: timestamp}}
Multi.new()
|> Addresses.run(changes, options)
|> Repo.transaction()
end
end

@ -7,13 +7,13 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
alias Ecto.Multi
alias Explorer.Chain.Import.Runner.{Blocks, Transactions}
alias Explorer.Chain.{Address, Block, Transaction}
alias Explorer.Chain
alias Explorer.Repo
alias Explorer.Chain.{Address, Block, InternalTransaction, Log, Transaction, TokenTransfer}
alias Explorer.{Chain, Repo}
describe "run/1" do
setup do
block = insert(:block, consensus: true)
miner = insert(:address)
block = params_for(:block, consensus: true, miner_hash: miner.hash)
timestamp = DateTime.utc_now()
options = %{timestamps: %{inserted_at: timestamp, updated_at: timestamp}}
@ -22,9 +22,11 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
end
test "derive_transaction_forks replaces hash on conflicting (uncle_hash, index)", %{
consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number} = consensus_block,
consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number},
options: options
} do
consensus_block = insert(:block, %{hash: block_hash, number: block_number})
transaction =
:transaction
|> insert()
@ -81,7 +83,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
end
test "delete_address_current_token_balances deletes rows with matching block number when consensus is true",
%{consensus_block: %Block{number: block_number} = block, options: options} do
%{consensus_block: %{number: block_number} = block, options: options} do
%Address.CurrentTokenBalance{address_hash: address_hash, token_contract_address_hash: token_contract_address_hash} =
insert(:address_current_token_balance, block_number: block_number)
@ -98,7 +100,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
end
test "delete_address_current_token_balances does not delete rows with matching block number when consensus is false",
%{consensus_block: %Block{number: block_number} = block, options: options} do
%{consensus_block: %{number: block_number} = block, options: options} do
%Address.CurrentTokenBalance{} = insert(:address_current_token_balance, block_number: block_number)
count = 1
@ -113,8 +115,80 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
assert count(Address.CurrentTokenBalance) == count
end
test "remove_nonconsensus_token_transfers deletes token transfer rows with matching block number when new consensus block is inserted",
%{consensus_block: %{number: block_number} = block, options: options} do
consensus_block = insert(:block, number: block_number, consensus: true)
transaction = insert(:transaction) |> with_block(consensus_block)
%TokenTransfer{transaction_hash: transaction_hash, log_index: log_index} =
insert(:token_transfer, block_number: block_number, transaction: transaction)
assert count(TokenTransfer) == 1
assert {:ok,
%{
remove_nonconsensus_token_transfers: [
%{transaction_hash: ^transaction_hash, log_index: ^log_index}
]
}} = run_block_consensus_change(block, true, options)
assert count(TokenTransfer) == 0
end
test "remove_nonconsensus_token_transfers does not delete token transfer rows with matching block number when new consensus block wasn't inserted",
%{consensus_block: %{number: block_number} = block, options: options} do
consensus_block = insert(:block, number: block_number, consensus: true)
transaction = insert(:transaction) |> with_block(consensus_block)
insert(:token_transfer, block_number: block_number, transaction: transaction)
count = 1
assert count(TokenTransfer) == count
assert {:ok, %{remove_nonconsensus_token_transfers: []}} = run_block_consensus_change(block, false, options)
assert count(TokenTransfer) == count
end
test "remove_nonconsensus_logs deletes nonconsensus logs", %{
consensus_block: %{number: block_number} = block,
options: options
} do
old_block = insert(:block, number: block_number, consensus: true)
forked_transaction = :transaction |> insert() |> with_block(old_block)
%Log{transaction_hash: hash, index: index} = insert(:log, transaction: forked_transaction)
assert count(Log) == 1
assert {:ok, %{remove_nonconsensus_logs: [%{transaction_hash: ^hash, index: ^index}]}} =
run_block_consensus_change(block, true, options)
assert count(Log) == 0
end
test "remove_nonconsensus_internal_transactions deletes nonconsensus internal transactions", %{
consensus_block: %{number: block_number} = block,
options: options
} do
old_block = insert(:block, number: block_number, consensus: true)
forked_transaction = :transaction |> insert() |> with_block(old_block)
%InternalTransaction{index: index, transaction_hash: hash} =
insert(:internal_transaction, index: 0, transaction: forked_transaction)
assert count(InternalTransaction) == 1
assert {:ok, %{remove_nonconsensus_internal_transactions: [%{transaction_hash: ^hash, index: ^index}]}} =
run_block_consensus_change(block, true, options)
assert count(InternalTransaction) == 0
end
test "derive_address_current_token_balances inserts rows if there is an address_token_balance left for the rows deleted by delete_address_current_token_balances",
%{consensus_block: %Block{number: block_number} = block, options: options} do
%{consensus_block: %{number: block_number} = block, options: options} do
token = insert(:token)
token_contract_address_hash = token.contract_address_hash
@ -172,7 +246,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
end
test "a non-holder reverting to a holder increases the holder_count",
%{consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do
%{consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do
token = insert(:token)
token_contract_address_hash = token.contract_address_hash
@ -204,7 +278,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
end
test "a holder reverting to a non-holder decreases the holder_count",
%{consensus_block: %Block{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do
%{consensus_block: %{hash: block_hash, miner_hash: miner_hash, number: block_number}, options: options} do
token = insert(:token)
token_contract_address_hash = token.contract_address_hash
@ -236,7 +310,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
end
test "a non-holder becoming and a holder becoming while a holder becomes a non-holder cancels out and holder_count does not change",
%{consensus_block: %Block{number: block_number} = block, options: options} do
%{consensus_block: %{number: block_number} = block, options: options} do
token = insert(:token)
token_contract_address_hash = token.contract_address_hash
@ -262,7 +336,8 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
# Regression test for https://github.com/poanetwork/blockscout/issues/1644
test "discards neighbouring blocks if they aren't related to the current one because of reorg and/or import timeout",
%{consensus_block: %Block{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do
%{consensus_block: %{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do
insert(:block, %{number: block_number, hash: block_hash})
old_block1 = params_for(:block, miner_hash: miner_hash, parent_hash: block_hash, number: block_number + 1)
new_block1 = params_for(:block, miner_hash: miner_hash, parent_hash: block_hash, number: block_number + 1)
@ -286,7 +361,8 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
# Regression test for https://github.com/poanetwork/blockscout/issues/1911
test "forces block refetch if transaction is re-collated in a different block",
%{consensus_block: %Block{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do
%{consensus_block: %{number: block_number, hash: block_hash, miner_hash: miner_hash}, options: options} do
insert(:block, %{number: block_number, hash: block_hash})
new_block1 = params_for(:block, miner_hash: miner_hash, parent_hash: block_hash, number: block_number + 1)
new_block2 = params_for(:block, miner_hash: miner_hash, parent_hash: new_block1.hash, number: block_number + 2)
@ -365,7 +441,7 @@ defmodule Explorer.Chain.Import.Runner.BlocksTest do
end
defp run_block_consensus_change(
%Block{hash: block_hash, miner_hash: miner_hash, number: block_number},
%{hash: block_hash, miner_hash: miner_hash, number: block_number},
consensus,
options
) do

@ -27,6 +27,7 @@ defmodule Explorer.ChainTest do
alias Explorer.Chain.Supply.ProofOfAuthority
alias Explorer.Counters.AddressesWithBalanceCounter
alias Explorer.Counters.AddressesCounter
doctest Explorer.Chain
@ -50,6 +51,22 @@ defmodule Explorer.ChainTest do
end
end
describe "count_addresses_from_cache/0" do
test "returns the number of all addresses" do
insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
addresses_with_balance = Chain.count_addresses_from_cache()
assert is_integer(addresses_with_balance)
assert addresses_with_balance == 3
end
end
describe "last_db_block_status/0" do
test "return no_blocks errors if db is empty" do
assert {:error, :no_blocks} = Chain.last_db_block_status()
@ -3879,9 +3896,9 @@ defmodule Explorer.ChainTest do
address = insert(:address)
today = NaiveDateTime.utc_now()
noon = Timex.set(today, hour: 12)
block = insert(:block, timestamp: noon)
block = insert(:block, timestamp: noon, number: 50)
yesterday = Timex.shift(noon, days: -1)
block_one_day_ago = insert(:block, timestamp: yesterday)
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)
@ -3908,6 +3925,22 @@ defmodule Explorer.ChainTest do
%{date: today |> NaiveDateTime.to_date() |> Date.to_string(), value: Decimal.new("1E-15")}
]
end
test "uses last block value if there a couple of change in the same day" do
address = insert(:address)
today = NaiveDateTime.utc_now()
past = Timex.shift(today, hours: -1)
block_now = insert(:block, timestamp: today, number: 1)
insert(:fetched_balance, address_hash: address.hash, value: 1, block_number: block_now.number)
block_past = insert(:block, timestamp: past, number: 2)
insert(:fetched_balance, address_hash: address.hash, value: 0, block_number: block_past.number)
[balance] = Chain.address_to_balances_by_day(address.hash)
assert balance.value == Decimal.new(0)
end
end
describe "block_combined_rewards/1" do

@ -0,0 +1,16 @@
defmodule Explorer.Counters.AddressesCounterTest do
use Explorer.DataCase
alias Explorer.Counters.AddressesCounter
test "populates the cache with the number of all addresses" do
insert(:address, fetched_coin_balance: 0)
insert(:address, fetched_coin_balance: 1)
insert(:address, fetched_coin_balance: 2)
start_supervised!(AddressesCounter)
AddressesCounter.consolidate()
assert AddressesCounter.fetch() == 3
end
end

@ -18,6 +18,46 @@ defmodule Explorer.ExchangeRates.Source.CoinGeckoTest do
}
"""
@coins_list """
[
{
"id": "poa-network",
"symbol": "poa",
"name": "POA Network"
},
{
"id": "poc-chain",
"symbol": "pocc",
"name": "POC Chain"
},
{
"id": "pocket-arena",
"symbol": "poc",
"name": "Pocket Arena"
},
{
"id": "ethereum",
"symbol": "eth",
"name": "Ethereum"
},
{
"id": "rootstock",
"symbol": "rbtc",
"name": "Rootstock RSK"
},
{
"id": "dai",
"symbol": "dai",
"name": "Dai"
},
{
"id": "callisto",
"symbol": "clo",
"name": "Callisto Network"
}
]
"""
describe "format_data/1" do
setup do
bypass = Bypass.open()
@ -62,4 +102,65 @@ defmodule Explorer.ExchangeRates.Source.CoinGeckoTest do
assert [] = CoinGecko.format_data(bad_data)
end
end
describe "coin_id/0" do
setup do
bypass = Bypass.open()
Application.put_env(:explorer, CoinGecko, base_url: "http://localhost:#{bypass.port}")
on_exit(fn ->
Application.put_env(:explorer, :coin, "POA")
end)
{:ok, bypass: bypass}
end
test "fetches poa coin id by default", %{bypass: bypass} do
Bypass.expect(bypass, "GET", "/coins/list", fn conn ->
Conn.resp(conn, 200, @coins_list)
end)
assert CoinGecko.coin_id() == {:ok, "poa-network"}
end
test "fetches eth coin id", %{bypass: bypass} do
Application.put_env(:explorer, :coin, "ETH")
Bypass.expect(bypass, "GET", "/coins/list", fn conn ->
Conn.resp(conn, 200, @coins_list)
end)
assert CoinGecko.coin_id() == {:ok, "ethereum"}
end
test "fetches rbtc coin id", %{bypass: bypass} do
Application.put_env(:explorer, :coin, "RBTC")
Bypass.expect(bypass, "GET", "/coins/list", fn conn ->
Conn.resp(conn, 200, @coins_list)
end)
assert CoinGecko.coin_id() == {:ok, "rootstock"}
end
test "fetches dai coin id", %{bypass: bypass} do
Application.put_env(:explorer, :coin, "DAI")
Bypass.expect(bypass, "GET", "/coins/list", fn conn ->
Conn.resp(conn, 200, @coins_list)
end)
assert CoinGecko.coin_id() == {:ok, "dai"}
end
test "fetches callisto coin id", %{bypass: bypass} do
Application.put_env(:explorer, :coin, "CLO")
Bypass.expect(bypass, "GET", "/coins/list", fn conn ->
Conn.resp(conn, 200, @coins_list)
end)
assert CoinGecko.coin_id() == {:ok, "callisto"}
end
end
end

@ -144,6 +144,34 @@ defmodule Explorer.SmartContract.VerifierTest do
assert abi != nil
end
test "verifies a library" do
bytecode =
"0x7349f540c22cba15c47a08c235e20081474201a742301460806040526004361060335760003560e01c8063c2985578146038575b600080fd5b603e60b0565b6040805160208082528351818301528351919283929083019185019080838360005b8381101560765781810151838201526020016060565b50505050905090810190601f16801560a25780820380516001836020036101000a031916815260200191505b509250505060405180910390f35b604080518082019091526003815262666f6f60e81b60208201529056fea265627a7a72315820174b282a3ef3b9778d79fbc2e4c36bc939c54dfaaaa51d3122ee6e648093844c64736f6c634300050b0032"
contract_address = insert(:contract_address, contract_code: bytecode)
code = """
pragma solidity 0.5.11;
library Foo {
function foo() external pure returns (string memory) {
return "foo";
}
}
"""
params = %{
"contract_source_code" => code,
"compiler_version" => "v0.5.11+commit.c082d0b4",
"evm_version" => "default",
"name" => "Foo",
"optimization" => true
}
assert {:ok, %{abi: abi}} = Verifier.evaluate_authenticity(contract_address.hash, params)
assert abi != nil
end
test "verifies smart contract compiled with Solidity 0.5.9 (includes new metadata in bytecode) with constructor args" do
path = File.cwd!() <> "/test/support/fixture/smart_contract/solidity_0.5.9_smart_contract.sol"
contract = File.read!(path)

@ -45,6 +45,8 @@ defmodule Explorer.DataCase do
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Blocks.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Transactions.child_id())
Supervisor.terminate_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
Supervisor.restart_child(Explorer.Supervisor, Explorer.Chain.Cache.Accounts.child_id())
:ok
end

@ -13,7 +13,7 @@ defmodule Indexer.Block.Fetcher do
alias Explorer.Chain
alias Explorer.Chain.{Address, Block, Hash, Import, Transaction}
alias Explorer.Chain.Cache.Blocks, as: BlocksCache
alias Explorer.Chain.Cache.{BlockNumber, Transactions}
alias Explorer.Chain.Cache.{Accounts, BlockNumber, Transactions}
alias Indexer.Block.Fetcher.Receipts
alias Indexer.Fetcher.{
@ -176,6 +176,7 @@ defmodule Indexer.Block.Fetcher do
result = {:ok, %{inserted: inserted, errors: blocks_errors}}
update_block_cache(inserted[:blocks])
update_transactions_cache(inserted[:transactions])
update_addresses_cache(inserted[:addresses])
result
else
{step, {:error, reason}} -> {:error, {step, reason}}
@ -183,6 +184,8 @@ defmodule Indexer.Block.Fetcher do
end
end
defp update_block_cache([]), do: :ok
defp update_block_cache(blocks) when is_list(blocks) do
{min_block, max_block} = Enum.min_max_by(blocks, & &1.number)
@ -197,6 +200,8 @@ defmodule Indexer.Block.Fetcher do
Transactions.update(transactions)
end
defp update_addresses_cache(addresses), do: Accounts.drop(addresses)
def import(
%__MODULE__{broadcast: broadcast, callback_module: callback_module} = state,
options

@ -27,6 +27,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
alias Ecto.Changeset
alias EthereumJSONRPC.{FetchedBalances, Subscription}
alias Explorer.Chain
alias Explorer.Chain.Cache.Accounts
alias Explorer.Counters.AverageBlockTime
alias Indexer.{Block, Tracer}
alias Indexer.Block.Realtime.TaskSupervisor
@ -197,6 +198,8 @@ defmodule Indexer.Block.Realtime.Fetcher do
json_rpc_named_arguments
)
Accounts.drop(imported[:addresses])
ok
end
end

@ -17,6 +17,7 @@ defmodule Indexer.Fetcher.BlockReward do
alias EthereumJSONRPC.FetchedBeneficiaries
alias Explorer.Chain
alias Explorer.Chain.{Block, Wei}
alias Explorer.Chain.Cache.Accounts
alias Indexer.{BufferedTask, Tracer}
alias Indexer.Fetcher.BlockReward.Supervisor, as: BlockRewardSupervisor
alias Indexer.Fetcher.CoinBalance
@ -130,7 +131,9 @@ defmodule Indexer.Fetcher.BlockReward do
|> add_gas_payments()
|> import_block_reward_params()
|> case do
{:ok, %{address_coin_balances: address_coin_balances}} ->
{:ok, %{address_coin_balances: address_coin_balances, addresses: addresses}} ->
Accounts.drop(addresses)
CoinBalance.async_fetch_balances(address_coin_balances)
retry_errors(errors)

@ -14,6 +14,7 @@ defmodule Indexer.Fetcher.CoinBalance do
alias EthereumJSONRPC.FetchedBalances
alias Explorer.Chain
alias Explorer.Chain.{Block, Hash}
alias Explorer.Chain.Cache.Accounts
alias Indexer.{BufferedTask, Tracer}
@behaviour BufferedTask
@ -136,7 +137,9 @@ defmodule Indexer.Fetcher.CoinBalance do
end
defp run_fetched_balances(%FetchedBalances{errors: errors} = fetched_balances, _) do
{:ok, _} = import_fetched_balances(fetched_balances)
{:ok, imported} = import_fetched_balances(fetched_balances)
Accounts.drop(imported[:addresses])
retry(errors)
end

@ -19,7 +19,7 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
alias Explorer.{Chain, Repo}
alias Explorer.Chain.Address
alias Explorer.Chain.Address.CoinBalance
alias Explorer.Chain.Cache.BlockNumber
alias Explorer.Chain.Cache.{Accounts, BlockNumber}
alias Explorer.Counters.AverageBlockTime
alias Indexer.Fetcher.CoinBalance, as: CoinBalanceFetcher
alias Timex.Duration
@ -71,7 +71,11 @@ defmodule Indexer.Fetcher.CoinBalanceOnDemand do
end
def handle_cast({:fetch_and_update, block_number, address}, state) do
fetch_and_update(block_number, address, state.json_rpc_named_arguments)
result = fetch_and_update(block_number, address, state.json_rpc_named_arguments)
with {:ok, %{addresses: addresses}} <- result do
Accounts.drop(addresses)
end
{:noreply, state}
end

@ -12,6 +12,7 @@ defmodule Indexer.Fetcher.ContractCode do
alias Explorer.Chain
alias Explorer.Chain.{Block, Hash}
alias Explorer.Chain.Cache.Accounts
alias Indexer.{BufferedTask, Tracer}
alias Indexer.Transform.Addresses
@ -126,7 +127,8 @@ defmodule Indexer.Fetcher.ContractCode do
addresses: %{params: merged_addresses_params},
timeout: :infinity
}) do
{:ok, _} ->
{:ok, imported} ->
Accounts.drop(imported[:addresses])
:ok
{:error, step, reason, _changes_so_far} ->

@ -14,6 +14,7 @@ defmodule Indexer.Fetcher.InternalTransaction do
alias Explorer.Chain
alias Explorer.Chain.{Block, Hash}
alias Explorer.Chain.Cache.Accounts
alias Indexer.{BufferedTask, Tracer}
alias Indexer.Transform.Addresses
@ -218,6 +219,8 @@ defmodule Indexer.Fetcher.InternalTransaction do
case imports do
{:ok, imported} ->
Accounts.drop(imported[:addreses])
async_import_coin_balances(imported, %{
address_hash_to_fetched_balance_block_number: address_hash_to_block_number
})

@ -14,6 +14,7 @@ defmodule Indexer.Fetcher.PendingTransaction do
alias Ecto.Changeset
alias Explorer.Chain
alias Explorer.Chain.Cache.Accounts
alias Indexer.Fetcher.PendingTransaction
alias Indexer.Transform.Addresses
@ -148,7 +149,8 @@ defmodule Indexer.Fetcher.PendingTransaction do
broadcast: :realtime,
transactions: %{params: transactions_params, on_conflict: :nothing}
}) do
{:ok, _} ->
{:ok, imported} ->
Accounts.drop(imported[:addresses])
:ok
{:error, [%Changeset{} | _] = changesets} ->

@ -12,6 +12,7 @@ defmodule Indexer.Fetcher.UncleBlock do
alias Ecto.Changeset
alias EthereumJSONRPC.Blocks
alias Explorer.Chain
alias Explorer.Chain.Cache.Accounts
alias Explorer.Chain.Hash
alias Indexer.{Block, BufferedTask, Tracer}
alias Indexer.Fetcher.UncleBlock
@ -126,7 +127,8 @@ defmodule Indexer.Fetcher.UncleBlock do
block_second_degree_relations: %{params: block_second_degree_relations_params},
transactions: %{params: transactions_params, on_conflict: :nothing}
}) do
{:ok, _} ->
{:ok, imported} ->
Accounts.drop(imported[:addresses])
retry(errors)
{:error, {:import = step, [%Changeset{} | _] = changesets}} ->

@ -105,19 +105,15 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
end)
unless Enum.empty?(matching_blocks_data) do
hashes = Enum.map(matching_blocks_data, fn {hash, _trans_num} -> hash end)
Block
|> where([block], block.hash in ^hashes)
|> Repo.update_all(set: [refetch_needed: false])
matching_blocks_data
|> Enum.map(fn {hash, _trans_num} -> hash end)
|> update_in_order(refetch_needed: false)
end
unless Enum.empty?(unmatching_blocks_data) do
hashes = Enum.map(unmatching_blocks_data, fn {hash, _trans_num} -> hash end)
Block
|> where([block], block.hash in ^hashes)
|> Repo.update_all(set: [refetch_needed: false, consensus: false])
unmatching_blocks_data
|> Enum.map(fn {hash, _trans_num} -> hash end)
|> update_in_order(refetch_needed: false, consensus: false)
end
if Enum.empty?(missing_blocks_data) do
@ -126,4 +122,19 @@ defmodule Indexer.Temporary.BlocksTransactionsMismatch do
{:retry, missing_blocks_data}
end
end
defp update_in_order(hashes, fields_to_set) do
query =
from(block in Block,
where: block.hash in ^hashes,
# Enforce Block ShareLocks order (see docs: sharelocks.md)
order_by: [asc: block.hash],
lock: "FOR UPDATE"
)
Repo.update_all(
from(b in Block, join: s in subquery(query), on: b.hash == s.hash),
set: fields_to_set
)
end
end

@ -84,18 +84,6 @@ defmodule Indexer.Block.Fetcher.ReceiptsTest do
"transactionIndex" => "0x0",
"transactionLogIndex" => "0x0",
"type" => "mined"
},
%{
"address" => "0x8bf38d4764929064f2d4d3a56520a76ab3df415c",
"blockHash" => nil,
"blockNumber" => nil,
"data" => "0x000000000000000000000000862d67cb0773ee3f8ce7ea89b328ffea861ab3ef",
"logIndex" => "0x1",
"topics" => ["0x600bcf04a13e752d1e3670a5a9f1c21177ca2a93c6f5391d4f1298d098097c22"],
"transactionHash" => "0x53bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5",
"transactionIndex" => "0x0",
"transactionLogIndex" => "0x0",
"type" => "pending"
}
],
"logsBloom" =>
@ -158,8 +146,6 @@ defmodule Indexer.Block.Fetcher.ReceiptsTest do
log[:transaction_hash] == "0x43bd884872de3e488692881baeec262e7b95234d3965248c39fe992fffd433e5" &&
log[:block_number] == 46147
end)
refute Enum.find(logs, fn log -> log[:type] == "pending" end)
end
end
end

@ -56,6 +56,7 @@ $ export NETWORK=POA
| `SUPPORTED_CHAINS` | | Array of supported chains that displays in the footer and in the chains dropdown. This var was introduced in this PR [#1900](https://github.com/poanetwork/blockscout/pull/1900) and looks like an array of JSON objects. | (empty) | v2.0.0+ | | |
| `BLOCK_COUNT_CACHE_PERIOD ` | | time to live of cache in seconds. This var was introduced in [#1876](https://github.com/poanetwork/blockscout/pull/1876) | 600 | v2.0.0+ | | |
| `ALLOWED_EVM_VERSIONS ` | | the comma-separated list of allowed EVM versions for contracts verification. This var was introduced in [#1964](https://github.com/poanetwork/blockscout/pull/1964) | "homestead, tangerineWhistle, spuriousDragon, byzantium, constantinople, petersburg" | v2.0.0+ | | |
| `UNCLES_IN_AVERAGE_BLOCK_TIME` | Include or exclude nonconsensus blocks in avg block time calculation. Exclude if `false`. | false | v2.0.1+ | | |
| `AVERAGE_BLOCK_CACHE_PERIOD` | | Update of average block cache, in seconds | 30 minutes | v2.0.2+ | |
| `MARKET_HISTORY_CACHE_PERIOD` | | Update of market history cache, in seconds | 6 hours | v2.0.2+ | |
| `DISABLE_WEBAPP` | | If `true`, endpoints to webapp are hidden (compile-time) | `false` | v2.0.3+ | :white_check_mark: | |
@ -65,6 +66,6 @@ $ export NETWORK=POA
| `WEBAPP_URL` | | Link to web application instance, e.g. `http://host/path` | (empty) | v2.0.3+ | | |
| `API_URL` | | Link to API instance, e.g. `http://host/path` | (empty) | v2.0.3+ | | |
| `CHAIN_SPEC_PATH` | | Chain specification path (absolute file system path or url) to import block emission reward ranges and genesis account balances from | (empty) | v2.0.4+ | | |
| `COIN_GECKO_ID` | | CoinGecko coin id required for fetching an exchange rate | poa-network | v2.0.4+ | | |
| `COIN_GECKO_ID` | | CoinGecko coin id required for fetching an exchange rate | poa-network | v2.0.4+ | | master |
| `EMISSION_FORMAT` | | Should be set to `POA` if you have block emission indentical to POA Network. This env var is used only if `CHAIN_SPEC_PATH` is set | `STANDARD` | v2.0.4+ | | |
| `REWARDS_CONTRACT_ADDRESS` | | Emission rewards contract address. This env var is used only if `EMISSION_FORMAT` is set to `POA` | `0xeca443e8e1ab29971a45a9c57a6a9875701698a5` | v2.0.4+ | | |

@ -0,0 +1,137 @@
<!--sharelocks.md -->
## ShareLocks
ShareLock is the row-level locking mechanism used internally by PostgreSQL.
### Deadlocks and prevention
When several DB transactions are acting on multiple rows of the same table, it's
possible to incur in a deadlock and so into an error.
This can be prevented by enforcing the same consistent order of lock aquisition
on *all* the transactions performing `INSERT`, `UPDATE` or `DELETE` on a given table.
On top of this, if multiple DB transactions act on multiple tables a deadlock
will occur, even if they follow the order on each table described above, if they
acquire locks on said tables in a different order.
This can also be prevented by using a consisten order of lock acquisition *between*
different tables.
### Imposing the lock acquisition order on a table with Ecto
When `INSERT`ing a list of rows Postgres will respect the order in which they
appear in the query, so the reordering can happen beforehand.
For example, this will work:
```elixir
entries = [...]
ordered_entries = Enum.sort_by(entries, & &1.id)
Repo.insert_all(__MODULE__, ordered_entries)
```
Performing `UPDATE`s is trickier because there is no `ORDER BY` clause.
The solution to this is to `JOIN` on a subquery that `SELECT`s with the option `FOR UPDATE`.
Using Ecto this can be done, for example, like this:
```elixir
query =
from(
entry in Entry,
where: not is_nil(entry.value),
order_by: entry.id,
lock: "FOR UPDATE"
)
Repo.update_all(
from(e in Entry, join: s in subquery(query), on: e.id == s.id),
[set: [value: nil]],
timeout: timeout)
```
`DELETE` has the same quircks as `UPDATE` and it is too solved in the same way.
For example:
```elixir
query =
from(
entry in Entry,
where: is_nil(entry.value),
order_by: entry.id,
lock: "FOR UPDATE"
)
Repo.delete_all(from(e in Entry, join: s in subquery(query), on: e.id == s.id))
```
### Imposing the lock acquisition order between tables with Ecto
When using an `Ecto.Multi` to perform `INSERT`, `UPDATE` or `DELETE` on multiple
tables the order to keep is between different operation.
For example, supposing `EntryA` was established to be modified before `EntryB`,
this is not correct:
```elixir
Multi.new()
|> Multi.run(:update_b, fn repo, _ ->
# operations with ordered locks on `EntryB`
end)
|> Multi.run(:update_a, fn repo, _ ->
# operations with ordered locks on `EntryA`
end)
|> Repo.transaction()
```
When possible, the simple solution is to move `:update_a` to be before `:update_b`.
When not possible, for instance if `:update_a` depends on the result of `:update_b`,
this can be solved by acquiring the locks in a separate operation.
For example:
```elixir
Multi.new()
|> Multi.run(:acquire_a, fn repo, _ ->
# acquire locks in order on `EntryA`
end)
|> Multi.run(:update_b, fn repo, _ ->
# operations with ordered locks on `EntryB`
end)
|> Multi.run(:update_a, fn repo, %{acquire_a: values} ->
# operations (no need to enforce order again) on `EntryA`
end)
|> Repo.transaction()
```
Note also that for the same reasons multiple operations on the same table in the
same transaction are not safe to perform if they each acquire locks in order,
because locks are not released until the transaction is committed.
### Order used for Explorer's tables
This is a complete list of the ordering currently in use on each table.
It also specifies the order between tables in the same transaction: locks for a
table on top need to be acquired before those from a table on the bottom.
Note that this should always be enforced because as long as there is one DB
transaction performing in a different order there is the possibility of a deadlock.
| schema module | table name | ordered by |
|---------------|------------|------------|
| Explorer.Chain.Address | addresses | asc: :hash |
| Explorer.Chain.Address.Name | address_names | [asc: :address_hash, asc: :name] |
| Explorer.Chain.Address.CoinBalance | address_coin_balances | [asc: :address_hash, asc: :block_number] |
| Explorer.Chain.Block | blocks | asc: :hash |
| Explorer.Chain.Block.SecondDegreeRelation | block_second_degree_relations | [asc: :nephew_hash, asc: :uncle_hash] |
| Explorer.Chain.Block.Reward | block_rewards | [asc: :address_hash, asc: :address_type, asc: :block_hash] |
| Explorer.Chain.Block.EmissionReward | emission_rewards | asc: :block_range |
| Explorer.Chain.Transaction | transactions | asc: :hash |
| Explorer.Chain.Transaction.Fork | transaction_forks | [asc: :uncle_hash, asc: :index] |
| Explorer.Chain.Log | logs | [asc: :transaction_hash, asc: :index] |
| Explorer.Chain.InternalTransaction | internal_transactions | [asc: :transaction_hash, asc: :index] |
| Explorer.Chain.Token | tokens | asc: :contract_address_hash |
| Explorer.Chain.TokenTransfer | token_transfers | [asc: :transaction_hash, asc: :log_index]|
| Explorer.Chain.Address.TokenBalance | address_token_balances | [asc: :address_hash, asc: :token_contract_address_hash, asc: :block_number] |
| Explorer.Chain.Address.CurrentTokenBalance | address_current_token_balances | [asc: :address_hash, asc: :token_contract_address_hash] |
| Explorer.Chain.StakingPool | staking_pools | :staking_address_hash |
| Explorer.Chain.StakingPoolsDelegator | staking_pools_delegators | [asc: :delegator_address_hash, asc: :pool_address_hash] |
| Explorer.Chain.ContractMethod | contract_methods | [asc: :identified, asc: :abi]
| Explorer.Market.MarketHistory | market_history | asc: :date |

@ -39,7 +39,7 @@
"ex_cldr_numbers": {:hex, :ex_cldr_numbers, "2.6.4", "5b1ac8451f889576bb29dee70412de1170974298727ab944aa4d17e91bdd3472", [:mix], [{:decimal, "~> 1.5", [hex: :decimal, repo: "hexpm", optional: false]}, {:ex_cldr, "~> 2.6", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_currencies, "~> 2.3", [hex: :ex_cldr_currencies, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ex_cldr_units": {:hex, :ex_cldr_units, "2.5.1", "0e65067a22a7c5146266c313d6333c2700868c32aa6d536f47c6c0d84aac3ac1", [:mix], [{:ex_cldr, "~> 2.6", [hex: :ex_cldr, repo: "hexpm", optional: false]}, {:ex_cldr_lists, "~> 2.2", [hex: :ex_cldr_lists, repo: "hexpm", optional: false]}, {:ex_cldr_numbers, "~> 2.6", [hex: :ex_cldr_numbers, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}], "hexpm"},
"ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm"},
"ex_json_schema": {:hex, :ex_json_schema, "0.6.1", "b57c0588385b8262b80f19d33d9b9b71fcd60d247691abf2635b57a03ec0ad44", [:mix], [], "hexpm"},
"ex_json_schema": {:hex, :ex_json_schema, "0.6.2", "de23d80478215987469c81688208fe0ff440ee0e0e6ae2268fcadbb2ff35df9d", [:mix], [], "hexpm"},
"ex_machina": {:hex, :ex_machina, "2.3.0", "92a5ad0a8b10ea6314b876a99c8c9e3f25f4dde71a2a835845b136b9adaf199a", [:mix], [{:ecto, "~> 2.2 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}, {:ecto_sql, "~> 3.0", [hex: :ecto_sql, repo: "hexpm", optional: true]}], "hexpm"},
"ex_rlp": {:hex, :ex_rlp, "0.5.2", "7f4ce7bd55e543c054ce6d49629b01e9833c3462e3d547952be89865f39f2c58", [:mix], [], "hexpm"},
"ex_utils": {:hex, :ex_utils, "0.1.7", "2c133e0bcdc49a858cf8dacf893308ebc05bc5fba501dc3d2935e65365ec0bf3", [:mix], [], "hexpm"},

Loading…
Cancel
Save