Merge pull request #615 from poanetwork/312-live-update-internal-transactions-on-address-page

312 live update internal transactions on address page
pull/678/head
Andrew Cravenho 6 years ago committed by GitHub
commit 5fca850d55
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 13
      apps/block_scout_web/assets/__tests__/pages/block.js
  2. 3
      apps/block_scout_web/assets/js/lib/token_balance_dropdown.js
  3. 60
      apps/block_scout_web/assets/js/pages/address.js
  4. 8
      apps/block_scout_web/assets/js/pages/block.js
  5. 8
      apps/block_scout_web/assets/js/pages/transaction.js
  6. 20
      apps/block_scout_web/lib/block_scout_web/chain.ex
  7. 28
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  8. 22
      apps/block_scout_web/lib/block_scout_web/controllers/address_internal_transaction_controller.ex
  9. 22
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  10. 1
      apps/block_scout_web/lib/block_scout_web/event_handler.ex
  11. 28
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  12. 2
      apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex
  13. 14
      apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex
  14. 4
      apps/block_scout_web/lib/block_scout_web/views/block_transaction_view.ex
  15. 22
      apps/block_scout_web/priv/gettext/default.pot
  16. 22
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  17. 18
      apps/block_scout_web/test/block_scout_web/chain_test.exs
  18. 70
      apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs
  19. 40
      apps/block_scout_web/test/block_scout_web/channels/exchange_rate_channel_test.exs
  20. 24
      apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs
  21. 16
      apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex
  22. 12
      apps/block_scout_web/test/block_scout_web/features/pages/chain_page.ex
  23. 4
      apps/block_scout_web/test/block_scout_web/features/pages/transaction_list_page.ex
  24. 4
      apps/block_scout_web/test/block_scout_web/features/pages/transaction_page.ex
  25. 25
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  26. 29
      apps/block_scout_web/test/block_scout_web/views/block_view_test.exs
  27. 5
      apps/explorer/lib/explorer/chain.ex
  28. 7
      apps/explorer/lib/explorer/chain/import.ex
  29. 6
      apps/explorer/test/explorer/chain/import_test.exs

@ -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")
})

@ -16,4 +16,7 @@ const tokenBalanceDropdown = (element) => {
}) })
} }
export function loadTokenBalanceDropdown () {
$('[data-token-balance-dropdown]').each((_index, element) => tokenBalanceDropdown(element)) $('[data-token-balance-dropdown]').each((_index, element) => tokenBalanceDropdown(element))
}
loadTokenBalanceDropdown()

@ -5,6 +5,7 @@ import socket from '../socket'
import router from '../router' import router from '../router'
import { batchChannel, initRedux } from '../utils' import { batchChannel, initRedux } from '../utils'
import { updateAllAges } from '../lib/from_now' import { updateAllAges } from '../lib/from_now'
import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown'
const BATCH_THRESHOLD = 10 const BATCH_THRESHOLD = 10
@ -14,6 +15,7 @@ export const initialState = {
beyondPageOne: null, beyondPageOne: null,
channelDisconnected: false, channelDisconnected: false,
filter: null, filter: null,
newInternalTransactions: [],
newTransactions: [], newTransactions: [],
balance: null, balance: null,
transactionCount: null transactionCount: null
@ -42,6 +44,29 @@ export function reducer (state = initialState, action) {
balance: action.msg.balance 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': { case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected || state.beyondPageOne) return state 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, { router.when('/address/:addressHash').then((params) => initRedux(reducer, {
main (store) { main (store) {
const { addressHash, blockNumber } = params const { addressHash } = params
const channel = socket.channel(`addresses:${addressHash}`, {}) const addressChannel = socket.channel(`addresses:${addressHash}`, {})
store.dispatch({ const state = store.dispatch({
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
params, params,
transactionCount: $('[data-selector="transaction-count"]').text() transactionCount: $('[data-selector="transaction-count"]').text()
}) })
channel.join() addressChannel.join()
channel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
channel.on('balance', (msg) => store.dispatch({ type: 'RECEIVED_UPDATED_BALANCE', msg })) addressChannel.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 (!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) { render (state, oldState) {
const $balance = $('[data-selector="balance-card"]')
const $channelBatching = $('[data-selector="channel-batching-message"]') const $channelBatching = $('[data-selector="channel-batching-message"]')
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 $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]')
const $emptyTransactionsList = $('[data-selector="empty-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 $transactionCount = $('[data-selector="transaction-count"]')
const $transactionsList = $('[data-selector="transactions-list"]') 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 ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload()
if (state.channelDisconnected) $channelDisconnected.show() 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 (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format())
if (state.batchCountAccumulator) { if (state.batchCountAccumulator) {
$channelBatching.show() $channelBatching.show()
@ -105,6 +144,9 @@ router.when('/address/:addressHash').then((params) => initRedux(reducer, {
} else { } else {
$channelBatching.hide() $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) { if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) {
$transactionsList.prepend(state.newTransactions.slice(oldState.newTransactions.length).reverse().join('')) $transactionsList.prepend(state.newTransactions.slice(oldState.newTransactions.length).reverse().join(''))
updateAllAges() updateAllAges()

@ -19,14 +19,12 @@ export function reducer (state = initialState, action) {
}) })
} }
case 'CHANNEL_DISCONNECTED': { case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state
return Object.assign({}, state, { return Object.assign({}, state, {
channelDisconnected: true channelDisconnected: true
}) })
} }
case 'RECEIVED_NEW_BLOCK': { case 'RECEIVED_NEW_BLOCK': {
if (state.channelDisconnected || state.beyondPageOne) return state if (state.channelDisconnected) return state
return Object.assign({}, state, { return Object.assign({}, state, {
newBlock: action.msg.blockHtml newBlock: action.msg.blockHtml
@ -39,13 +37,15 @@ export function reducer (state = initialState, action) {
router.when('/blocks', { exactPathMatch: true }).then(({ blockNumber }) => initRedux(reducer, { router.when('/blocks', { exactPathMatch: true }).then(({ blockNumber }) => initRedux(reducer, {
main (store) { main (store) {
const state = store.dispatch({ type: 'PAGE_LOAD', blockNumber })
if (!state.beyondPageOne) {
const blocksChannel = socket.channel(`blocks:new_block`, {}) const blocksChannel = socket.channel(`blocks:new_block`, {})
store.dispatch({ type: 'PAGE_LOAD', blockNumber })
blocksChannel.join() blocksChannel.join()
blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
blocksChannel.on('new_block', (msg) => blocksChannel.on('new_block', (msg) =>
store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) }) store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })
) )
}
}, },
render (state, oldState) { render (state, oldState) {
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')

@ -28,8 +28,6 @@ export function reducer (state = initialState, action) {
}) })
} }
case 'CHANNEL_DISCONNECTED': { case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state
return Object.assign({}, state, { return Object.assign({}, state, {
channelDisconnected: true, channelDisconnected: true,
batchCountAccumulator: 0 batchCountAccumulator: 0
@ -88,17 +86,19 @@ router.when('/tx/:transactionHash').then(() => initRedux(reducer, {
router.when('/txs', { exactPathMatch: true }).then((params) => initRedux(reducer, { router.when('/txs', { exactPathMatch: true }).then((params) => initRedux(reducer, {
main (store) { main (store) {
const { index } = params const { index } = params
const transactionsChannel = socket.channel(`transactions:new_transaction`) const state = store.dispatch({
store.dispatch({
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text(), transactionCount: $('[data-selector="transaction-count"]').text(),
index index
}) })
if (!state.beyondPageOne) {
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join() transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('new_transaction', batchChannel((msgs) => transactionsChannel.on('new_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
) )
}
}, },
render (state, oldState) { render (state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]') const $channelBatching = $('[data-selector="channel-batching-message"]')

@ -18,6 +18,26 @@ defmodule BlockScoutWeb.Chain do
@page_size 50 @page_size 50
@default_paging_options %PagingOptions{page_size: @page_size + 1} @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} @spec from_param(String.t()) :: {:ok, Address.t() | Block.t() | Transaction.t()} | {:error, :not_found}
def from_param(param) def from_param(param)

@ -4,10 +4,10 @@ defmodule BlockScoutWeb.AddressChannel do
""" """
use BlockScoutWeb, :channel use BlockScoutWeb, :channel
alias BlockScoutWeb.{AddressTransactionView, AddressView} alias BlockScoutWeb.{AddressInternalTransactionView, AddressView, TransactionView}
alias Phoenix.View alias Phoenix.View
intercept(["balance_update", "count", "transaction"]) intercept(["balance_update", "count", "internal_transaction", "transaction"])
def join("addresses:" <> _address_hash, _params, socket) do def join("addresses:" <> _address_hash, _params, socket) do
{:ok, %{}, socket} {:ok, %{}, socket}
@ -40,13 +40,33 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket} {:noreply, socket}
end 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 def handle_out("transaction", %{address: address, transaction: transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
rendered = rendered =
View.render_to_string( View.render_to_string(
AddressTransactionView, TransactionView,
"_transaction.html", "_tile.html",
address: address, address: address,
transaction: transaction transaction: transaction
) )

@ -6,7 +6,7 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
import BlockScoutWeb.AddressController, only: [transaction_count: 1] 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.{Chain, Market}
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
@ -46,24 +46,4 @@ defmodule BlockScoutWeb.AddressInternalTransactionController do
not_found(conn) not_found(conn)
end end
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 end

@ -6,7 +6,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
use BlockScoutWeb, :controller use BlockScoutWeb, :controller
import BlockScoutWeb.AddressController, only: [transaction_count: 1] 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.{Chain, Market}
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
@ -46,24 +46,4 @@ defmodule BlockScoutWeb.AddressTransactionController do
not_found(conn) not_found(conn)
end end
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 end

@ -19,6 +19,7 @@ defmodule BlockScoutWeb.EventHandler do
Chain.subscribe_to_events(:addresses) Chain.subscribe_to_events(:addresses)
Chain.subscribe_to_events(:blocks) Chain.subscribe_to_events(:blocks)
Chain.subscribe_to_events(:exchange_rate) Chain.subscribe_to_events(:exchange_rate)
Chain.subscribe_to_events(:internal_transactions)
Chain.subscribe_to_events(:transactions) Chain.subscribe_to_events(:transactions)
{:ok, []} {:ok, []}
end end

@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Notifier do
""" """
alias Explorer.{Chain, Market, Repo} alias Explorer.{Chain, Market, Repo}
alias Explorer.Chain.Address alias Explorer.Chain.{Address, InternalTransaction}
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias BlockScoutWeb.Endpoint alias BlockScoutWeb.Endpoint
@ -36,6 +36,12 @@ defmodule BlockScoutWeb.Notifier do
}) })
end 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 def handle_event({:chain_event, :transactions, transaction_hashes}) do
transaction_hashes transaction_hashes
|> Chain.hashes_to_transactions( |> Chain.hashes_to_transactions(
@ -65,6 +71,24 @@ defmodule BlockScoutWeb.Notifier do
}) })
end 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 defp broadcast_transaction(transaction) do
Endpoint.broadcast("transactions:new_transaction", "new_transaction", %{ Endpoint.broadcast("transactions:new_transaction", "new_transaction", %{
transaction: transaction transaction: transaction
@ -75,7 +99,7 @@ defmodule BlockScoutWeb.Notifier do
transaction: transaction 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", %{ Endpoint.broadcast("addresses:#{transaction.to_address_hash}", "transaction", %{
address: transaction.to_address, address: transaction.to_address,
transaction: transaction transaction: transaction

@ -9,7 +9,7 @@
data-usd-exchange-rate="<%= @exchange_rate.usd_value %>"> data-usd-exchange-rate="<%= @exchange_rate.usd_value %>">
</span> </span>
<div class="mt-3" data-token-balance-dropdown data-api_path="<%= address_token_balance_path(@conn, :index, @address.hash) %>"> <div class="mt-3" data-token-balance-dropdown data-api_path="<%= address_token_balance_path(BlockScoutWeb.Endpoint, :index, @address.hash) %>">
<p data-loading class="mb-0 text-light" style="display: none;"> <p data-loading class="mb-0 text-light" style="display: none;">
<i class="fa fa-spinner fa-spin"></i> <i class="fa fa-spinner fa-spin"></i>
<%= gettext("Fetching tokens...") %> <%= gettext("Fetching tokens...") %>

@ -78,6 +78,16 @@
</ul> </ul>
</div> </div>
<div class="card-body"> <div class="card-body">
<div data-selector="channel-batching-message" style="display:none;">
<div data-selector="reload-button" class="alert alert-info">
<a href="#" class="alert-link"><span data-selector="channel-batching-count"></span> <%= gettext "More internal transactions have come in" %></a>
</div>
</div>
<div data-selector="channel-disconnected-message" style="display:none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer internal transactions" %></a>
</div>
</div>
<div class="dropdown float-right u-push-sm"> <div class="dropdown float-right u-push-sm">
<button data-test="filter_dropdown" class="button button--secondary button--xsmall dropdown-toggle" type="button" <button data-test="filter_dropdown" class="button button--secondary button--xsmall dropdown-toggle" type="button"
id="dropdownMenu2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false"> id="dropdownMenu2" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
@ -116,12 +126,14 @@
</div> </div>
<h2 class="card-title"><%= gettext "Internal Transactions" %></h2> <h2 class="card-title"><%= gettext "Internal Transactions" %></h2>
<%= if Enum.count(@internal_transactions) > 0 do %> <%= if Enum.count(@internal_transactions) > 0 do %>
<span data-selector="internal-transactions-list">
<%= for internal_transaction <- @internal_transactions do %> <%= for internal_transaction <- @internal_transactions do %>
<%= render "_internal_transaction.html", address: @address, internal_transaction: internal_transaction %> <%= render "_internal_transaction.html", address: @address, internal_transaction: internal_transaction %>
<% end %> <% end %>
</span>
<% else %> <% else %>
<div class="tile tile-muted text-center"> <div class="tile tile-muted text-center">
<span><%= gettext "There are no internal transactions for this address." %></span> <span data-selector="empty-internal-transactions-list"><%= gettext "There are no internal transactions for this address." %></span>
</div> </div>
<% end %> <% end %>
<div> <div>

@ -1,7 +1,3 @@
defmodule BlockScoutWeb.BlockTransactionView do defmodule BlockScoutWeb.BlockTransactionView do
use BlockScoutWeb, :view use BlockScoutWeb, :view
alias BlockScoutWeb.BlockView
defdelegate formatted_timestamp(block), to: BlockView
end end

@ -134,7 +134,7 @@ msgstr ""
msgid "Address" msgid "Address"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:105 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:115
#: lib/block_scout_web/templates/address_transaction/index.html.eex:116 #: lib/block_scout_web/templates/address_transaction/index.html.eex:116
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10
@ -150,7 +150,7 @@ msgstr ""
msgid "Success" msgid "Success"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:94 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:104
#: lib/block_scout_web/templates/address_transaction/index.html.eex:105 #: lib/block_scout_web/templates/address_transaction/index.html.eex:105
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_transaction_view.ex:9 #: lib/block_scout_web/views/address_transaction_view.ex:9
@ -305,7 +305,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:20 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:20
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:117 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:127
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_transaction/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:20
#: lib/block_scout_web/templates/address_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_transaction/index.html.eex:60
@ -354,7 +354,7 @@ msgstr ""
msgid "Wei" msgid "Wei"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:88 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:98
#: lib/block_scout_web/templates/address_transaction/index.html.eex:99 #: lib/block_scout_web/templates/address_transaction/index.html.eex:99
#: lib/block_scout_web/views/address_internal_transaction_view.ex:11 #: lib/block_scout_web/views/address_internal_transaction_view.ex:11
#: lib/block_scout_web/views/address_transaction_view.ex:11 #: lib/block_scout_web/views/address_transaction_view.ex:11
@ -476,7 +476,7 @@ msgstr ""
msgid "There are no Transactions" msgid "There are no Transactions"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:130 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:142
#: lib/block_scout_web/templates/address_transaction/index.html.eex:143 #: lib/block_scout_web/templates/address_transaction/index.html.eex:143
#: lib/block_scout_web/templates/block/index.html.eex:15 #: lib/block_scout_web/templates/block/index.html.eex:15
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -606,7 +606,7 @@ msgid "QR Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:124 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:136
msgid "There are no internal transactions for this address." msgid "There are no internal transactions for this address."
msgstr "" msgstr ""
@ -1000,3 +1000,13 @@ msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:42 #: lib/block_scout_web/views/transaction_view.ex:42
msgid "Max of" msgid "Max of"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:88
msgid "Connection Lost, click to load newer internal transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:83
msgid "More internal transactions have come in"
msgstr ""

@ -146,7 +146,7 @@ msgstr "%{count} transactions in this block"
msgid "Address" msgid "Address"
msgstr "Address" msgstr "Address"
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:105 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:115
#: lib/block_scout_web/templates/address_transaction/index.html.eex:116 #: lib/block_scout_web/templates/address_transaction/index.html.eex:116
#: lib/block_scout_web/views/address_internal_transaction_view.ex:10 #: lib/block_scout_web/views/address_internal_transaction_view.ex:10
#: lib/block_scout_web/views/address_transaction_view.ex:10 #: lib/block_scout_web/views/address_transaction_view.ex:10
@ -162,7 +162,7 @@ msgstr "Overview"
msgid "Success" msgid "Success"
msgstr "Success" msgstr "Success"
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:94 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:104
#: lib/block_scout_web/templates/address_transaction/index.html.eex:105 #: lib/block_scout_web/templates/address_transaction/index.html.eex:105
#: lib/block_scout_web/views/address_internal_transaction_view.ex:9 #: lib/block_scout_web/views/address_internal_transaction_view.ex:9
#: lib/block_scout_web/views/address_transaction_view.ex:9 #: lib/block_scout_web/views/address_transaction_view.ex:9
@ -317,7 +317,7 @@ msgstr ""
#: lib/block_scout_web/templates/address_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:20 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:20
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:60
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:117 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:127
#: lib/block_scout_web/templates/address_read_contract/index.html.eex:17 #: lib/block_scout_web/templates/address_read_contract/index.html.eex:17
#: lib/block_scout_web/templates/address_transaction/index.html.eex:20 #: lib/block_scout_web/templates/address_transaction/index.html.eex:20
#: lib/block_scout_web/templates/address_transaction/index.html.eex:60 #: lib/block_scout_web/templates/address_transaction/index.html.eex:60
@ -366,7 +366,7 @@ msgstr ""
msgid "Wei" msgid "Wei"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:88 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:98
#: lib/block_scout_web/templates/address_transaction/index.html.eex:99 #: lib/block_scout_web/templates/address_transaction/index.html.eex:99
#: lib/block_scout_web/views/address_internal_transaction_view.ex:11 #: lib/block_scout_web/views/address_internal_transaction_view.ex:11
#: lib/block_scout_web/views/address_transaction_view.ex:11 #: lib/block_scout_web/views/address_transaction_view.ex:11
@ -488,7 +488,7 @@ msgstr ""
msgid "There are no Transactions" msgid "There are no Transactions"
msgstr "" msgstr ""
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:130 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:142
#: lib/block_scout_web/templates/address_transaction/index.html.eex:143 #: lib/block_scout_web/templates/address_transaction/index.html.eex:143
#: lib/block_scout_web/templates/block/index.html.eex:15 #: lib/block_scout_web/templates/block/index.html.eex:15
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50 #: lib/block_scout_web/templates/block_transaction/index.html.eex:50
@ -618,7 +618,7 @@ msgid "QR Code"
msgstr "" msgstr ""
#, elixir-format #, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:124 #: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:136
msgid "There are no internal transactions for this address." msgid "There are no internal transactions for this address."
msgstr "" msgstr ""
@ -1012,3 +1012,13 @@ msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:42 #: lib/block_scout_web/views/transaction_view.ex:42
msgid "Max of" msgid "Max of"
msgstr "" msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:88
msgid "Connection Lost, click to load newer internal transactions"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_internal_transaction/index.html.eex:83
msgid "More internal transactions have come in"
msgstr ""

@ -4,6 +4,24 @@ defmodule BlockScoutWeb.ChainTest do
alias Explorer.Chain.{Address, Block, Transaction} alias Explorer.Chain.{Address, Block, Transaction}
alias BlockScoutWeb.Chain alias BlockScoutWeb.Chain
describe "current_filter/1" do
test "sets direction based on to filter" do
assert [direction: :to] = Chain.current_filter(%{"filter" => "to"})
end
test "sets direction based on from filter" do
assert [direction: :from] = Chain.current_filter(%{"filter" => "from"})
end
test "no direction set" do
assert [] = Chain.current_filter(%{})
end
test "no direction set with paging_options" do
assert [paging_options: "test"] = Chain.current_filter(%{paging_options: "test"})
end
end
describe "from_param/1" do describe "from_param/1" do
test "finds a block by block number with a valid block number" do test "finds a block by block number with a valid block number" do
%Block{number: number} = insert(:block, number: 37) %Block{number: number} = insert(:block, number: 37)

@ -110,5 +110,75 @@ defmodule BlockScoutWeb.AddressChannelTest do
100 -> assert true 100 -> assert true
end end
end end
test "notified of new_internal_transaction for matching from_address", %{address: address, topic: topic} do
transaction =
:transaction
|> insert(from_address: address)
|> with_block()
internal_transaction = insert(:internal_transaction, transaction: transaction, from_address: address, index: 0)
Notifier.handle_event({:chain_event, :internal_transactions, [internal_transaction]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "internal_transaction", payload: payload} ->
assert payload.address.hash == address.hash
assert payload.internal_transaction.id == internal_transaction.id
after
5_000 ->
assert false, "Expected message received nothing."
end
end
test "notified of new_internal_transaction for matching to_address", %{address: address, topic: topic} do
transaction =
:transaction
|> insert(to_address: address)
|> with_block()
internal_transaction = insert(:internal_transaction, transaction: transaction, to_address: address, index: 0)
Notifier.handle_event({:chain_event, :internal_transactions, [internal_transaction]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "internal_transaction", payload: payload} ->
assert payload.address.hash == address.hash
assert payload.internal_transaction.id == internal_transaction.id
after
5_000 ->
assert false, "Expected message received nothing."
end
end
test "not notified twice of new_internal_transaction if to and from address are equal", %{
address: address,
topic: topic
} do
transaction =
:transaction
|> insert(from_address: address, to_address: address)
|> with_block()
internal_transaction =
insert(:internal_transaction, transaction: transaction, from_address: address, to_address: address, index: 0)
Notifier.handle_event({:chain_event, :internal_transactions, [internal_transaction]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "internal_transaction", payload: payload} ->
assert payload.address.hash == address.hash
assert payload.internal_transaction.id == internal_transaction.id
after
5_000 ->
assert false, "Expected message received nothing."
end
receive do
_ -> assert false, "Received duplicate broadcast."
after
100 -> assert true
end
end
end end
end end

@ -7,6 +7,7 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
alias Explorer.ExchangeRates alias Explorer.ExchangeRates
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Explorer.ExchangeRates.Source.TestSource alias Explorer.ExchangeRates.Source.TestSource
alias Explorer.Market
setup :verify_on_exit! setup :verify_on_exit!
@ -25,7 +26,7 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
market_cap_usd: Decimal.new("1000000.0"), market_cap_usd: Decimal.new("1000000.0"),
name: "test", name: "test",
symbol: Explorer.coin(), symbol: Explorer.coin(),
usd_value: Decimal.new("1.0"), usd_value: Decimal.new("2.5"),
volume_24h_usd: Decimal.new("1000.0") volume_24h_usd: Decimal.new("1000.0")
} }
@ -36,7 +37,8 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
{:ok, %{token: token}} {:ok, %{token: token}}
end end
test "subscribed user is notified of new_rate event", %{token: token} do describe "new_rate" do
test "subscribed user is notified", %{token: token} do
ExchangeRates.handle_info({nil, {:ok, [token]}}, %{}) ExchangeRates.handle_info({nil, {:ok, [token]}}, %{})
topic = "exchange_rate:new_rate" topic = "exchange_rate:new_rate"
@ -47,9 +49,43 @@ defmodule BlockScoutWeb.ExchangeRateChannelTest do
receive do receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "new_rate", payload: payload} -> %Phoenix.Socket.Broadcast{topic: ^topic, event: "new_rate", payload: payload} ->
assert payload.exchange_rate == token assert payload.exchange_rate == token
assert payload.market_history_data == []
after after
5_000 -> 5_000 ->
assert false, "Expected message received nothing." assert false, "Expected message received nothing."
end end
end end
test "subscribed user is notified with market history", %{token: token} do
ExchangeRates.handle_info({nil, {:ok, [token]}}, %{})
today = Date.utc_today()
old_records =
for i <- 1..29 do
%{
date: Timex.shift(today, days: i * -1),
closing_price: Decimal.new(1)
}
end
records = [%{date: today, closing_price: token.usd_value} | old_records]
Market.bulk_insert_history(records)
topic = "exchange_rate:new_rate"
@endpoint.subscribe(topic)
Notifier.handle_event({:chain_event, :exchange_rate})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "new_rate", payload: payload} ->
assert payload.exchange_rate == token
assert payload.market_history_data == records
after
5_000 ->
assert false, "Expected message received nothing."
end
end
end
end end

@ -6,6 +6,14 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
setup :verify_on_exit! setup :verify_on_exit!
describe "GET index/3" do describe "GET index/3" do
test "error for invalid address", %{conn: conn} do
path = smart_contract_path(BlockScoutWeb.Endpoint, :index, :en, hash: "0x00")
conn = get(conn, path)
assert conn.status == 404
end
test "only responds to ajax requests", %{conn: conn} do test "only responds to ajax requests", %{conn: conn} do
smart_contract = insert(:smart_contract) smart_contract = insert(:smart_contract)
@ -36,6 +44,22 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
end end
describe "GET show/3" do describe "GET show/3" do
test "error for invalid address", %{conn: conn} do
path =
smart_contract_path(
BlockScoutWeb.Endpoint,
:show,
:en,
"0x00",
function_name: "get",
args: []
)
conn = get(conn, path)
assert conn.status == 404
end
test "only responds to ajax requests", %{conn: conn} do test "only responds to ajax requests", %{conn: conn} do
smart_contract = insert(:smart_contract) smart_contract = insert(:smart_contract)

@ -31,15 +31,19 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-test='address_detail_hash']", text: to_string(address_hash)) css("[data-test='address_detail_hash']", text: to_string(address_hash))
end end
def internal_transaction(%InternalTransaction{id: id}) do
css("[data-test='internal_transaction'][data-internal-transaction-id='#{id}']")
end
def internal_transactions(count: count) do def internal_transactions(count: count) do
css("[data-test='internal_transaction']", count: count) css("[data-test='internal_transaction']", count: count)
end end
def internal_transaction_address_link(%InternalTransaction{id: id, to_address_hash: address_hash}, :to) do def internal_transaction_address_link(%InternalTransaction{id: id, from_address_hash: address_hash}, :from) do
css("[data-internal-transaction-id='#{id}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']") css("[data-internal-transaction-id='#{id}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']")
end end
def internal_transaction_address_link(%InternalTransaction{id: id, from_address_hash: address_hash}, :from) do def internal_transaction_address_link(%InternalTransaction{id: id, to_address_hash: address_hash}, :to) do
css("[data-internal-transaction-id='#{id}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']") css("[data-internal-transaction-id='#{id}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']")
end end
@ -55,11 +59,11 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-transaction-hash='#{transaction_hash}']") css("[data-transaction-hash='#{transaction_hash}']")
end end
def transaction_address_link(%Transaction{hash: hash, to_address_hash: address_hash}, :to) do def transaction_address_link(%Transaction{hash: hash, from_address_hash: address_hash}, :from) do
css("[data-transaction-hash='#{hash}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']") css("[data-transaction-hash='#{hash}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']")
end end
def transaction_address_link(%Transaction{hash: hash, from_address_hash: address_hash}, :from) do def transaction_address_link(%Transaction{hash: hash, to_address_hash: address_hash}, :to) do
css("[data-transaction-hash='#{hash}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']") css("[data-transaction-hash='#{hash}'] [data-test='address_hash_link'] [data-address-hash='#{address_hash}']")
end end
@ -91,8 +95,4 @@ defmodule BlockScoutWeb.AddressPage do
def token_transfers_expansion(%Transaction{hash: transaction_hash}) do def token_transfers_expansion(%Transaction{hash: transaction_hash}) do
css("[data-transaction-hash='#{transaction_hash}'] [data-test='token_transfers_expansion']") css("[data-transaction-hash='#{transaction_hash}'] [data-test='token_transfers_expansion']")
end end
def transaction_type do
css("[data-test='transaction_type']")
end
end end

@ -15,10 +15,6 @@ defmodule BlockScoutWeb.ChainPage do
css("[data-test='contract-creation'] [data-address-hash='#{hash}']") css("[data-test='contract-creation'] [data-address-hash='#{hash}']")
end end
def non_loaded_transaction_count(count) do
css("[data-selector='channel-batching-count']", text: count)
end
def search(session, text) do def search(session, text) do
session session
|> fill_in(css("[data-test='search_input']"), with: text) |> fill_in(css("[data-test='search_input']"), with: text)
@ -37,14 +33,6 @@ defmodule BlockScoutWeb.ChainPage do
css("[data-test='chain_transaction']", count: count) css("[data-test='chain_transaction']", count: count)
end end
def transaction(%Transaction{hash: transaction_hash}) do
css("[data-transaction-hash='#{transaction_hash}']")
end
def transaction_status(%Transaction{hash: transaction_hash}) do
css("[data-transaction-hash='#{transaction_hash}'] [data-test='transaction_status']")
end
def visit_page(session) do def visit_page(session) do
visit(session, "/") visit(session, "/")
end end

@ -19,10 +19,6 @@ defmodule BlockScoutWeb.TransactionListPage do
css("[data-transaction-hash='#{hash}'] [data-test='transaction_type']", text: "Contract Creation") css("[data-transaction-hash='#{hash}'] [data-test='transaction_type']", text: "Contract Creation")
end end
def non_loaded_transaction_count(count) do
css("[data-selector='channel-batching-count']", text: count)
end
def transaction(%Transaction{hash: transaction_hash}) do def transaction(%Transaction{hash: transaction_hash}) do
css("[data-transaction-hash='#{transaction_hash}']") css("[data-transaction-hash='#{transaction_hash}']")
end end

@ -18,8 +18,4 @@ defmodule BlockScoutWeb.TransactionPage do
def visit_page(session, %Transaction{hash: transaction_hash}) do def visit_page(session, %Transaction{hash: transaction_hash}) do
visit(session, "/tx/#{transaction_hash}") visit(session, "/tx/#{transaction_hash}")
end end
def visit_page(session, transaction_hash = %Hash{}) do
visit(session, "/tx/#{transaction_hash}")
end
end end

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
alias Explorer.Chain.Wei alias Explorer.Chain.Wei
alias Explorer.Factory alias Explorer.Factory
alias BlockScoutWeb.{AddressPage, AddressView} alias BlockScoutWeb.{AddressPage, AddressView, Notifier}
setup do setup do
block = insert(:block) block = insert(:block)
@ -169,6 +169,7 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
session session
|> AddressPage.visit_page(addresses.lincoln) |> AddressPage.visit_page(addresses.lincoln)
|> assert_has(AddressPage.transaction_address_link(transactions.from_lincoln, :to)) |> assert_has(AddressPage.transaction_address_link(transactions.from_lincoln, :to))
|> refute_has(AddressPage.transaction_address_link(transactions.from_lincoln, :from))
end end
end end
@ -216,6 +217,28 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
|> AddressPage.visit_page(addresses.lincoln) |> AddressPage.visit_page(addresses.lincoln)
|> AddressPage.click_internal_transactions() |> AddressPage.click_internal_transactions()
|> assert_has(AddressPage.internal_transaction_address_link(internal_transaction, :from)) |> assert_has(AddressPage.internal_transaction_address_link(internal_transaction, :from))
|> refute_has(AddressPage.internal_transaction_address_link(internal_transaction, :to))
end
test "viewing new internal transactions via live update", %{addresses: addresses, session: session} do
transaction =
:transaction
|> insert(from_address: addresses.lincoln)
|> with_block()
session
|> AddressPage.visit_page(addresses.lincoln)
|> AddressPage.click_internal_transactions()
|> assert_has(AddressPage.internal_transactions(count: 2))
internal_transaction =
insert(:internal_transaction, transaction: transaction, index: 0, from_address: addresses.lincoln)
Notifier.handle_event({:chain_event, :internal_transactions, [internal_transaction]})
session
|> assert_has(AddressPage.internal_transactions(count: 3))
|> assert_has(AddressPage.internal_transaction(internal_transaction))
end end
end end

@ -0,0 +1,29 @@
defmodule BlockScoutWeb.BlockViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.BlockView
alias Explorer.Repo
describe "average_gas_price/1" do
test "returns an average of the gas prices for a block's transactions with the unit value" do
block = insert(:block)
Enum.each(1..10, fn index ->
:transaction
|> insert(gas_price: 10_000_000_000 * index)
|> with_block(block)
end)
assert "55 Gwei" == BlockView.average_gas_price(Repo.preload(block, [:transactions]))
end
end
describe "formatted_timestamp/1" do
test "returns a formatted timestamp string for a block" do
block = insert(:block)
assert Timex.format!(block.timestamp, "%b-%d-%Y %H:%M:%S %p %Z", :strftime) ==
BlockView.formatted_timestamp(block)
end
end
end

@ -48,7 +48,8 @@ defmodule Explorer.Chain do
@typedoc """ @typedoc """
Event type where data is broadcasted whenever data is inserted from chain indexing. Event type where data is broadcasted whenever data is inserted from chain indexing.
""" """
@type chain_event :: :addresses | :balances | :blocks | :exchange_rate | :logs | :transactions @type chain_event ::
:addresses | :balances | :blocks | :exchange_rate | :internal_transactions | :logs | :transactions
@type direction :: :from | :to @type direction :: :from | :to
@ -1212,7 +1213,7 @@ defmodule Explorer.Chain do
""" """
@spec subscribe_to_events(chain_event()) :: :ok @spec subscribe_to_events(chain_event()) :: :ok
def subscribe_to_events(event_type) def subscribe_to_events(event_type)
when event_type in ~w(addresses balances blocks exchange_rate logs transactions)a do when event_type in ~w(addresses balances blocks exchange_rate internal_transactions logs transactions)a do
Registry.register(Registry.ChainEvents, event_type, []) Registry.register(Registry.ChainEvents, event_type, [])
:ok :ok
end end

@ -209,7 +209,8 @@ defmodule Explorer.Chain.Import do
end end
defp broadcast_events(data) do defp broadcast_events(data) do
for {event_type, event_data} <- data, event_type in ~w(addresses balances blocks logs transactions)a do for {event_type, event_data} <- data,
event_type in ~w(addresses balances blocks internal_transactions logs transactions)a do
broadcast_event_data(event_type, event_data) broadcast_event_data(event_type, event_data)
end end
end end
@ -666,7 +667,7 @@ defmodule Explorer.Chain.Import do
conflict_target: [:transaction_hash, :index], conflict_target: [:transaction_hash, :index],
for: InternalTransaction, for: InternalTransaction,
on_conflict: :replace_all, on_conflict: :replace_all,
returning: [:index, :transaction_hash], returning: [:id, :index, :transaction_hash],
timeout: timeout, timeout: timeout,
timestamps: timestamps timestamps: timestamps
) )
@ -674,7 +675,7 @@ defmodule Explorer.Chain.Import do
{:ok, {:ok,
for( for(
internal_transaction <- internal_transactions, internal_transaction <- internal_transactions,
do: Map.take(internal_transaction, [:index, :transaction_hash]) do: Map.take(internal_transaction, [:id, :index, :transaction_hash])
)} )}
end end

@ -417,6 +417,12 @@ defmodule Explorer.Chain.ImportTest do
assert_received {:chain_event, :blocks, [%Block{}]} assert_received {:chain_event, :blocks, [%Block{}]}
end end
test "publishes internal_transaction data to subscribers on insert" do
Chain.subscribe_to_events(:internal_transactions)
Import.all(@import_data)
assert_received {:chain_event, :internal_transactions, [%{id: _}, %{id: _}]}
end
test "publishes log data to subscribers on insert" do test "publishes log data to subscribers on insert" do
Chain.subscribe_to_events(:logs) Chain.subscribe_to_events(:logs)
Import.all(@import_data) Import.all(@import_data)

Loading…
Cancel
Save