diff --git a/apps/block_scout_web/assets/__tests__/pages/block.js b/apps/block_scout_web/assets/__tests__/pages/block.js new file mode 100644 index 0000000000..0f50e9cf7f --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/block.js @@ -0,0 +1,13 @@ +import { reducer, initialState } from '../../js/pages/block' + +test('RECEIVED_NEW_BLOCK', () => { + const action = { + type: 'RECEIVED_NEW_BLOCK', + msg: { + blockHtml: "test" + } + } + const output = reducer(initialState, action) + + expect(output.newBlock).toBe("test") +}) diff --git a/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js b/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js index c1e8759859..ff763df87c 100644 --- a/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js +++ b/apps/block_scout_web/assets/js/lib/token_balance_dropdown.js @@ -16,4 +16,7 @@ const tokenBalanceDropdown = (element) => { }) } -$('[data-token-balance-dropdown]').each((_index, element) => tokenBalanceDropdown(element)) +export function loadTokenBalanceDropdown () { + $('[data-token-balance-dropdown]').each((_index, element) => tokenBalanceDropdown(element)) +} +loadTokenBalanceDropdown() diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index dd8116be1e..277d158d0c 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -5,6 +5,7 @@ import socket from '../socket' import router from '../router' import { batchChannel, initRedux } from '../utils' import { updateAllAges } from '../lib/from_now' +import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' const BATCH_THRESHOLD = 10 @@ -14,6 +15,7 @@ export const initialState = { beyondPageOne: null, channelDisconnected: false, filter: null, + newInternalTransactions: [], newTransactions: [], balance: null, transactionCount: null @@ -42,6 +44,29 @@ export function reducer (state = initialState, action) { balance: action.msg.balance }) } + case 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH': { + if (state.channelDisconnected || state.beyondPageOne) return state + + const incomingInternalTransactions = humps.camelizeKeys(action.msgs) + .filter(({toAddressHash, fromAddressHash}) => ( + !state.filter || + (state.filter === 'to' && toAddressHash === state.addressHash) || + (state.filter === 'from' && fromAddressHash === state.addressHash) + )) + + if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { + return Object.assign({}, state, { + newInternalTransactions: [ + ...state.newInternalTransactions, + ...incomingInternalTransactions.map(({internalTransactionHtml}) => internalTransactionHtml) + ] + }) + } else { + return Object.assign({}, state, { + batchCountAccumulator: state.batchCountAccumulator + action.msgs.length + }) + } + } case 'RECEIVED_NEW_TRANSACTION_BATCH': { if (state.channelDisconnected || state.beyondPageOne) return state @@ -74,30 +99,44 @@ export function reducer (state = initialState, action) { router.when('/address/:addressHash').then((params) => initRedux(reducer, { main (store) { - const { addressHash, blockNumber } = params - const channel = socket.channel(`addresses:${addressHash}`, {}) - store.dispatch({ + const { addressHash } = params + const addressChannel = socket.channel(`addresses:${addressHash}`, {}) + const state = store.dispatch({ type: 'PAGE_LOAD', params, transactionCount: $('[data-selector="transaction-count"]').text() }) - channel.join() - channel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - 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 }))) + addressChannel.join() + addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + addressChannel.on('balance', (msg) => store.dispatch({ type: 'RECEIVED_UPDATED_BALANCE', msg })) + if (!state.beyondPageOne) { + addressChannel.on('transaction', batchChannel((msgs) => + store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs }) + )) + + addressChannel.on('internal_transaction', batchChannel((msgs) => + store.dispatch({ type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', msgs }) + )) + } }, render (state, oldState) { + const $balance = $('[data-selector="balance-card"]') const $channelBatching = $('[data-selector="channel-batching-message"]') const $channelBatchingCount = $('[data-selector="channel-batching-count"]') const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') + const $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]') const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]') - const $balance = $('[data-selector="balance-card"]') + const $internalTransactionsList = $('[data-selector="internal-transactions-list"]') const $transactionCount = $('[data-selector="transaction-count"]') const $transactionsList = $('[data-selector="transactions-list"]') + if ($emptyInternalTransactionsList.length && state.newInternalTransactions.length) window.location.reload() if ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload() if (state.channelDisconnected) $channelDisconnected.show() - if (oldState.balance !== state.balance) $balance.empty().append(state.balance) + if (oldState.balance !== state.balance) { + $balance.empty().append(state.balance) + loadTokenBalanceDropdown() + } if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) if (state.batchCountAccumulator) { $channelBatching.show() @@ -105,6 +144,9 @@ router.when('/address/:addressHash').then((params) => initRedux(reducer, { } else { $channelBatching.hide() } + if (oldState.newInternalTransactions !== state.newInternalTransactions && $internalTransactionsList.length) { + $internalTransactionsList.prepend(state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join('')) + } if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) { $transactionsList.prepend(state.newTransactions.slice(oldState.newTransactions.length).reverse().join('')) updateAllAges() diff --git a/apps/block_scout_web/assets/js/pages/block.js b/apps/block_scout_web/assets/js/pages/block.js index 2c32d9ffae..09e1899713 100644 --- a/apps/block_scout_web/assets/js/pages/block.js +++ b/apps/block_scout_web/assets/js/pages/block.js @@ -19,14 +19,12 @@ export function reducer (state = initialState, action) { }) } case 'CHANNEL_DISCONNECTED': { - if (state.beyondPageOne) return state - return Object.assign({}, state, { channelDisconnected: true }) } case 'RECEIVED_NEW_BLOCK': { - if (state.channelDisconnected || state.beyondPageOne) return state + if (state.channelDisconnected) return state return Object.assign({}, state, { newBlock: action.msg.blockHtml @@ -39,13 +37,15 @@ export function reducer (state = initialState, action) { router.when('/blocks', { exactPathMatch: true }).then(({ blockNumber }) => initRedux(reducer, { main (store) { - const blocksChannel = socket.channel(`blocks:new_block`, {}) - store.dispatch({ type: 'PAGE_LOAD', blockNumber }) - blocksChannel.join() - blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - blocksChannel.on('new_block', (msg) => - store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) }) - ) + const state = store.dispatch({ type: 'PAGE_LOAD', blockNumber }) + if (!state.beyondPageOne) { + const blocksChannel = socket.channel(`blocks:new_block`, {}) + blocksChannel.join() + blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + blocksChannel.on('new_block', (msg) => + store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) }) + ) + } }, render (state, oldState) { const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') diff --git a/apps/block_scout_web/assets/js/pages/transaction.js b/apps/block_scout_web/assets/js/pages/transaction.js index 84183423e9..50f9fc89cb 100644 --- a/apps/block_scout_web/assets/js/pages/transaction.js +++ b/apps/block_scout_web/assets/js/pages/transaction.js @@ -28,8 +28,6 @@ export function reducer (state = initialState, action) { }) } case 'CHANNEL_DISCONNECTED': { - if (state.beyondPageOne) return state - return Object.assign({}, state, { channelDisconnected: true, batchCountAccumulator: 0 @@ -88,17 +86,19 @@ router.when('/tx/:transactionHash').then(() => initRedux(reducer, { router.when('/txs', { exactPathMatch: true }).then((params) => initRedux(reducer, { main (store) { const { index } = params - const transactionsChannel = socket.channel(`transactions:new_transaction`) - store.dispatch({ + const state = store.dispatch({ type: 'PAGE_LOAD', transactionCount: $('[data-selector="transaction-count"]').text(), index }) - transactionsChannel.join() - transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - transactionsChannel.on('new_transaction', batchChannel((msgs) => - store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) - ) + if (!state.beyondPageOne) { + const transactionsChannel = socket.channel(`transactions:new_transaction`) + transactionsChannel.join() + transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + transactionsChannel.on('new_transaction', batchChannel((msgs) => + store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) + ) + } }, render (state, oldState) { const $channelBatching = $('[data-selector="channel-batching-message"]') diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 87299c4bf0..9d68396390 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -18,6 +18,26 @@ defmodule BlockScoutWeb.Chain do @page_size 50 @default_paging_options %PagingOptions{page_size: @page_size + 1} + def current_filter(%{paging_options: paging_options} = params) do + params + |> Map.get("filter") + |> case do + "to" -> [direction: :to, paging_options: paging_options] + "from" -> [direction: :from, paging_options: paging_options] + _ -> [paging_options: paging_options] + end + end + + def current_filter(params) do + params + |> Map.get("filter") + |> case do + "to" -> [direction: :to] + "from" -> [direction: :from] + _ -> [] + end + end + @spec from_param(String.t()) :: {:ok, Address.t() | Block.t() | Transaction.t()} | {:error, :not_found} def from_param(param) 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 4a177eee86..0bd9d484d9 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 @@ -4,10 +4,10 @@ defmodule BlockScoutWeb.AddressChannel do """ use BlockScoutWeb, :channel - alias BlockScoutWeb.{AddressTransactionView, AddressView} + alias BlockScoutWeb.{AddressInternalTransactionView, AddressView, TransactionView} alias Phoenix.View - intercept(["balance_update", "count", "transaction"]) + intercept(["balance_update", "count", "internal_transaction", "transaction"]) def join("addresses:" <> _address_hash, _params, socket) do {:ok, %{}, socket} @@ -40,13 +40,33 @@ defmodule BlockScoutWeb.AddressChannel do {:noreply, socket} end + def handle_out("internal_transaction", %{address: address, internal_transaction: internal_transaction}, socket) do + Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) + + rendered_internal_transaction = + View.render_to_string( + AddressInternalTransactionView, + "_internal_transaction.html", + address: address, + internal_transaction: internal_transaction + ) + + push(socket, "internal_transaction", %{ + to_address_hash: to_string(internal_transaction.to_address_hash), + from_address_hash: to_string(internal_transaction.from_address_hash), + internal_transaction_html: rendered_internal_transaction + }) + + {:noreply, socket} + end + def handle_out("transaction", %{address: address, transaction: transaction}, socket) do Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) rendered = View.render_to_string( - AddressTransactionView, - "_transaction.html", + TransactionView, + "_tile.html", address: address, transaction: transaction ) 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 cf70be275d..de65aa50aa 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 @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do use BlockScoutWeb, :controller import BlockScoutWeb.AddressController, only: [transaction_count: 1] - import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1] alias Explorer.{Chain, Market} alias Explorer.ExchangeRates.Token @@ -46,24 +46,4 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do not_found(conn) end end - - defp current_filter(%{paging_options: paging_options} = params) do - params - |> Map.get("filter") - |> case do - "to" -> [direction: :to, paging_options: paging_options] - "from" -> [direction: :from, paging_options: paging_options] - _ -> [paging_options: paging_options] - end - end - - defp current_filter(params) do - params - |> Map.get("filter") - |> case do - "to" -> [direction: :to] - "from" -> [direction: :from] - _ -> [] - end - end end 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 5c406dcc63..5bf5cccc43 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 @@ -6,7 +6,7 @@ defmodule BlockScoutWeb.AddressTransactionController do use BlockScoutWeb, :controller import BlockScoutWeb.AddressController, only: [transaction_count: 1] - import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1] + import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1] alias Explorer.{Chain, Market} alias Explorer.ExchangeRates.Token @@ -46,24 +46,4 @@ defmodule BlockScoutWeb.AddressTransactionController do not_found(conn) end end - - defp current_filter(%{paging_options: paging_options} = params) do - params - |> Map.get("filter") - |> case do - "to" -> [direction: :to, paging_options: paging_options] - "from" -> [direction: :from, paging_options: paging_options] - _ -> [paging_options: paging_options] - end - end - - defp current_filter(params) do - params - |> Map.get("filter") - |> case do - "to" -> [direction: :to] - "from" -> [direction: :from] - _ -> [] - end - end end diff --git a/apps/block_scout_web/lib/block_scout_web/event_handler.ex b/apps/block_scout_web/lib/block_scout_web/event_handler.ex index a66107d656..5b7bf51118 100644 --- a/apps/block_scout_web/lib/block_scout_web/event_handler.ex +++ b/apps/block_scout_web/lib/block_scout_web/event_handler.ex @@ -19,6 +19,7 @@ defmodule BlockScoutWeb.EventHandler do Chain.subscribe_to_events(:addresses) Chain.subscribe_to_events(:blocks) Chain.subscribe_to_events(:exchange_rate) + Chain.subscribe_to_events(:internal_transactions) Chain.subscribe_to_events(:transactions) {:ok, []} end 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 2546a352dc..849daaa2c7 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Notifier do """ alias Explorer.{Chain, Market, Repo} - alias Explorer.Chain.Address + alias Explorer.Chain.{Address, InternalTransaction} alias Explorer.ExchangeRates.Token alias BlockScoutWeb.Endpoint @@ -36,6 +36,12 @@ defmodule BlockScoutWeb.Notifier do }) end + def handle_event({:chain_event, :internal_transactions, internal_transactions}) do + internal_transactions + |> Stream.map(&(InternalTransaction |> Repo.get(&1.id) |> Repo.preload([:from_address, :to_address]))) + |> Enum.each(&broadcast_internal_transaction/1) + end + def handle_event({:chain_event, :transactions, transaction_hashes}) do transaction_hashes |> Chain.hashes_to_transactions( @@ -65,6 +71,24 @@ defmodule BlockScoutWeb.Notifier do }) end + defp broadcast_internal_transaction(internal_transaction) do + Endpoint.broadcast("internal_transactions:new_internal_transaction", "new_internal_transaction", %{ + internal_transaction: internal_transaction + }) + + Endpoint.broadcast("addresses:#{internal_transaction.from_address_hash}", "internal_transaction", %{ + address: internal_transaction.from_address, + internal_transaction: internal_transaction + }) + + if internal_transaction.to_address_hash != internal_transaction.from_address_hash do + Endpoint.broadcast("addresses:#{internal_transaction.to_address_hash}", "internal_transaction", %{ + address: internal_transaction.to_address, + internal_transaction: internal_transaction + }) + end + end + defp broadcast_transaction(transaction) do Endpoint.broadcast("transactions:new_transaction", "new_transaction", %{ transaction: transaction @@ -75,7 +99,7 @@ defmodule BlockScoutWeb.Notifier do transaction: transaction }) - if transaction.to_address_hash && transaction.to_address_hash != transaction.from_address_hash do + if transaction.to_address_hash != transaction.from_address_hash do Endpoint.broadcast("addresses:#{transaction.to_address_hash}", "transaction", %{ address: transaction.to_address, transaction: transaction 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 3471ddc699..4d1ed942ae 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 @@ -9,7 +9,7 @@ data-usd-exchange-rate="<%= @exchange_rate.usd_value %>"> -