Setup live updates for address balance

pull/429/head
jimmay5469 6 years ago
parent e51e6c42b5
commit 4d612d59f0
  1. 13
      apps/explorer/lib/explorer/chain.ex
  2. 8
      apps/explorer/test/explorer/chain_test.exs
  3. 8
      apps/explorer_web/assets/__tests__/pages/address.js
  4. 6
      apps/explorer_web/assets/__tests__/pages/transaction.js
  5. 13
      apps/explorer_web/assets/js/pages/address.js
  6. 10
      apps/explorer_web/assets/js/pages/transaction.js
  7. 13
      apps/explorer_web/lib/explorer_web/channels/address_channel.ex
  8. 1
      apps/explorer_web/lib/explorer_web/event_handler.ex
  9. 40
      apps/explorer_web/lib/explorer_web/notifier.ex
  10. 10
      apps/explorer_web/lib/explorer_web/templates/address/_balance_card.html.eex
  11. 13
      apps/explorer_web/lib/explorer_web/templates/address/overview.html.eex
  12. 26
      apps/explorer_web/test/explorer_web/features/viewing_addresses_test.exs
  13. 2
      apps/explorer_web/test/explorer_web/features/viewing_transactions_test.exs

@ -362,10 +362,11 @@ defmodule Explorer.Chain do
] ]
) :: {:ok, [Hash.Address.t()]} | {:error, [Changeset.t()]} ) :: {:ok, [Hash.Address.t()]} | {:error, [Changeset.t()]}
def update_balances(addresses_params, options \\ []) when is_list(options) do def update_balances(addresses_params, options \\ []) when is_list(options) do
with {:ok, changes_list} <- changes_list(addresses_params, for: Address, with: :balance_changeset) do with {:ok, changes_list} <- changes_list(addresses_params, for: Address, with: :balance_changeset),
timestamps = timestamps() {:ok, address_hashes} <-
insert_addresses(changes_list, timeout: options[:timeout] || @transaction_timeout, timestamps: timestamps()) do
insert_addresses(changes_list, timeout: options[:timeout] || @transaction_timeout, timestamps: timestamps) broadcast_events([{:balance_updates, address_hashes}])
{:ok, address_hashes}
end end
end end
@ -1734,7 +1735,7 @@ defmodule Explorer.Chain do
:ok :ok
""" """
@spec subscribe_to_events(chain_event()) :: :ok @spec subscribe_to_events(chain_event()) :: :ok
def subscribe_to_events(event_type) when event_type in ~w(blocks logs transactions)a do def subscribe_to_events(event_type) when event_type in ~w(balance_updates blocks logs transactions)a do
Registry.register(Registry.ChainEvents, event_type, []) Registry.register(Registry.ChainEvents, event_type, [])
:ok :ok
end end
@ -1893,7 +1894,7 @@ defmodule Explorer.Chain do
end end
defp broadcast_events(data) do defp broadcast_events(data) do
for {event_type, event_data} <- data, event_type in ~w(blocks logs transactions)a do for {event_type, event_data} <- data, event_type in ~w(balance_updates blocks logs transactions)a do
broadcast_event_data(event_type, event_data) broadcast_event_data(event_type, event_data)
end end
end end

@ -1189,4 +1189,12 @@ defmodule Explorer.ChainTest do
assert_received {:chain_event, :logs, [%Log{}]} assert_received {:chain_event, :logs, [%Log{}]}
end end
end end
test "publishes update_balance data to subscribers on upsert" do
address = %Address{hash: address_hash} = insert(:address, fetched_balance: 3, fetched_balance_block_number: 3)
Chain.subscribe_to_events(:balance_updates)
Chain.update_balances([Map.from_struct(address)])
assert_received {:chain_event, :balance_updates, [^address_hash]}
end
end end

@ -74,17 +74,17 @@ test('CHANNEL_DISCONNECTED', () => {
expect(output.batchCountAccumulator).toBe(0) expect(output.batchCountAccumulator).toBe(0)
}) })
test('RECEIVED_UPDATED_OVERVIEW', () => { test('RECEIVED_UPDATED_BALANCE', () => {
const state = initialState const state = initialState
const action = { const action = {
type: 'RECEIVED_UPDATED_OVERVIEW', type: 'RECEIVED_UPDATED_BALANCE',
msg: { msg: {
overview: 'hello world' balance: 'hello world'
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.overview).toBe('hello world') expect(output.balance).toBe('hello world')
}) })
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {

@ -1,14 +1,14 @@
import { reducer, initialState } from '../../js/pages/transaction' import { reducer, initialState } from '../../js/pages/transaction'
test('RECEIVED_UPDATED_CONFIRMATIONS', () => { test('RECEIVED_UPDATED_CONFIRMATIONS', () => {
const state = initialState const state = { ...initialState, blockNumber: 1 }
const action = { const action = {
type: 'RECEIVED_UPDATED_CONFIRMATIONS', type: 'RECEIVED_UPDATED_CONFIRMATIONS',
msg: { msg: {
confirmations: 5 blockNumber: 5
} }
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.confirmations).toBe(5) expect(output.confirmations).toBe(4)
}) })

@ -15,7 +15,7 @@ export const initialState = {
channelDisconnected: false, channelDisconnected: false,
filter: null, filter: null,
newTransactions: [], newTransactions: [],
overview: null, balance: null,
transactionCount: null transactionCount: null
} }
@ -37,9 +37,9 @@ export function reducer (state = initialState, action) {
batchCountAccumulator: 0 batchCountAccumulator: 0
}) })
} }
case 'RECEIVED_UPDATED_OVERVIEW': { case 'RECEIVED_UPDATED_BALANCE': {
return Object.assign({}, state, { return Object.assign({}, state, {
overview: action.msg.overview balance: action.msg.balance
}) })
} }
case 'RECEIVED_NEW_TRANSACTION_BATCH': { case 'RECEIVED_NEW_TRANSACTION_BATCH': {
@ -53,7 +53,6 @@ export function reducer (state = initialState, action) {
)) ))
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
console.log(state.transactionCount + action.msgs.length);
return Object.assign({}, state, { return Object.assign({}, state, {
newTransactions: [ newTransactions: [
...state.newTransactions, ...state.newTransactions,
@ -88,7 +87,7 @@ router.when('/addresses/:addressHash').then((params) => initRedux(reducer, {
.receive('ok', resp => { console.log('Joined successfully', `addresses:${addressHash}`, resp) }) .receive('ok', resp => { console.log('Joined successfully', `addresses:${addressHash}`, resp) })
.receive('error', resp => { console.log('Unable to join', `addresses:${addressHash}`, resp) }) .receive('error', resp => { console.log('Unable to join', `addresses:${addressHash}`, resp) })
channel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) channel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
channel.on('overview', (msg) => store.dispatch({ type: 'RECEIVED_UPDATED_OVERVIEW', msg })) channel.on('balance', (msg) => store.dispatch({ type: 'RECEIVED_UPDATED_BALANCE', msg }))
if (!blockNumber) channel.on('transaction', batchChannel((msgs) => store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs }))) if (!blockNumber) channel.on('transaction', batchChannel((msgs) => store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs })))
}, },
render (state, oldState) { render (state, oldState) {
@ -96,13 +95,13 @@ router.when('/addresses/:addressHash').then((params) => initRedux(reducer, {
const $channelBatchingCount = $('[data-selector="channel-batching-count"]') const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]') const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]')
const $overview = $('[data-selector="overview"]') const $balance = $('[data-selector="balance"]')
const $transactionCount = $('[data-selector="transaction-count"]') const $transactionCount = $('[data-selector="transaction-count"]')
const $transactionsList = $('[data-selector="transactions-list"]') const $transactionsList = $('[data-selector="transactions-list"]')
if ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload() if ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload()
if (state.channelDisconnected) $channelDisconnected.show() if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.overview !== state.overview) $overview.empty().append(state.overview) if (oldState.balance !== state.balance) $balance.empty().append(state.balance)
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format())
if (state.batchCountAccumulator) { if (state.batchCountAccumulator) {
$channelBatching.show() $channelBatching.show()

@ -1,4 +1,5 @@
import $ from 'jquery' import $ from 'jquery'
import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
import 'numeral/locales' import 'numeral/locales'
import socket from '../socket' import socket from '../socket'
@ -18,9 +19,9 @@ export function reducer (state = initialState, action) {
}) })
} }
case 'RECEIVED_UPDATED_CONFIRMATIONS': { case 'RECEIVED_UPDATED_CONFIRMATIONS': {
if ((action.msg.block_number - state.blockNumber) > state.confirmations) { if ((action.msg.blockNumber - state.blockNumber) > state.confirmations) {
return Object.assign({}, state, { return Object.assign({}, state, {
confirmations: action.msg.block_number - state.blockNumber confirmations: action.msg.blockNumber - state.blockNumber
}) })
} else return state } else return state
} }
@ -29,9 +30,8 @@ export function reducer (state = initialState, action) {
} }
} }
router.when('/transactions/:transactionHash').then((params) => initRedux(reducer, { router.when('/transactions/:transactionHash').then(({ locale }) => initRedux(reducer, {
main (store) { main (store) {
const { transactionHash, locale } = params
const channel = socket.channel(`transactions:confirmations`, {}) const channel = socket.channel(`transactions:confirmations`, {})
const $transactionBlockNumber = $('[data-selector="block-number"]') const $transactionBlockNumber = $('[data-selector="block-number"]')
numeral.locale(locale) numeral.locale(locale)
@ -39,7 +39,7 @@ router.when('/transactions/:transactionHash').then((params) => initRedux(reducer
channel.join() channel.join()
.receive('ok', resp => { console.log('Joined successfully', `transactions:confirmations`, resp) }) .receive('ok', resp => { console.log('Joined successfully', `transactions:confirmations`, resp) })
.receive('error', resp => { console.log('Unable to join', `transactions:confirmations`, resp) }) .receive('error', resp => { console.log('Unable to join', `transactions:confirmations`, resp) })
channel.on('update', (msg) => store.dispatch({ type: 'RECEIVED_UPDATED_CONFIRMATIONS', msg })) channel.on('update', (msg) => store.dispatch({ type: 'RECEIVED_UPDATED_CONFIRMATIONS', msg: humps.camelizeKeys(msg) }))
}, },
render (state, oldState) { render (state, oldState) {
const $blockConfirmations = $('[data-selector="block-confirmations"]') const $blockConfirmations = $('[data-selector="block-confirmations"]')

@ -7,7 +7,7 @@ defmodule ExplorerWeb.AddressChannel do
alias ExplorerWeb.{AddressTransactionView, AddressView} alias ExplorerWeb.{AddressTransactionView, AddressView}
alias Phoenix.View alias Phoenix.View
intercept(["overview", "transaction"]) intercept(["balance_update", "transaction"])
def join("addresses:" <> _address_hash, _params, socket) do def join("addresses:" <> _address_hash, _params, socket) do
{:ok, %{}, socket} {:ok, %{}, socket}
@ -35,8 +35,8 @@ defmodule ExplorerWeb.AddressChannel do
end end
def handle_out( def handle_out(
"overview", "balance_update",
%{address: address, exchange_rate: exchange_rate, transaction_count: transaction_count}, %{address: address, exchange_rate: exchange_rate},
socket socket
) do ) do
Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale) Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale)
@ -44,14 +44,13 @@ defmodule ExplorerWeb.AddressChannel do
rendered = rendered =
View.render_to_string( View.render_to_string(
AddressView, AddressView,
"_values.html", "_balance_card.html",
locale: socket.assigns.locale, locale: socket.assigns.locale,
address: address, address: address,
exchange_rate: exchange_rate, exchange_rate: exchange_rate
transaction_count: transaction_count
) )
push(socket, "overview", %{overview: rendered}) push(socket, "balance", %{balance: rendered})
{:noreply, socket} {:noreply, socket}
end end
end end

@ -14,6 +14,7 @@ defmodule ExplorerWeb.EventHandler do
def init([]) do def init([]) do
Chain.subscribe_to_events(:blocks) Chain.subscribe_to_events(:blocks)
Chain.subscribe_to_events(:transactions) Chain.subscribe_to_events(:transactions)
Chain.subscribe_to_events(:balance_updates)
{:ok, []} {:ok, []}
end end

@ -1,8 +1,9 @@
defmodule ExplorerWeb.Notifier do defmodule ExplorerWeb.Notifier do
alias Explorer.Chain alias Explorer.{Chain, Market}
alias Explorer.ExchangeRates.Token
alias ExplorerWeb.Endpoint alias ExplorerWeb.Endpoint
def handle_event({:chain_event, :blocks, []}), do: IO.inspect "EMPTY BLOCKS" def handle_event({:chain_event, :blocks, []}), do: IO.inspect("EMPTY BLOCKS")
def handle_event({:chain_event, :blocks, blocks}) do def handle_event({:chain_event, :blocks, blocks}) do
max_numbered_block = Enum.max_by(blocks, & &1.number).number max_numbered_block = Enum.max_by(blocks, & &1.number).number
@ -14,22 +15,39 @@ defmodule ExplorerWeb.Notifier do
|> Enum.each(&broadcast_transaction/1) |> Enum.each(&broadcast_transaction/1)
end end
def handle_event({:chain_event, :balance_updates, address_hashes}) do
address_hashes
|> Enum.each(&broadcast_balance/1)
end
def handle_event(event), do: IO.inspect({:error, event}) def handle_event(event), do: IO.inspect({:error, event})
defp broadcast_balance(address_hash) do
{:ok, address} = Chain.hash_to_address(address_hash)
ExplorerWeb.Endpoint.broadcast("addresses:#{address.hash}", "balance_update", %{
address: address,
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null()
})
end
defp broadcast_transaction(transaction_hash) do defp broadcast_transaction(transaction_hash) do
{:ok, transaction} = Chain.hash_to_transaction( {:ok, transaction} =
transaction_hash, Chain.hash_to_transaction(
necessity_by_association: %{ transaction_hash,
block: :required, necessity_by_association: %{
from_address: :optional, block: :required,
to_address: :optional from_address: :optional,
} to_address: :optional
) }
)
ExplorerWeb.Endpoint.broadcast("addresses:#{transaction.from_address_hash}", "transaction", %{ ExplorerWeb.Endpoint.broadcast("addresses:#{transaction.from_address_hash}", "transaction", %{
address: transaction.from_address, address: transaction.from_address,
transaction: transaction transaction: transaction
}) })
if (transaction.from_address && transaction.to_address != transaction.from_address) do
if transaction.to_address && transaction.to_address != transaction.from_address do
ExplorerWeb.Endpoint.broadcast("addresses:#{transaction.to_address_hash}", "transaction", %{ ExplorerWeb.Endpoint.broadcast("addresses:#{transaction.to_address_hash}", "transaction", %{
address: transaction.to_address, address: transaction.to_address,
transaction: transaction transaction: transaction

@ -0,0 +1,10 @@
<div class="card bg-primary" data-selector='balance'>
<div class="card-body">
<h2 class="card-title text-white"><%= gettext "Balance" %></h2>
<span></span>
<div class="text-right">
<h3 class="text-white" data-test="address_balance"><%= balance(@address) %></h3>
<span class="text-light"><%= formatted_usd(@address, @exchange_rate) %></span>
</div>
</div>
</div>

@ -1,5 +1,5 @@
<section> <section>
<div class="row" data-selector='overview'> <div class="row">
<div class="col-md-12 col-lg-8"> <div class="col-md-12 col-lg-8">
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
@ -53,16 +53,7 @@
</div> </div>
</div> </div>
<div class="col-md-6 col-lg-4"> <div class="col-md-6 col-lg-4">
<div class="card bg-primary"> <%= render ExplorerWeb.AddressView, "_balance_card.html", address: @address, exchange_rate: @exchange_rate %>
<div class="card-body">
<h2 class="card-title text-white"><%= gettext "Balance" %></h2>
<span></span>
<div class="text-right">
<h3 class="text-white" data-test="address_balance"><%= balance(@address) %></h3>
<span class="text-light"><%= formatted_usd(@address, @exchange_rate) %></span>
</div>
</div>
</div>
</div> </div>
</div> </div>
</section> </section>

@ -1,7 +1,8 @@
defmodule ExplorerWeb.ViewingAddressesTest do defmodule ExplorerWeb.ViewingAddressesTest do
use ExplorerWeb.FeatureCase, async: true use ExplorerWeb.FeatureCase, async: true
alias Explorer.Chain.Wei alias Explorer.Chain
alias Explorer.Chain.{Address, Wei}
alias ExplorerWeb.{AddressPage, HomePage, Notifier} alias ExplorerWeb.{AddressPage, HomePage, Notifier}
setup do setup do
@ -261,6 +262,29 @@ defmodule ExplorerWeb.ViewingAddressesTest do
assert_text(session, AddressPage.transaction_count(), "3") assert_text(session, AddressPage.transaction_count(), "3")
end end
test "viewing updated balance via live update", %{session: session} do
address = %Address{hash: hash} = insert(:address, fetched_balance: 500)
session
|> AddressPage.visit_page(address)
|> assert_text(AddressPage.balance(), "0.0000000000000005 POA")
{:ok, [^hash]} =
Chain.update_balances([
%{
fetched_balance: 100,
fetched_balance_block_number: 1,
hash: hash
}
])
{:ok, updated_address} = Chain.hash_to_address(hash)
Notifier.handle_event({:chain_event, :balance_updates, [updated_address.hash]})
assert_text(session, AddressPage.balance(), "0.0000000000000001 POA")
end
test "contract creation is shown for to_address on list page", %{ test "contract creation is shown for to_address on list page", %{
addresses: addresses, addresses: addresses,
block: block, block: block,

@ -4,7 +4,7 @@ defmodule ExplorerWeb.ViewingTransactionsTest do
use ExplorerWeb.FeatureCase, async: true use ExplorerWeb.FeatureCase, async: true
alias Explorer.Chain.Wei alias Explorer.Chain.Wei
alias ExplorerWeb.{AddressPage, HomePage, Notifier,TransactionListPage, TransactionLogsPage, TransactionPage} alias ExplorerWeb.{AddressPage, HomePage, Notifier, TransactionListPage, TransactionLogsPage, TransactionPage}
setup do setup do
block = block =

Loading…
Cancel
Save