From 5582c3df1c8b0fdd39ca16064c356f18441dbd16 Mon Sep 17 00:00:00 2001 From: zachdaniel Date: Wed, 23 Jan 2019 14:59:52 -0500 Subject: [PATCH] feat: synchronously fetch coin balances when an address is viewed. This is only done if the latest balance we have is as of a block that is older than approximately one day. We could just fetch a day old block and compare, but using the average_block_time lets us avoid hitting the database again, and also accounts for somehow having bad information in the blocks table, getting us the latest balance no matter what. If there is an unfetched coin balance, then we just fetch that instead of doing the above. We also don't render the balance if there is an unfetched row. --- .../css/components/address-overview.scss | 10 + .../assets/js/pages/address.js | 25 ++- .../channels/address_channel.ex | 70 +++++-- .../address_coin_balance_controller.ex | 2 + .../address_contract_controller.ex | 2 + ...address_internal_transaction_controller.ex | 2 + .../address_read_contract_controller.ex | 2 + .../controllers/address_token_controller.ex | 2 + .../address_token_transfer_controller.ex | 2 + .../address_transaction_controller.ex | 2 + .../lib/block_scout_web/notifier.ex | 17 +- .../block_scout_web/realtime_event_handler.ex | 2 + .../templates/address/_balance_card.html.eex | 17 +- .../templates/address/overview.html.eex | 24 ++- apps/block_scout_web/mix.exs | 1 + apps/block_scout_web/priv/gettext/default.pot | 29 ++- .../priv/gettext/en/LC_MESSAGES/default.po | 31 +-- .../channels/address_channel_test.exs | 24 +++ .../address_contract_verification_test.exs | 2 +- apps/explorer/lib/explorer/chain/address.ex | 13 ++ .../lib/explorer/chain/events/subscriber.ex | 4 +- apps/explorer/lib/explorer/chain/hash.ex | 9 + .../lib/explorer/chain/transaction.ex | 20 ++ .../lib/indexer/coin_balance/fetcher.ex | 18 +- .../indexer/coin_balance/on_demand_fetcher.ex | 180 ++++++++++++++++++ .../lib/indexer/coin_balance/supervisor.ex | 5 +- .../coin_balance/on_demand_fetcher_test.exs | 153 +++++++++++++++ 27 files changed, 597 insertions(+), 71 deletions(-) create mode 100644 apps/indexer/lib/indexer/coin_balance/on_demand_fetcher.ex create mode 100644 apps/indexer/test/indexer/coin_balance/on_demand_fetcher_test.exs diff --git a/apps/block_scout_web/assets/css/components/address-overview.scss b/apps/block_scout_web/assets/css/components/address-overview.scss index 77e89e69b7..9acb7fcdfa 100644 --- a/apps/block_scout_web/assets/css/components/address-overview.scss +++ b/apps/block_scout_web/assets/css/components/address-overview.scss @@ -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; +} diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index 857bb1c215..5b35a0b9d7 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -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) + })) } diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex index ffba90df33..cbbaa7eba7 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex index 3346b70f5e..0c837ab45f 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_coin_balance_controller.ex @@ -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), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex index 4e694397e0..f3f0c8655b 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_contract_controller.ex @@ -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) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex index 08be14a9fc..5163e57795 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex @@ -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"], diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex index 76c5147d2f..adf5febf78 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_read_contract_controller.ex @@ -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) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex index 6938f741bb..7023cbca76 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_controller.ex @@ -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), diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex index a0e96df02a..40c2131bfd 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_token_transfer_controller.ex @@ -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, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 80b122a80c..65a3bdf134 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -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), diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 3f42eff792..d1f538cf85 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex index b3ff207e3c..f0f73f402c 100644 --- a/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/realtime_event_handler.ex @@ -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, []} diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex index 9f8159f73d..1d10fa2d17 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex @@ -1,16 +1,19 @@
-

<%= gettext "Balance" %>

+

<%= gettext "Balance" %>

<%= balance(@address) %>

- - + <%= unless match?({:pending, _}, @coin_balance_status) do %> + + + + (@ /<%= gettext("Ether") %>) +
- (@ /<%= gettext("Ether") %>) -
+ <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index ac69c6d0cf..41127fb323 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -33,15 +33,23 @@ <%= link(token_title(@address.token), to: token_path(@conn, :show, @address.hash), "data-test": "token_hash_link" ) %> <% end %> - - - <%= Cldr.Number.to_string!(@transaction_count, format: "#,###") %> + + + + <%= Cldr.Number.to_string!(@transaction_count, format: "#,###") %> + + <%= gettext("Transactions Sent") %> + + + <%= gettext("Last Balance Update: Block #") %><%= @address.fetched_coin_balance_block_number %> - <%= gettext("Transactions sent") %> <%= if validator?(@validation_count) do %> - - <%= Cldr.Number.to_string!(@validation_count, format: "#,###") %> - <%= gettext("Blocks Validated") %> + + + <%= Cldr.Number.to_string!(@validation_count, format: "#,###") %> + + <%= gettext("Blocks Validated") %> + <% end %>
@@ -66,7 +74,7 @@
- <%= 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 %>
diff --git a/apps/block_scout_web/mix.exs b/apps/block_scout_web/mix.exs index 1e2e88d199..d6ab96dc2b 100644 --- a/apps/block_scout_web/mix.exs +++ b/apps/block_scout_web/mix.exs @@ -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}, diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 781c55af15..8472653101 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -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 "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index 62ae365a66..14ade55e6a 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -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 "" diff --git a/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs index d2e374defb..49d643fe27 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs @@ -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) diff --git a/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs b/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs index 97a8c9e11b..74af74063b 100644 --- a/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/address_contract_verification_test.exs @@ -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 diff --git a/apps/explorer/lib/explorer/chain/address.ex b/apps/explorer/lib/explorer/chain/address.ex index 623fbf8f63..ee1c09eba2 100644 --- a/apps/explorer/lib/explorer/chain/address.ex +++ b/apps/explorer/lib/explorer/chain/address.ex @@ -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) diff --git a/apps/explorer/lib/explorer/chain/events/subscriber.ex b/apps/explorer/lib/explorer/chain/events/subscriber.ex index 5538dddb6e..6a9df99438 100644 --- a/apps/explorer/lib/explorer/chain/events/subscriber.ex +++ b/apps/explorer/lib/explorer/chain/events/subscriber.ex @@ -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. diff --git a/apps/explorer/lib/explorer/chain/hash.ex b/apps/explorer/lib/explorer/chain/hash.ex index 74f429d6de..ed02b5ac53 100644 --- a/apps/explorer/lib/explorer/chain/hash.ex +++ b/apps/explorer/lib/explorer/chain/hash.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 3568006f4e..9f50aaeeb2 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -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) diff --git a/apps/indexer/lib/indexer/coin_balance/fetcher.ex b/apps/indexer/lib/indexer/coin_balance/fetcher.ex index ac74b7fb15..9320e78e69 100644 --- a/apps/indexer/lib/indexer/coin_balance/fetcher.ex +++ b/apps/indexer/lib/indexer/coin_balance/fetcher.ex @@ -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 diff --git a/apps/indexer/lib/indexer/coin_balance/on_demand_fetcher.ex b/apps/indexer/lib/indexer/coin_balance/on_demand_fetcher.ex new file mode 100644 index 0000000000..06d0780307 --- /dev/null +++ b/apps/indexer/lib/indexer/coin_balance/on_demand_fetcher.ex @@ -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 diff --git a/apps/indexer/lib/indexer/coin_balance/supervisor.ex b/apps/indexer/lib/indexer/coin_balance/supervisor.ex index 27aedd603f..6624ee1bec 100644 --- a/apps/indexer/lib/indexer/coin_balance/supervisor.ex +++ b/apps/indexer/lib/indexer/coin_balance/supervisor.ex @@ -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 ) diff --git a/apps/indexer/test/indexer/coin_balance/on_demand_fetcher_test.exs b/apps/indexer/test/indexer/coin_balance/on_demand_fetcher_test.exs new file mode 100644 index 0000000000..071493e28a --- /dev/null +++ b/apps/indexer/test/indexer/coin_balance/on_demand_fetcher_test.exs @@ -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