Merge pull request #1375 from poanetwork/fetch-latest-coin-balance

feat: synchronously fetch coin balances when an address is viewed.
pull/1444/head
Andrew Cravenho 6 years ago committed by GitHub
commit 5b5a0b3cfe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 10
      apps/block_scout_web/assets/css/components/address-overview.scss
  2. 25
      apps/block_scout_web/assets/js/pages/address.js
  3. 70
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  4. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex
  5. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex
  6. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex
  7. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex
  8. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex
  9. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex
  10. 2
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  11. 17
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  12. 2
      apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex
  13. 17
      apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex
  14. 24
      apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex
  15. 1
      apps/block_scout_web/mix.exs
  16. 29
      apps/block_scout_web/priv/gettext/default.pot
  17. 31
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  18. 24
      apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs
  19. 2
      apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs
  20. 13
      apps/explorer/lib/explorer/chain/address.ex
  21. 4
      apps/explorer/lib/explorer/chain/events/subscriber.ex
  22. 9
      apps/explorer/lib/explorer/chain/hash.ex
  23. 20
      apps/explorer/lib/explorer/chain/transaction.ex
  24. 18
      apps/indexer/lib/indexer/coin_balance/fetcher.ex
  25. 180
      apps/indexer/lib/indexer/coin_balance/on_demand_fetcher.ex
  26. 5
      apps/indexer/lib/indexer/coin_balance/supervisor.ex
  27. 153
      apps/indexer/test/indexer/coin_balance/on_demand_fetcher_test.exs

@ -8,3 +8,13 @@
height: 100%;
}
}
.balance-card-title {
margin-bottom: .5rem;
}
.address-detail-item{
display: inline-block;
padding-bottom: 0.5em;
margin-right: 1em;
}

@ -15,6 +15,8 @@ export const initialState = {
filter: null,
balance: null,
balanceCard: null,
fetchedCoinBalanceBlockNumber: null,
transactionCount: null,
validationCount: null
}
@ -47,7 +49,9 @@ export function reducer (state = initialState, action) {
}
case 'RECEIVED_UPDATED_BALANCE': {
return Object.assign({}, state, {
balance: action.msg.balance
balanceCard: action.msg.balanceCard,
balance: parseFloat(action.msg.balance),
fetchedCoinBalanceBlockNumber: action.msg.fetchedCoinBalanceBlockNumber
})
}
default:
@ -63,11 +67,11 @@ const elements = {
},
'[data-selector="balance-card"]': {
load ($el) {
return { balance: $el.html() }
return { balanceCard: $el.html(), balance: parseFloat($el.find('.current-balance-in-wei').attr('data-wei-value')) }
},
render ($el, state, oldState) {
if (oldState.balance === state.balance) return
$el.empty().append(state.balance)
$el.empty().append(state.balanceCard)
loadTokenBalanceDropdown()
updateAllCalculatedUsdValues()
}
@ -81,6 +85,15 @@ const elements = {
$el.empty().append(numeral(state.transactionCount).format())
}
},
'[data-selector="fetched-coin-balance-block-number"]': {
load ($el) {
return {fetchedCoinBalanceBlockNumber: numeral($el.text()).value()}
},
render ($el, state, oldState) {
if (oldState.fetchedCoinBalanceBlockNumber === state.fetchedCoinBalanceBlockNumber) return
$el.empty().append(numeral(state.fetchedCoinBalanceBlockNumber).format())
}
},
'[data-selector="validation-count"]': {
load ($el) {
return { validationCount: numeral($el.text()).value() }
@ -130,4 +143,10 @@ if ($addressDetailsPage.length) {
type: 'RECEIVED_NEW_BLOCK',
msg: humps.camelizeKeys(msg)
}))
addressChannel.push('get_balance', {})
.receive('ok', (msg) => store.dispatch({
type: 'RECEIVED_UPDATED_BALANCE',
msg: humps.camelizeKeys(msg)
}))
}

@ -5,8 +5,10 @@ defmodule BlockScoutWeb.AddressChannel do
use BlockScoutWeb, :channel
alias BlockScoutWeb.{AddressCoinBalanceView, AddressView, InternalTransactionView, TransactionView}
alias Explorer.Chain
alias Explorer.{Chain, Market}
alias Explorer.Chain.Hash
alias Explorer.Chain.Hash.Address, as: AddressHash
alias Explorer.ExchangeRates.Token
alias Phoenix.View
intercept(["balance_update", "coin_balance", "count", "internal_transaction", "transaction"])
@ -15,23 +17,45 @@ defmodule BlockScoutWeb.AddressChannel do
{:ok, %{}, assign(socket, :address_hash, address_hash)}
end
def handle_in("get_balance", _, socket) do
with {:ok, casted_address_hash} <- AddressHash.cast(socket.assigns.address_hash),
{:ok, address = %{fetched_coin_balance: balance}} when not is_nil(balance) <-
Chain.hash_to_address(casted_address_hash),
exchange_rate <- Market.get_exchange_rate(Explorer.coin()) || Token.null(),
{:ok, rendered} <- render_balance_card(address, exchange_rate, socket.assigns.locale) do
reply =
{:ok,
%{
balance_card: rendered,
balance: address.fetched_coin_balance.value,
fetched_coin_balance_block_number: address.fetched_coin_balance_block_number
}}
{:reply, reply, socket}
else
_ ->
{:noreply, socket}
end
end
def handle_out(
"balance_update",
%{address: address, exchange_rate: exchange_rate},
socket
) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
rendered =
View.render_to_string(
AddressView,
"_balance_card.html",
address: address,
exchange_rate: exchange_rate
)
push(socket, "balance", %{balance: rendered})
{:noreply, socket}
case render_balance_card(address, exchange_rate, socket.assigns.locale) do
{:ok, rendered} ->
push(socket, "balance", %{
balance_card: rendered,
balance: address.fetched_coin_balance.value,
fetched_coin_balance_block_number: address.fetched_coin_balance_block_number
})
{:noreply, socket}
_ ->
{:noreply, socket}
end
end
def handle_out("count", %{count: count}, socket) do
@ -104,4 +128,24 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket}
end
defp render_balance_card(address, exchange_rate, locale) do
Gettext.put_locale(BlockScoutWeb.Gettext, locale)
try do
rendered =
View.render_to_string(
AddressView,
"_balance_card.html",
address: address,
coin_balance_status: :current,
exchange_rate: exchange_rate
)
{:ok, rendered}
rescue
_ ->
:error
end
end
end

@ -11,6 +11,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
alias BlockScoutWeb.AddressCoinBalanceView
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.CoinBalance.OnDemandFetcher
alias Phoenix.View
def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do
@ -61,6 +62,7 @@ defmodule BlockScoutWeb.AddressCoinBalanceController do
{:ok, address} <- Chain.hash_to_address(address_hash) do
render(conn, "index.html",
address: address,
coin_balance_status: OnDemandFetcher.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
transaction_count: transaction_count(address),
validation_count: validation_count(address),

@ -5,6 +5,7 @@ defmodule BlockScoutWeb.AddressContractController do
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.CoinBalance.OnDemandFetcher
def index(conn, %{"address_id" => address_hash_string}) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
@ -13,6 +14,7 @@ defmodule BlockScoutWeb.AddressContractController do
conn,
"index.html",
address: address,
coin_balance_status: OnDemandFetcher.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
transaction_count: transaction_count(address),
validation_count: validation_count(address)

@ -11,6 +11,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do
alias BlockScoutWeb.InternalTransactionView
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.CoinBalance.OnDemandFetcher
alias Phoenix.View
def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do
@ -66,6 +67,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do
conn,
"index.html",
address: address,
coin_balance_status: OnDemandFetcher.trigger_fetch(address),
current_path: current_path(conn),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
filter: params["filter"],

@ -10,6 +10,7 @@ defmodule BlockScoutWeb.AddressReadContractController do
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.CoinBalance.OnDemandFetcher
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1]
@ -20,6 +21,7 @@ defmodule BlockScoutWeb.AddressReadContractController do
conn,
"index.html",
address: address,
coin_balance_status: OnDemandFetcher.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
transaction_count: transaction_count(address),
validation_count: validation_count(address)

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.AddressTokenController do
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.CoinBalance.OnDemandFetcher
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1]
import BlockScoutWeb.Chain, only: [next_page_params: 3, paging_options: 1, split_list_by_page: 1]
@ -17,6 +18,7 @@ defmodule BlockScoutWeb.AddressTokenController do
conn,
"index.html",
address: address,
coin_balance_status: OnDemandFetcher.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
transaction_count: transaction_count(address),
validation_count: validation_count(address),

@ -4,6 +4,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
alias BlockScoutWeb.TransactionView
alias Explorer.ExchangeRates.Token
alias Explorer.{Chain, Market}
alias Indexer.CoinBalance.OnDemandFetcher
alias Phoenix.View
import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1]
@ -80,6 +81,7 @@ defmodule BlockScoutWeb.AddressTokenTransferController do
conn,
"index.html",
address: address,
coin_balance_status: OnDemandFetcher.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
current_path: current_path(conn),
token: token,

@ -11,6 +11,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
alias BlockScoutWeb.TransactionView
alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias Indexer.CoinBalance.OnDemandFetcher
alias Phoenix.View
@transaction_necessity_by_association [
@ -90,6 +91,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
conn,
"index.html",
address: address,
coin_balance_status: OnDemandFetcher.trigger_fetch(address),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
filter: params["filter"],
transaction_count: transaction_count(address),

@ -10,7 +10,7 @@ defmodule BlockScoutWeb.Notifier do
alias Explorer.Counters.AverageBlockTime
alias Explorer.ExchangeRates.Token
def handle_event({:chain_event, :addresses, :realtime, addresses}) do
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()})
addresses
@ -18,7 +18,8 @@ defmodule BlockScoutWeb.Notifier do
|> Enum.each(&broadcast_balance/1)
end
def handle_event({:chain_event, :address_coin_balances, :realtime, address_coin_balances}) do
def handle_event({:chain_event, :address_coin_balances, type, address_coin_balances})
when type in [:realtime, :on_demand] do
Enum.each(address_coin_balances, &broadcast_address_coin_balance/1)
end
@ -103,10 +104,14 @@ defmodule BlockScoutWeb.Notifier do
end
defp broadcast_balance(%Address{hash: address_hash} = address) do
Endpoint.broadcast("addresses:#{address_hash}", "balance_update", %{
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
})
Endpoint.broadcast(
"addresses:#{address_hash}",
"balance_update",
%{
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
}
)
end
defp broadcast_block(block) do

@ -21,6 +21,8 @@ defmodule BlockScoutWeb.RealtimeEventHandler do
Subscriber.to(:internal_transactions, :realtime)
Subscriber.to(:token_transfers, :realtime)
Subscriber.to(:transactions, :realtime)
Subscriber.to(:addresses, :on_demand)
Subscriber.to(:address_coin_balances, :on_demand)
# Does not come from the indexer
Subscriber.to(:exchange_rate)
{:ok, []}

@ -1,16 +1,19 @@
<div class="card card-primary" data-test="outside_of_dropdown">
<div class="card-body">
<h2 class="card-title text-white"><%= gettext "Balance" %></h2>
<span></span>
<h2 class="card-title text-white balance-card-title"><%= gettext "Balance" %></h2>
<div class="text-right">
<h3 class="text-white" data-test="address_balance"><%= balance(@address) %></h3>
<span class="text-white text-faded">
<span
data-wei-value="<%= if @address.fetched_coin_balance, do: @address.fetched_coin_balance.value %>"
data-usd-exchange-rate="<%= @exchange_rate.usd_value %>">
<%= unless match?({:pending, _}, @coin_balance_status) do %>
<span class="text-white text-faded">
<span class="current-balance-in-wei"
data-wei-value="<%= if @address.fetched_coin_balance, do: @address.fetched_coin_balance.value %>"
data-usd-exchange-rate="<%= @exchange_rate.usd_value %>">
</spanc>
<small>(@ <span data-usd-unit-price="<%= @exchange_rate.usd_value %>"></span>/<%= gettext("Ether") %>)</small>
<br>
</span>
<small>(@ <span data-usd-unit-price="<%= @exchange_rate.usd_value %>"></span>/<%= gettext("Ether") %>)</small>
</span>
<% end %>
<div class="mt-3" data-token-balance-dropdown data-api_path="<%= address_token_balance_path(BlockScoutWeb.Endpoint, :index, @address.hash) %>">
<div data-loading class="mb-0 text-white text-faded">

@ -33,15 +33,23 @@
<%= link(token_title(@address.token), to: token_path(@conn, :show, @address.hash), "data-test": "token_hash_link" ) %>
</span>
<% end %>
<span class="mr-4 mb-2">
<span data-selector="transaction-count">
<%= Cldr.Number.to_string!(@transaction_count, format: "#,###") %>
<span>
<span class="address-detail-item">
<span data-selector="transaction-count">
<%= Cldr.Number.to_string!(@transaction_count, format: "#,###") %>
</span>
<%= gettext("Transactions Sent") %>
</span>
<span class="address-detail-item">
<%= gettext("Last Balance Update: Block #") %><span data-selector="fetched-coin-balance-block-number"><%= @address.fetched_coin_balance_block_number %></span>
</span>
<%= gettext("Transactions sent") %>
<%= if validator?(@validation_count) do %>
<span data-selector="validation-count">
<%= Cldr.Number.to_string!(@validation_count, format: "#,###") %>
</span> <%= gettext("Blocks Validated") %>
<span class="address-detail-item">
<span data-selector="validation-count">
<%= Cldr.Number.to_string!(@validation_count, format: "#,###") %>
</span>
<%= gettext("Blocks Validated") %>
</span>
<% end %>
</span>
</div>
@ -66,7 +74,7 @@
</div>
</div>
<div class="card-section col-md-12 col-lg-4" data-selector="balance-card">
<%= render BlockScoutWeb.AddressView, "_balance_card.html", conn: @conn, address: @address, exchange_rate: @exchange_rate %>
<%= render BlockScoutWeb.AddressView, "_balance_card.html", conn: @conn, address: @address, exchange_rate: @exchange_rate, coin_balance_status: @coin_balance_status %>
</div>
</div>

@ -90,6 +90,7 @@ defmodule BlockScoutWeb.Mixfile do
{:flow, "~> 0.12"},
{:gettext, "~> 0.16.1"},
{:httpoison, "~> 1.0"},
{:indexer, in_umbrella: true, runtime: false},
# JSON parser and generator
{:jason, "~> 1.0"},
{:junit_formatter, ">= 0.0.0", only: [:test], runtime: false},

@ -123,7 +123,7 @@ msgid "Average block time"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:3
#: lib/block_scout_web/templates/address/_balance_card.html.eex:4
msgid "Balance"
msgstr ""
@ -189,7 +189,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:40
#: lib/block_scout_web/templates/address/_tabs.html.eex:103
#: lib/block_scout_web/templates/address/overview.html.eex:44
#: lib/block_scout_web/templates/address/overview.html.eex:51
#: lib/block_scout_web/templates/address_validation/index.html.eex:30
#: lib/block_scout_web/templates/address_validation/index.html.eex:57
#: lib/block_scout_web/views/address_view.ex:274
@ -209,8 +209,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:81
#: lib/block_scout_web/templates/address/overview.html.eex:89
#: lib/block_scout_web/templates/address/overview.html.eex:97
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:91
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:99
msgid "Close"
@ -328,7 +328,7 @@ msgid "Copy Txn Hash"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:50
#: lib/block_scout_web/templates/address/overview.html.eex:58
msgid "Created by"
msgstr ""
@ -372,7 +372,7 @@ msgid "Enter the Solidity Contract Code below"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:25
#: lib/block_scout_web/templates/address/_balance_card.html.eex:28
msgid "Error trying to fetch balances."
msgstr ""
@ -387,7 +387,7 @@ msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:12
#: lib/block_scout_web/templates/address/_balance_card.html.eex:13
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:16
#: lib/block_scout_web/templates/layout/app.html.eex:51
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:19
@ -408,7 +408,7 @@ msgid "Execute"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:21
#: lib/block_scout_web/templates/address/_balance_card.html.eex:24
msgid "Fetching tokens..."
msgstr ""
@ -686,7 +686,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:13
#: lib/block_scout_web/templates/address/overview.html.eex:80
#: lib/block_scout_web/templates/address/overview.html.eex:88
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:13
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:13
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:90
@ -969,7 +969,6 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tile.html.eex:19
#: lib/block_scout_web/templates/address/overview.html.eex:40
msgid "Transactions sent"
msgstr ""
@ -1096,7 +1095,7 @@ msgid "Yes"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:56
#: lib/block_scout_web/templates/address/overview.html.eex:64
msgid "at"
msgstr ""
@ -1628,3 +1627,13 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:53
msgid "Enter contructor arguments if the contract had any"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:44
msgid "Last Balance Update: Block #"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:41
msgid "Transactions Sent"
msgstr ""

@ -123,7 +123,7 @@ msgid "Average block time"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:3
#: lib/block_scout_web/templates/address/_balance_card.html.eex:4
msgid "Balance"
msgstr ""
@ -189,7 +189,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tabs.html.eex:40
#: lib/block_scout_web/templates/address/_tabs.html.eex:103
#: lib/block_scout_web/templates/address/overview.html.eex:44
#: lib/block_scout_web/templates/address/overview.html.eex:51
#: lib/block_scout_web/templates/address_validation/index.html.eex:30
#: lib/block_scout_web/templates/address_validation/index.html.eex:57
#: lib/block_scout_web/views/address_view.ex:274
@ -209,8 +209,8 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_validator_metadata_modal.html.eex:37
#: lib/block_scout_web/templates/address/overview.html.eex:81
#: lib/block_scout_web/templates/address/overview.html.eex:89
#: lib/block_scout_web/templates/address/overview.html.eex:97
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:91
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:99
msgid "Close"
@ -328,7 +328,7 @@ msgid "Copy Txn Hash"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:50
#: lib/block_scout_web/templates/address/overview.html.eex:58
msgid "Created by"
msgstr ""
@ -372,7 +372,7 @@ msgid "Enter the Solidity Contract Code below"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:25
#: lib/block_scout_web/templates/address/_balance_card.html.eex:28
msgid "Error trying to fetch balances."
msgstr ""
@ -387,7 +387,7 @@ msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:12
#: lib/block_scout_web/templates/address/_balance_card.html.eex:13
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:16
#: lib/block_scout_web/templates/layout/app.html.eex:51
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:19
@ -408,7 +408,7 @@ msgid "Execute"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_balance_card.html.eex:21
#: lib/block_scout_web/templates/address/_balance_card.html.eex:24
msgid "Fetching tokens..."
msgstr ""
@ -686,7 +686,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:13
#: lib/block_scout_web/templates/address/overview.html.eex:80
#: lib/block_scout_web/templates/address/overview.html.eex:88
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:13
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:13
#: lib/block_scout_web/templates/tokens/overview/_details.html.eex:90
@ -969,7 +969,6 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/_tile.html.eex:19
#: lib/block_scout_web/templates/address/overview.html.eex:40
msgid "Transactions sent"
msgstr ""
@ -1096,7 +1095,7 @@ msgid "Yes"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:56
#: lib/block_scout_web/templates/address/overview.html.eex:64
msgid "at"
msgstr ""
@ -1624,7 +1623,17 @@ msgstr ""
msgid "Contract Libraries"
msgstr ""
#, elixir-format, fuzzy
#, elixir-format
#: lib/block_scout_web/templates/address_contract_verification/new.html.eex:53
msgid "Enter contructor arguments if the contract had any"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:44
msgid "Last Balance Update: Block #"
msgstr ""
#, elixir-format, fuzzy
#: lib/block_scout_web/templates/address/overview.html.eex:41
msgid "Transactions Sent"
msgstr ""

@ -3,6 +3,7 @@ defmodule BlockScoutWeb.AddressChannelTest do
# ETS tables are shared in `Explorer.Counters.AddressesWithBalanceCounter`
async: false
alias BlockScoutWeb.UserSocket
alias BlockScoutWeb.Notifier
alias Explorer.Counters.AddressesWithBalanceCounter
@ -20,6 +21,29 @@ defmodule BlockScoutWeb.AddressChannelTest do
assert_receive %Phoenix.Socket.Broadcast{topic: ^topic, event: "count", payload: %{count: _}}, :timer.seconds(5)
end
describe "user pushing to channel" do
setup do
address = insert(:address, fetched_coin_balance: 100_000, fetched_coin_balance_block_number: 1)
topic = "addresses:#{address.hash}"
{:ok, _, socket} =
UserSocket
|> socket("no_id", %{locale: "en"})
|> subscribe_and_join(topic)
{:ok, %{address: address, topic: topic, socket: socket}}
end
test "can retrieve current balance card of the address", %{socket: socket, address: address} do
ref = push(socket, "get_balance", %{})
assert_reply(ref, :ok, %{balance: sent_balance, balance_card: balance_card})
assert sent_balance == address.fetched_coin_balance.value
assert balance_card =~ "/address/#{address.hash}/token_balances"
end
end
describe "user subscribed to address" do
setup do
address = insert(:address)

@ -1,5 +1,5 @@
defmodule BlockScoutWeb.AddressContractVerificationTest do
use BlockScoutWeb.FeatureCase, async: true
use BlockScoutWeb.FeatureCase, async: false
alias BlockScoutWeb.{AddressContractPage, ContractVerifyPage}
alias Explorer.Factory

@ -30,6 +30,9 @@ defmodule Explorer.Chain.Address do
* `names` - names known for the address
* `inserted_at` - when this address was inserted
* `updated_at` when this address was last updated
`fetched_coin_balance` and `fetched_coin_balance_block_number` may be updated when a new coin_balance row is fetched.
They may also be updated when the balance is fetched via the on demand fetcher.
"""
@type t :: %__MODULE__{
fetched_coin_balance: Wei.t(),
@ -43,6 +46,16 @@ defmodule Explorer.Chain.Address do
nonce: non_neg_integer() | nil
}
@derive {Poison.Encoder,
except: [
:__meta__,
:smart_contract,
:token,
:contracts_creation_internal_transaction,
:contracts_creation_transaction,
:names
]}
@primary_key {:hash, Hash.Address, autogenerate: false}
schema "addresses" do
field(:fetched_coin_balance, Wei)

@ -5,11 +5,11 @@ defmodule Explorer.Chain.Events.Subscriber do
@allowed_broadcast_events ~w(addresses address_coin_balances blocks block_rewards internal_transactions token_transfers transactions)a
@allowed_broadcast_types ~w(catchup realtime)a
@allowed_broadcast_types ~w(catchup realtime on_demand)a
@allowed_events ~w(exchange_rate)a
@type broadcast_type :: :realtime | :catchup
@type broadcast_type :: :realtime | :catchup | :on_demand
@doc """
Subscribes the caller process to a specified subset of chain-related events.

@ -4,6 +4,7 @@ defmodule Explorer.Chain.Hash do
"""
import Bitwise
alias Poison.Encoder.BitString
@bits_per_byte 8
@hexadecimal_digits_per_byte 2
@ -224,4 +225,12 @@ defmodule Explorer.Chain.Hash do
@for.to_string(hash)
end
end
defimpl Poison.Encoder do
def encode(hash, options) do
hash
|> to_string()
|> BitString.encode(options)
end
end
end

@ -156,6 +156,26 @@ defmodule Explorer.Chain.Transaction do
value: Wei.t()
}
@derive {Poison.Encoder,
only: [
:block_number,
:cumulative_gas_used,
:error,
:gas,
:gas_price,
:gas_used,
:index,
:internal_transactions_indexed_at,
:created_contract_code_indexed_at,
:input,
:nonce,
:r,
:s,
:v,
:status,
:value
]}
@primary_key {:hash, Hash.Full, autogenerate: false}
schema "transactions" do
field(:block_number, :integer)

@ -120,20 +120,22 @@ defmodule Indexer.CoinBalance.Fetcher do
end)
end
defp run_fetched_balances(%FetchedBalances{params_list: []}, original_entries), do: {:retry, original_entries}
defp run_fetched_balances(%FetchedBalances{params_list: params_list, errors: errors}, _) do
def import_fetched_balances(%FetchedBalances{params_list: params_list}, broadcast_type \\ false) do
value_fetched_at = DateTime.utc_now()
importable_balances_params = Enum.map(params_list, &Map.put(&1, :value_fetched_at, value_fetched_at))
addresses_params = balances_params_to_address_params(importable_balances_params)
{:ok, _} =
Chain.import(%{
addresses: %{params: addresses_params, with: :balance_changeset},
address_coin_balances: %{params: importable_balances_params}
})
Chain.import(%{
addresses: %{params: addresses_params, with: :balance_changeset},
address_coin_balances: %{params: importable_balances_params},
broadcast: broadcast_type
})
end
defp run_fetched_balances(%FetchedBalances{errors: errors} = fetched_balances, _) do
{:ok, _} = import_fetched_balances(fetched_balances)
retry(errors)
end

@ -0,0 +1,180 @@
defmodule Indexer.CoinBalance.OnDemandFetcher do
@moduledoc """
Ensures that we have a reasonably up to date coin balance for a given address.
If we have an unfetched coin balance for that address, it will be synchronously fetched.
If not we will fetch the coin balance and created a fetched coin balance.
If we have a fetched coin balance, but it is over 100 blocks old, we will fetch and create a fetched coin baalnce.
"""
@latest_balance_stale_threshold :timer.hours(24)
use GenServer
import Ecto.Query, only: [from: 2]
import EthereumJSONRPC, only: [integer_to_quantity: 1]
alias EthereumJSONRPC.FetchedBalances
alias Explorer.{Chain, Repo}
alias Explorer.Chain.{Address, BlockNumberCache}
alias Explorer.Chain.Address.CoinBalance
alias Explorer.Counters.AverageBlockTime
alias Indexer.CoinBalance.Fetcher
alias Timex.Duration
@type block_number :: integer
@typedoc """
`block_number` represents the block that we will be updating the address to.
If there is a pending balance in the window, we will not fetch the balance
as of the latest block, we will instead fetch that pending balance.
"""
@type balance_status ::
:current
| {:stale, block_number}
| {:pending, block_number}
## Interface
@spec trigger_fetch(Address.t()) :: balance_status
def trigger_fetch(address) do
latest_block_number = latest_block_number()
case stale_balance_window(latest_block_number) do
{:error, :no_average_block_time} ->
:current
stale_balance_window ->
do_trigger_fetch(address, latest_block_number, stale_balance_window)
end
end
## Callbacks
def child_spec([json_rpc_named_arguments, server_opts]) do
%{
id: __MODULE__,
start: {__MODULE__, :start_link, [json_rpc_named_arguments, server_opts]},
type: :worker
}
end
def start_link(json_rpc_named_arguments, server_opts) do
GenServer.start_link(__MODULE__, json_rpc_named_arguments, server_opts)
end
def init(json_rpc_named_arguments) do
{:ok, %{json_rpc_named_arguments: json_rpc_named_arguments}}
end
def handle_cast({:fetch_and_update, block_number, address}, state) do
fetch_and_update(block_number, address, state.json_rpc_named_arguments)
{:noreply, state}
end
def handle_cast({:fetch_and_import, block_number, address}, state) do
fetch_and_import(block_number, address, state.json_rpc_named_arguments)
{:noreply, state}
end
## Implementation
defp do_trigger_fetch(%Address{fetched_coin_balance_block_number: nil} = address, latest_block_number, _) do
GenServer.cast(__MODULE__, {:fetch_and_update, latest_block_number, address})
{:stale, 0}
end
defp do_trigger_fetch(address, latest_block_number, stale_balance_window) do
latest =
from(
cb in CoinBalance,
where: cb.address_hash == ^address.hash,
where: cb.block_number >= ^stale_balance_window,
where: is_nil(cb.value_fetched_at),
order_by: [desc: :block_number],
limit: 1
)
if address.fetched_coin_balance_block_number < stale_balance_window do
GenServer.cast(__MODULE__, {:fetch_and_update, latest_block_number, address})
{:stale, latest_block_number}
else
case Repo.one(latest) do
nil ->
# There is no recent coin balance to fetch, so we check to see how old the
# balance is on the address. If it is too old, we check again, just to be safe.
:current
%CoinBalance{value_fetched_at: nil, block_number: block_number} ->
GenServer.cast(__MODULE__, {:fetch_and_import, block_number, address})
{:pending, block_number}
%CoinBalance{} ->
:current
end
end
end
defp fetch_and_import(block_number, address, json_rpc_named_arguments) do
case fetch_balances(block_number, address, json_rpc_named_arguments) do
{:ok, fetched_balances} -> do_import(fetched_balances)
_ -> :ok
end
end
defp fetch_and_update(block_number, address, json_rpc_named_arguments) do
case fetch_balances(block_number, address, json_rpc_named_arguments) do
{:ok, %{params_list: []}} ->
:ok
{:ok, %{params_list: params_list}} ->
address_params = Fetcher.balances_params_to_address_params(params_list)
Chain.import(%{
addresses: %{params: address_params, with: :balance_changeset},
broadcast: :on_demand
})
_ ->
:ok
end
end
defp fetch_balances(block_number, address, json_rpc_named_arguments) do
params = %{block_quantity: integer_to_quantity(block_number), hash_data: to_string(address.hash)}
EthereumJSONRPC.fetch_balances([params], json_rpc_named_arguments)
end
defp do_import(%FetchedBalances{} = fetched_balances) do
case Fetcher.import_fetched_balances(fetched_balances, :on_demand) do
{:ok, %{addresses: [address]}} -> {:ok, address}
_ -> :error
end
end
defp latest_block_number do
BlockNumberCache.max_number()
end
defp stale_balance_window(block_number) do
case AverageBlockTime.average_block_time() do
{:error, :disabled} ->
{:error, :no_average_block_time}
duration ->
average_block_time =
duration
|> Duration.to_milliseconds()
|> round()
block_number - div(@latest_balance_stale_threshold, average_block_time)
end
end
end

@ -5,7 +5,7 @@ defmodule Indexer.CoinBalance.Supervisor do
use Supervisor
alias Indexer.CoinBalance.Fetcher
alias Indexer.CoinBalance.{Fetcher, OnDemandFetcher}
def child_spec([init_arguments]) do
child_spec([init_arguments, []])
@ -30,7 +30,8 @@ defmodule Indexer.CoinBalance.Supervisor do
Supervisor.init(
[
{Task.Supervisor, name: Indexer.CoinBalance.TaskSupervisor},
{Fetcher, [fetcher_arguments, [name: Fetcher]]}
{Fetcher, [fetcher_arguments, [name: Fetcher]]},
{OnDemandFetcher, [fetcher_arguments[:json_rpc_named_arguments], [name: OnDemandFetcher]]}
],
strategy: :one_for_one
)

@ -0,0 +1,153 @@
defmodule Indexer.CoinBalance.OnDemandFetcherTest do
# MUST be `async: false` so that {:shared, pid} is set for connection to allow CoinBalanceFetcher's self-send to have
# connection allowed immediately.
use EthereumJSONRPC.Case, async: false
use Explorer.DataCase
import Mox
alias Explorer.Chain.Events.Subscriber
alias Explorer.Chain.Wei
alias Explorer.Counters.AverageBlockTime
alias Indexer.CoinBalance.OnDemandFetcher
@moduletag :capture_log
# MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to
# use expectations and stubs from test's pid.
setup :set_mox_global
setup :verify_on_exit!
setup %{json_rpc_named_arguments: json_rpc_named_arguments} do
mocked_json_rpc_named_arguments = Keyword.put(json_rpc_named_arguments, :transport, EthereumJSONRPC.Mox)
start_supervised!({Task.Supervisor, name: Indexer.TaskSupervisor})
start_supervised!(AverageBlockTime)
start_supervised!({OnDemandFetcher, [mocked_json_rpc_named_arguments, [name: OnDemandFetcher]]})
Application.put_env(:explorer, AverageBlockTime, enabled: true)
on_exit(fn ->
Application.put_env(:explorer, AverageBlockTime, enabled: false)
end)
%{json_rpc_named_arguments: mocked_json_rpc_named_arguments}
end
describe "trigger_fetch/1" do
setup do
now = Timex.now()
# we space these very far apart so that we know it will consider the 0th block stale (it calculates how far
# back we'd need to go to get 24 hours in the past)
block_0 = insert(:block, number: 0, timestamp: Timex.shift(now, hours: -50))
AverageBlockTime.average_block_time(block_0)
block_1 = insert(:block, number: 1, timestamp: now)
AverageBlockTime.average_block_time(block_1)
stale_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 0)
current_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 1)
pending_address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 1)
insert(:unfetched_balance, address_hash: pending_address.hash, block_number: 2)
%{stale_address: stale_address, current_address: current_address, pending_address: pending_address}
end
test "treats all addresses as current if the average block time is disabled", %{stale_address: address} do
Application.put_env(:explorer, AverageBlockTime, enabled: false)
assert OnDemandFetcher.trigger_fetch(address) == :current
end
test "if the address has not been fetched within the last 24 hours of blocks it is considered stale", %{
stale_address: address
} do
assert OnDemandFetcher.trigger_fetch(address) == {:stale, 1}
end
test "if the address has been fetched within the last 24 hours of blocks it is considered current", %{
current_address: address
} do
assert OnDemandFetcher.trigger_fetch(address) == :current
end
test "if there is an unfetched balance within the window for an address, it is considered pending", %{
pending_address: pending_address
} do
assert OnDemandFetcher.trigger_fetch(pending_address) == {:pending, 2}
end
end
describe "update behaviour" do
setup do
Subscriber.to(:addresses, :on_demand)
Subscriber.to(:address_coin_balances, :on_demand)
now = Timex.now()
# we space these very far apart so that we know it will consider the 0th block stale (it calculates how far
# back we'd need to go to get 24 hours in the past)
block_0 = insert(:block, number: 0, timestamp: Timex.shift(now, hours: -50))
AverageBlockTime.average_block_time(block_0)
block_1 = insert(:block, number: 1, timestamp: now)
AverageBlockTime.average_block_time(block_1)
:ok
end
test "a stale address broadcasts the new address" do
address = insert(:address, fetched_coin_balance: 1, fetched_coin_balance_block_number: 0)
address_hash = address.hash
string_address_hash = to_string(address.hash)
expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [
%{
id: id,
method: "eth_getBalance",
params: [^string_address_hash, "0x1"]
}
],
_options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]}
end)
assert OnDemandFetcher.trigger_fetch(address) == {:stale, 1}
{:ok, expected_wei} = Wei.cast(2)
assert_receive(
{:chain_event, :addresses, :on_demand,
[%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 1}]}
)
end
test "a pending address broadcasts the new address and the new coin balance" do
address = insert(:address, fetched_coin_balance: 0, fetched_coin_balance_block_number: 1)
insert(:unfetched_balance, address_hash: address.hash, block_number: 2)
address_hash = address.hash
string_address_hash = to_string(address.hash)
expect(EthereumJSONRPC.Mox, :json_rpc, 1, fn [
%{
id: id,
method: "eth_getBalance",
params: [^string_address_hash, "0x2"]
}
],
_options ->
{:ok, [%{id: id, jsonrpc: "2.0", result: "0x02"}]}
end)
assert OnDemandFetcher.trigger_fetch(address) == {:pending, 2}
{:ok, expected_wei} = Wei.cast(2)
assert_receive(
{:chain_event, :addresses, :on_demand,
[%{hash: ^address_hash, fetched_coin_balance: ^expected_wei, fetched_coin_balance_block_number: 2}]}
)
end
end
end
Loading…
Cancel
Save