Add pending transactions to top of address_transactions list. Standardize to transaction tile for pending transactions.

Live load pending transactions and remove when collated.
pull/816/head
Stamates 6 years ago
parent b3447d4376
commit 95d01e177f
  1. 263
      apps/block_scout_web/assets/__tests__/pages/address.js
  2. 84
      apps/block_scout_web/assets/js/pages/address.js
  3. 4
      apps/block_scout_web/assets/js/pages/transaction.js
  4. 11
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  5. 2
      apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex
  6. 9
      apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex
  7. 18
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  8. 2
      apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex
  9. 9
      apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex
  10. 4
      apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex
  11. 4
      apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex
  12. 2
      apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex
  13. 11
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
  14. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex
  15. 4
      apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex
  16. 10
      apps/block_scout_web/lib/block_scout_web/views/address_view.ex
  17. 8
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  18. 16
      apps/block_scout_web/lib/block_scout_web/views/view_helpers.ex
  19. 15
      apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs
  20. 29
      apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs
  21. 12
      apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs
  22. 14
      apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex
  23. 47
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  24. 63
      apps/block_scout_web/test/block_scout_web/views/address_view_test.exs
  25. 44
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  26. 48
      apps/block_scout_web/test/block_scout_web/views/view_helpers_test.exs
  27. 38
      apps/explorer/lib/explorer/chain.ex
  28. 64
      apps/explorer/test/explorer/chain_test.exs

@ -5,16 +5,18 @@ describe('PAGE_LOAD', () => {
const state = initialState const state = initialState
const action = { const action = {
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
addressHash: '1234',
beyondPageOne: false, beyondPageOne: false,
addressHash: '1234' pendingTransactionHashes: ['0x00']
} }
const output = reducer(state, action) const output = reducer(state, action)
expect(output.addressHash).toBe('1234') expect(output.addressHash).toBe('1234')
expect(output.filter).toBe(undefined)
expect(output.beyondPageOne).toBe(false) expect(output.beyondPageOne).toBe(false)
expect(output.filter).toBe(undefined)
expect(output.pendingTransactionHashes).toEqual(['0x00'])
}) })
test('page 2+ without filter', () => { test('page 2 without filter', () => {
const state = initialState const state = initialState
const action = { const action = {
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
@ -41,7 +43,7 @@ describe('PAGE_LOAD', () => {
expect(output.filter).toBe('to') expect(output.filter).toBe('to')
expect(output.beyondPageOne).toBe(false) expect(output.beyondPageOne).toBe(false)
}) })
test('page 2+ with "to" filter', () => { test('page 2 with "to" filter', () => {
const state = initialState const state = initialState
const action = { const action = {
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
@ -81,6 +83,192 @@ test('RECEIVED_UPDATED_BALANCE', () => {
expect(output.balance).toBe('hello world') expect(output.balance).toBe('hello world')
}) })
describe('RECEIVED_NEW_PENDING_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test'])
expect(output.pendingTransactionHashes).toEqual(['0x00'])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(null)
})
test('large batch of transactions', () => {
const state = initialState
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x01',
transactionHtml: 'test 1'
},{
transactionHash: '0x02',
transactionHtml: 'test 2'
},{
transactionHash: '0x03',
transactionHtml: 'test 3'
},{
transactionHash: '0x04',
transactionHtml: 'test 4'
},{
transactionHash: '0x05',
transactionHtml: 'test 5'
},{
transactionHash: '0x06',
transactionHtml: 'test 6'
},{
transactionHash: '0x07',
transactionHtml: 'test 7'
},{
transactionHash: '0x08',
transactionHtml: 'test 8'
},{
transactionHash: '0x09',
transactionHtml: 'test 9'
},{
transactionHash: '0x10',
transactionHtml: 'test 10'
},{
transactionHash: '0x11',
transactionHtml: 'test 11'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual([
"0x01", "0x02", "0x03", "0x04", "0x05", "0x06", "0x07", "0x08", "0x09", "0x10", "0x11"
])
expect(output.batchPendingCountAccumulator).toEqual(11)
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(null)
})
test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1'],
pendingTransactionHashes: ['0x01']
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x02',
transactionHtml: 'test 2'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual(['test 1', 'test 2'])
expect(output.pendingTransactionHashes).toEqual(['0x01', '0x02'])
expect(output.batchPendingCountAccumulator).toEqual(0)
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newTransactions: [],
batchPendingCountAccumulator: 11
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x12',
transactionHtml: 'test 12'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual(['0x12'])
expect(output.batchPendingCountAccumulator).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newPendingTransactions: [],
batchPendingCountAccumulator: 11
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x12',
transactionHtml: 'test 12'
},{
transactionHash: '0x13',
transactionHtml: 'test 13'
},{
transactionHash: '0x14',
transactionHtml: 'test 14'
},{
transactionHash: '0x15',
transactionHtml: 'test 15'
},{
transactionHash: '0x16',
transactionHtml: 'test 16'
},{
transactionHash: '0x17',
transactionHtml: 'test 17'
},{
transactionHash: '0x18',
transactionHtml: 'test 18'
},{
transactionHash: '0x19',
transactionHtml: 'test 19'
},{
transactionHash: '0x20',
transactionHtml: 'test 20'
},{
transactionHash: '0x21',
transactionHtml: 'test 21'
},{
transactionHash: '0x22',
transactionHtml: 'test 22'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionHashes.length).toBe(11)
expect(output.batchPendingCountAccumulator).toEqual(22)
})
test('after disconnection', () => {
const state = Object.assign({}, initialState, {
channelDisconnected: true
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual([])
expect(output.batchPendingCountAccumulator).toEqual(0)
})
test('on page 2', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
})
})
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => { test('single transaction', () => {
const state = initialState const state = initialState
@ -212,7 +400,7 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
expect(output.newTransactions).toEqual([]) expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0) expect(output.batchCountAccumulator).toEqual(0)
}) })
test('on page 2+', () => { test('on page 2', () => {
const state = Object.assign({}, initialState, { const state = Object.assign({}, initialState, {
beyondPageOne: true beyondPageOne: true
}) })
@ -291,4 +479,69 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
expect(output.newTransactions).toEqual([]) expect(output.newTransactions).toEqual([])
}) })
test('single transaction collated from pending', () => {
const state = Object.assign({}, initialState, {
pendingTransactionHashes: ['0x00']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual(['test'])
expect(output.pendingTransactionHashes).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(1)
})
test('large batch of transactions', () => {
const state = Object.assign({}, initialState, {
pendingTransactionHashes: ['0x01', '0x02', '0x12']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x01',
transactionHtml: 'test 1'
},{
transactionHash: '0x02',
transactionHtml: 'test 2'
},{
transactionHash: '0x03',
transactionHtml: 'test 3'
},{
transactionHash: '0x04',
transactionHtml: 'test 4'
},{
transactionHash: '0x05',
transactionHtml: 'test 5'
},{
transactionHash: '0x06',
transactionHtml: 'test 6'
},{
transactionHash: '0x07',
transactionHtml: 'test 7'
},{
transactionHash: '0x08',
transactionHtml: 'test 8'
},{
transactionHash: '0x09',
transactionHtml: 'test 9'
},{
transactionHash: '0x10',
transactionHtml: 'test 10'
},{
transactionHash: '0x11',
transactionHtml: 'test 11'
}]
}
const output = reducer(state, action)
expect(output.newTransactions).toEqual([])
expect(output.pendingTransactionHashes).toEqual(['0x12'])
expect(output.transactionCount).toEqual(11)
})
}) })

@ -1,4 +1,5 @@
import $ from 'jquery' import $ from 'jquery'
import _ from 'lodash'
import URI from 'urijs' import URI from 'urijs'
import humps from 'humps' import humps from 'humps'
import numeral from 'numeral' import numeral from 'numeral'
@ -12,13 +13,17 @@ const BATCH_THRESHOLD = 10
export const initialState = { export const initialState = {
addressHash: null, addressHash: null,
balance: null,
batchCountAccumulator: 0, batchCountAccumulator: 0,
batchPendingCountAccumulator: 0,
beyondPageOne: null, beyondPageOne: null,
channelDisconnected: false, channelDisconnected: false,
filter: null, filter: null,
newInternalTransactions: [], newInternalTransactions: [],
newPendingTransactions: [],
newTransactions: [], newTransactions: [],
balance: null, newTransactionHashes: [],
pendingTransactionHashes: [],
transactionCount: null transactionCount: null
} }
@ -29,6 +34,7 @@ export function reducer (state = initialState, action) {
addressHash: action.addressHash, addressHash: action.addressHash,
beyondPageOne: action.beyondPageOne, beyondPageOne: action.beyondPageOne,
filter: action.filter, filter: action.filter,
pendingTransactionHashes: action.pendingTransactionHashes,
transactionCount: numeral(action.transactionCount).value() transactionCount: numeral(action.transactionCount).value()
}) })
} }
@ -59,7 +65,7 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
newInternalTransactions: [ newInternalTransactions: [
...state.newInternalTransactions, ...state.newInternalTransactions,
...incomingInternalTransactions.map(({internalTransactionHtml}) => internalTransactionHtml) ..._.map(incomingInternalTransactions, 'internalTransactionHtml')
] ]
}) })
} else { } else {
@ -68,6 +74,36 @@ export function reducer (state = initialState, action) {
}) })
} }
} }
case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': {
if (state.channelDisconnected || state.beyondPageOne) return state
const incomingPendingTransactions = humps.camelizeKeys(action.msgs)
.filter(({toAddressHash, fromAddressHash}) => (
!state.filter ||
(state.filter === 'to' && toAddressHash === state.addressHash) ||
(state.filter === 'from' && fromAddressHash === state.addressHash)
))
if (!state.batchPendingCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newPendingTransactions: [
...state.newPendingTransactions,
..._.map(incomingPendingTransactions, 'transactionHtml')
],
pendingTransactionHashes: [
...state.pendingTransactionHashes,
..._.map(incomingPendingTransactions, 'transactionHash')
]
})
} else {
return Object.assign({}, state, {
batchPendingCountAccumulator: state.batchPendingCountAccumulator + action.msgs.length,
pendingTransactionHashes: [
...state.pendingTransactionHashes,
..._.map(incomingPendingTransactions, 'transactionHash')
]
})
}
}
case 'RECEIVED_NEW_TRANSACTION_BATCH': { case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected || state.beyondPageOne) return state if (state.channelDisconnected || state.beyondPageOne) return state
@ -78,17 +114,26 @@ export function reducer (state = initialState, action) {
(state.filter === 'from' && fromAddressHash === state.addressHash) (state.filter === 'from' && fromAddressHash === state.addressHash)
)) ))
const updatePendingTransactionHashes =
_.difference(state.pendingTransactionHashes, _.map(incomingTransactions, 'transactionHash'))
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, { return Object.assign({}, state, {
batchPendingCountAccumulator: state.batchPendingCountAccumulator - action.msgs.length,
newTransactions: [ newTransactions: [
...state.newTransactions, ...state.newTransactions,
...incomingTransactions.map(({transactionHtml}) => transactionHtml) ..._.map(incomingTransactions, 'transactionHtml')
], ],
newTransactionHashes: _.map(incomingTransactions, 'transactionHash'),
pendingTransactionHashes: updatePendingTransactionHashes,
transactionCount: state.transactionCount + action.msgs.length transactionCount: state.transactionCount + action.msgs.length
}) })
} else { } else {
return Object.assign({}, state, { return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, batchCountAccumulator: state.batchCountAccumulator + action.msgs.length,
batchPendingCountAccumulator: state.batchPendingCountAccumulator - action.msgs.length,
newTransactionHashes: _.map(incomingTransactions, 'transactionHash'),
pendingTransactionHashes: updatePendingTransactionHashes,
transactionCount: state.transactionCount + action.msgs.length transactionCount: state.transactionCount + action.msgs.length
}) })
} }
@ -116,8 +161,12 @@ if ($addressDetailsPage.length) {
const state = store.dispatch({ const state = store.dispatch({
type: 'PAGE_LOAD', type: 'PAGE_LOAD',
addressHash, addressHash,
filter,
beyondPageOne: !!blockNumber, beyondPageOne: !!blockNumber,
filter,
pendingTransactionHashes:
$('[data-selector="pending-transactions-list"] [data-transaction-hash]').map((index, el) =>
el.attributes['data-transaction-hash'].nodeValue
).toArray(),
transactionCount: $('[data-selector="transaction-count"]').text() transactionCount: $('[data-selector="transaction-count"]').text()
}) })
addressChannel.join() addressChannel.join()
@ -137,16 +186,25 @@ if ($addressDetailsPage.length) {
addressChannel.on('internal_transaction', batchChannel((msgs) => addressChannel.on('internal_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', msgs }) store.dispatch({ type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', msgs })
)) ))
addressChannel.on('pending_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs })
))
addressChannel.on('transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs })
))
} }
}, },
render (state, oldState) { render (state, oldState) {
const $balance = $('[data-selector="balance-card"]') 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 $channelPendingBatching = $('[data-selector="channel-pending-batching-message"]')
const $channelPendingBatchingCount = $('[data-selector="channel-pending-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 $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]')
const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]') const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]')
const $internalTransactionsList = $('[data-selector="internal-transactions-list"]') const $internalTransactionsList = $('[data-selector="internal-transactions-list"]')
const $pendingTransactionsList = $('[data-selector="pending-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"]')
const $validationsList = $('[data-selector="validations-list"]') const $validationsList = $('[data-selector="validations-list"]')
@ -166,10 +224,28 @@ if ($addressDetailsPage.length) {
} else { } else {
$channelBatching.hide() $channelBatching.hide()
} }
if (state.batchPendingCountAccumulator > 0) {
$channelPendingBatching.show()
$channelPendingBatchingCount[0].innerHTML = numeral(state.batchPendingCountAccumulator).format()
} else {
$channelPendingBatching.hide()
}
if (oldState.pendingTransactionHashes !== state.pendingTransactionHashes && state.newTransactionHashes.length > 0) {
let $transaction
_.each(state.newTransactionHashes, (hash) => {
$transaction = $(`[data-selector="pending-transactions-list"] [data-transaction-hash="${hash}"]`)
$transaction.addClass('shrink-out')
setTimeout(() => $transaction.slideUp({ complete: () => $transaction.remove() }), 400)
})
}
if (oldState.newInternalTransactions !== state.newInternalTransactions && $internalTransactionsList.length) { if (oldState.newInternalTransactions !== state.newInternalTransactions && $internalTransactionsList.length) {
prependWithClingBottom($internalTransactionsList, state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join('')) prependWithClingBottom($internalTransactionsList, state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join(''))
updateAllAges() updateAllAges()
} }
if (oldState.newPendingTransactions !== state.newPendingTransactions && $pendingTransactionsList.length) {
prependWithClingBottom($pendingTransactionsList, state.newPendingTransactions.slice(oldState.newPendingTransactions.length).reverse().join(''))
updateAllAges()
}
if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) { if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) {
prependWithClingBottom($transactionsList, state.newTransactions.slice(oldState.newTransactions.length).reverse().join('')) prependWithClingBottom($transactionsList, state.newTransactions.slice(oldState.newTransactions.length).reverse().join(''))
updateAllAges() updateAllAges()

@ -91,7 +91,7 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, { return Object.assign({}, state, {
newTransactions: [ newTransactions: [
...state.newTransactions, ...state.newTransactions,
...action.msgs.map(({transactionHtml}) => transactionHtml) ..._.map(action.msgs, 'transactionHtml')
], ],
transactionCount transactionCount
}) })
@ -147,7 +147,7 @@ if ($transactionPendingListPage.length) {
const transactionsChannel = socket.channel(`transactions:new_transaction`) 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', (msg) => transactionsChannel.on('transaction', (msg) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) }) store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })
) )
const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`) const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`)

@ -4,10 +4,11 @@ defmodule BlockScoutWeb.AddressChannel do
""" """
use BlockScoutWeb, :channel use BlockScoutWeb, :channel
alias Explorer.Chain.Hash
alias BlockScoutWeb.{InternalTransactionView, AddressView, TransactionView} alias BlockScoutWeb.{InternalTransactionView, AddressView, TransactionView}
alias Phoenix.View alias Phoenix.View
intercept(["balance_update", "count", "internal_transaction", "transaction"]) intercept(["balance_update", "count", "internal_transaction", "pending_transaction", "transaction"])
def join("addresses:" <> _address_hash, _params, socket) do def join("addresses:" <> _address_hash, _params, socket) do
{:ok, %{}, socket} {:ok, %{}, socket}
@ -60,7 +61,10 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket} {:noreply, socket}
end end
def handle_out("transaction", %{address: address, transaction: transaction}, socket) do def handle_out("transaction", data, socket), do: handle_transaction(data, socket, "transaction")
def handle_out("pending_transaction", data, socket), do: handle_transaction(data, socket, "pending_transaction")
def handle_transaction(%{address: address, transaction: transaction}, socket, event) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale) Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
rendered = rendered =
@ -71,9 +75,10 @@ defmodule BlockScoutWeb.AddressChannel do
transaction: transaction transaction: transaction
) )
push(socket, "transaction", %{ push(socket, event, %{
to_address_hash: to_string(transaction.to_address_hash), to_address_hash: to_string(transaction.to_address_hash),
from_address_hash: to_string(transaction.from_address_hash), from_address_hash: to_string(transaction.from_address_hash),
transaction_hash: Hash.to_string(transaction.hash),
transaction_html: rendered transaction_html: rendered
}) })

@ -28,7 +28,7 @@ defmodule BlockScoutWeb.TransactionChannel do
rendered_transaction = rendered_transaction =
View.render_to_string( View.render_to_string(
TransactionView, TransactionView,
"_pending_tile.html", "_tile.html",
transaction: transaction transaction: transaction
) )

@ -14,18 +14,20 @@ defmodule BlockScoutWeb.AddressTransactionController do
def index(conn, %{"address_id" => address_hash_string} = params) do def index(conn, %{"address_id" => address_hash_string} = params) do
with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string),
{:ok, address} <- Chain.hash_to_address(address_hash) do {:ok, address} <- Chain.hash_to_address(address_hash) do
full_options = pending_options =
[ [
necessity_by_association: %{ necessity_by_association: %{
:block => :required,
[created_contract_address: :names] => :optional, [created_contract_address: :names] => :optional,
[from_address: :names] => :optional, [from_address: :names] => :optional,
[to_address: :names] => :optional [to_address: :names] => :optional,
:token_transfers => :optional
} }
] ]
|> Keyword.merge(paging_options(params)) |> Keyword.merge(paging_options(params))
|> Keyword.merge(current_filter(params)) |> Keyword.merge(current_filter(params))
full_options = put_in(pending_options, [:necessity_by_association, :block], :required)
transactions_plus_one = Chain.address_to_transactions(address, full_options) transactions_plus_one = Chain.address_to_transactions(address, full_options)
{transactions, next_page} = split_list_by_page(transactions_plus_one) {transactions, next_page} = split_list_by_page(transactions_plus_one)
@ -36,6 +38,7 @@ defmodule BlockScoutWeb.AddressTransactionController do
next_page_params: next_page_params(next_page, transactions, params), next_page_params: next_page_params(next_page, transactions, params),
exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(),
filter: params["filter"], filter: params["filter"],
pending_transactions: Chain.address_to_pending_transactions(address, pending_options),
transactions: transactions, transactions: transactions,
transaction_count: transaction_count(address), transaction_count: transaction_count(address),
validation_count: validation_count(address) validation_count: validation_count(address)

@ -94,25 +94,27 @@ defmodule BlockScoutWeb.Notifier do
end end
defp broadcast_transaction(%Transaction{block_number: nil} = pending) do defp broadcast_transaction(%Transaction{block_number: nil} = pending) do
Endpoint.broadcast("transactions:new_pending_transaction", "new_pending_transaction", %{ broadcast_transaction(pending, "transactions:new_pending_transaction", "pending_transaction")
transaction: pending
})
end end
defp broadcast_transaction(transaction) do defp broadcast_transaction(transaction) do
Endpoint.broadcast("transactions:new_transaction", "new_transaction", %{ broadcast_transaction(transaction, "transactions:new_transaction", "transaction")
transaction: transaction end
})
defp broadcast_transaction(transaction, transaction_channel, event) do
Endpoint.broadcast("transactions:#{transaction.hash}", "collated", %{}) Endpoint.broadcast("transactions:#{transaction.hash}", "collated", %{})
Endpoint.broadcast("addresses:#{transaction.from_address_hash}", "transaction", %{ Endpoint.broadcast(transaction_channel, event, %{
transaction: transaction
})
Endpoint.broadcast("addresses:#{transaction.from_address_hash}", event, %{
address: transaction.from_address, address: transaction.from_address,
transaction: transaction transaction: transaction
}) })
if 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}", event, %{
address: transaction.to_address, address: transaction.to_address,
transaction: transaction transaction: transaction
}) })

@ -21,7 +21,7 @@
<div class="col-10 col-md-11"> <div class="col-10 col-md-11">
<div class="row"> <div class="row">
<div class="col-md-7 d-flex flex-column mt-3 mt-md-0"> <div class="col-md-7 d-flex flex-column mt-3 mt-md-0">
<%= address |> BlockScoutWeb.AddressView.address_partial_selector(nil, nil) |> BlockScoutWeb.AddressView.render_partial() %> <%= address |> BlockScoutWeb.AddressView.address_partial_selector(nil, nil) |> BlockScoutWeb.ViewHelpers.render_partial() %>
<!-- number of txns for this address --> <!-- number of txns for this address -->
<span> <span>
<span data-test="transaction_count"><%= transaction_count(address) %></span> <span data-test="transaction_count"><%= transaction_count(address) %></span>

@ -55,8 +55,15 @@
) %> ) %>
</div> </div>
</div> </div>
<%= if Enum.count(@transactions) > 0 do %> <%= if Enum.count(@transactions) > 0 or Enum.count(@pending_transactions) > 0 do %>
<h2 class="card-title"><%= gettext "Transactions" %></h2> <h2 class="card-title"><%= gettext "Transactions" %></h2>
<% end %>
<div class="mb-3" data-selector="pending-transactions-list">
<%= for pending_transaction <- @pending_transactions do %>
<%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: pending_transaction) %>
<% end %>
</div>
<%= if Enum.count(@transactions) > 0 do %>
<span data-selector="transactions-list"> <span data-selector="transactions-list">
<%= for transaction <- @transactions do %> <%= for transaction <- @transactions do %>
<%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: transaction) %> <%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: transaction) %>

@ -0,0 +1,4 @@
<%= link(
gettext("Block #%{number}", number: to_string(@block.number)),
to: block_path(BlockScoutWeb.Endpoint, :show, @block)
) %>

@ -6,9 +6,9 @@
<div class="col-md-7 col-lg-8 d-flex flex-column text-nowrap pr-2 pr-sm-2 pr-md-0"> <div class="col-md-7 col-lg-8 d-flex flex-column text-nowrap pr-2 pr-sm-2 pr-md-0">
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %> <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %>
<span class="text-nowrap"> <span class="text-nowrap">
<%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %> <%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.ViewHelpers.render_partial() %>
&rarr; &rarr;
<%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %> <%= @internal_transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.ViewHelpers.render_partial() %>
</span> </span>
<span class="tile-title text-truncate mt-3 mt-md-0"> <span class="tile-title text-truncate mt-3 mt-md-0">
<%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %> <%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %>

@ -61,7 +61,7 @@
</p> </p>
<span data-selector="transactions-pending-list"> <span data-selector="transactions-pending-list">
<%= for transaction <- @transactions do %> <%= for transaction <- @transactions do %>
<%= render BlockScoutWeb.TransactionView, "_pending_tile.html", transaction: transaction %> <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %>
<% end %> <% end %>
</span> </span>

@ -11,9 +11,9 @@
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0"> <div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<%= render "_link.html", transaction_hash: @transaction.hash %> <%= render "_link.html", transaction_hash: @transaction.hash %>
<span class="text-nowrap"> <span class="text-nowrap">
<%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, assigns[:current_address]) |> BlockScoutWeb.ViewHelpers.render_partial() %>
&rarr; &rarr;
<%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.AddressView.render_partial() %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, assigns[:current_address]) |> BlockScoutWeb.ViewHelpers.render_partial() %>
</span> </span>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0"> <span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="tile-title"> <span class="tile-title">
@ -26,12 +26,9 @@
</div> </div>
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-start text-md-right mt-3 mt-md-0"> <div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column flex-nowrap justify-content-start text-md-right mt-3 mt-md-0">
<span class="mr-2 mr-md-0 order-1"> <span class="mr-2 mr-md-0 order-1">
<%= link( <%= @transaction |> block_number() |> BlockScoutWeb.ViewHelpers.render_partial() %>
gettext("Block #%{number}", number: to_string(@transaction.block_number)),
to: block_path(BlockScoutWeb.Endpoint, :show, @transaction.block)
) %>
</span> </span>
<span class="mr-2 mr-md-0 order-2" data-from-now="<%= @transaction.block.timestamp %>"></span> <span class="mr-2 mr-md-0 order-2" data-from-now="<%= block_timestamp(@transaction) %>"></span>
<%= if from_or_to_address?(@transaction, assigns[:current_address]) do %> <%= if from_or_to_address?(@transaction, assigns[:current_address]) do %>
<span class="mr-2 mr-md-0 order-0 order-md-3"> <span class="mr-2 mr-md-0 order-0 order-md-3">
<%= if @transaction.from_address_hash == assigns[:current_address].hash do %> <%= if @transaction.from_address_hash == assigns[:current_address].hash do %>

@ -11,9 +11,9 @@
</span> </span>
<% end %> <% end %>
<% end %> <% end %>
<%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:from, @address, true) |> BlockScoutWeb.AddressView.render_partial() %> <%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:from, @address, true) |> BlockScoutWeb.ViewHelpers.render_partial() %>
&rarr; &rarr;
<%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:to, @address, true) |> BlockScoutWeb.AddressView.render_partial() %> <%= @token_transfer |> BlockScoutWeb.AddressView.address_partial_selector(:to, @address, true) |> BlockScoutWeb.ViewHelpers.render_partial() %>
</span> </span>
<span class="col-12 col-md-7 ml-3 ml-sm-0"> <span class="col-12 col-md-7 ml-3 ml-sm-0">
<%= token_transfer_amount(@token_transfer) %> <%= token_transfer_amount(@token_transfer) %>

@ -26,9 +26,9 @@
<h3 data-test="transaction_detail_hash"><%= @transaction %> </h3> <h3 data-test="transaction_detail_hash"><%= @transaction %> </h3>
<span class="d-block mb-2 text-muted"> <span class="d-block mb-2 text-muted">
<%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, nil) |> BlockScoutWeb.AddressView.render_partial() %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, nil) |> BlockScoutWeb.ViewHelpers.render_partial() %>
<span class="text-muted"> &rarr; </span> <span class="text-muted"> &rarr; </span>
<%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, nil) |> BlockScoutWeb.AddressView.render_partial() %> <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, nil) |> BlockScoutWeb.ViewHelpers.render_partial() %>
</span> </span>
<div class="d-flex flex-row justify-content-start text-muted"> <div class="d-flex flex-row justify-content-start text-muted">
<span class="mr-4 text-<%= BlockScoutWeb.TransactionView.type_suffix(@transaction) %>"><%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %></span> <span class="mr-4 text-<%= BlockScoutWeb.TransactionView.type_suffix(@transaction) %>"><%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %></span>

@ -181,21 +181,23 @@ defmodule BlockScoutWeb.AddressView do
def trimmed_hash(_), do: "" def trimmed_hash(_), do: ""
defp matching_address_check(%Address{hash: hash} = current_address, %Address{hash: hash}, contract?, truncate) do defp matching_address_check(%Address{hash: hash} = current_address, %Address{hash: hash}, contract?, truncate) do
%{ [
view_module: __MODULE__,
partial: "_responsive_hash.html", partial: "_responsive_hash.html",
address: current_address, address: current_address,
contract: contract?, contract: contract?,
truncate: truncate truncate: truncate
} ]
end end
defp matching_address_check(_current_address, %Address{} = address, contract?, truncate) do defp matching_address_check(_current_address, %Address{} = address, contract?, truncate) do
%{ [
view_module: __MODULE__,
partial: "_link.html", partial: "_link.html",
address: address, address: address,
contract: contract?, contract: contract?,
truncate: truncate truncate: truncate
} ]
end end
@doc """ @doc """

@ -3,7 +3,7 @@ defmodule BlockScoutWeb.TransactionView do
alias Cldr.Number alias Cldr.Number
alias Explorer.Chain alias Explorer.Chain
alias Explorer.Chain.{Address, InternalTransaction, Transaction, Wei} alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction, Wei}
alias BlockScoutWeb.{AddressView, BlockView, TabHelpers} alias BlockScoutWeb.{AddressView, BlockView, TabHelpers}
import BlockScoutWeb.Gettext import BlockScoutWeb.Gettext
@ -14,6 +14,12 @@ defmodule BlockScoutWeb.TransactionView do
defdelegate formatted_timestamp(block), to: BlockView defdelegate formatted_timestamp(block), to: BlockView
def block_number(%Transaction{block_number: nil}), do: gettext("Block Pending")
def block_number(%Transaction{block: block}), do: [view_module: BlockView, partial: "_link.html", block: block]
def block_timestamp(%Transaction{block_number: nil, inserted_at: time}), do: time
def block_timestamp(%Transaction{block: %Block{timestamp: time}}), do: time
def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do def confirmations(%Transaction{block: block}, named_arguments) when is_list(named_arguments) do
case block do case block do
nil -> 0 nil -> 0

@ -0,0 +1,16 @@
defmodule BlockScoutWeb.ViewHelpers do
@moduledoc """
Helper functions for views
"""
use BlockScoutWeb, :view
def render_partial(args) when is_list(args) do
render(
Keyword.get(args, :view_module),
Keyword.get(args, :partial),
args
)
end
def render_partial(text), do: text
end

@ -51,6 +51,21 @@ defmodule BlockScoutWeb.AddressChannelTest do
end end
end end
test "notified of new_pending_transaction for matching from_address", %{address: address, topic: topic} do
pending = insert(:transaction, from_address: address)
Notifier.handle_event({:chain_event, :transactions, [pending.hash]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "pending_transaction", payload: payload} ->
assert payload.address.hash == address.hash
assert payload.transaction.hash == pending.hash
after
5_000 ->
assert false, "Expected message received nothing."
end
end
test "notified of new_transaction for matching from_address", %{address: address, topic: topic} do test "notified of new_transaction for matching from_address", %{address: address, topic: topic} do
transaction = transaction =
:transaction :transaction

@ -1,9 +1,10 @@
defmodule BlockScoutWeb.TransactionChannelTest do defmodule BlockScoutWeb.TransactionChannelTest do
use BlockScoutWeb.ChannelCase use BlockScoutWeb.ChannelCase
alias Explorer.Chain.Hash
alias BlockScoutWeb.Notifier alias BlockScoutWeb.Notifier
test "subscribed user is notified of new_transaction event" do test "subscribed user is notified of new_transaction topic" do
topic = "transactions:new_transaction" topic = "transactions:new_transaction"
@endpoint.subscribe(topic) @endpoint.subscribe(topic)
@ -15,7 +16,7 @@ defmodule BlockScoutWeb.TransactionChannelTest do
Notifier.handle_event({:chain_event, :transactions, [transaction.hash]}) Notifier.handle_event({:chain_event, :transactions, [transaction.hash]})
receive do receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "new_transaction", payload: payload} -> %Phoenix.Socket.Broadcast{topic: ^topic, event: "transaction", payload: payload} ->
assert payload.transaction.hash == transaction.hash assert payload.transaction.hash == transaction.hash
after after
5_000 -> 5_000 ->
@ -23,7 +24,7 @@ defmodule BlockScoutWeb.TransactionChannelTest do
end end
end end
test "subscribed user is notified of new_pending_transaction event" do test "subscribed user is notified of new_pending_transaction topic" do
topic = "transactions:new_pending_transaction" topic = "transactions:new_pending_transaction"
@endpoint.subscribe(topic) @endpoint.subscribe(topic)
@ -32,11 +33,31 @@ defmodule BlockScoutWeb.TransactionChannelTest do
Notifier.handle_event({:chain_event, :transactions, [pending.hash]}) Notifier.handle_event({:chain_event, :transactions, [pending.hash]})
receive do receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "new_pending_transaction", payload: payload} -> %Phoenix.Socket.Broadcast{topic: ^topic, event: "pending_transaction", payload: payload} ->
assert payload.transaction.hash == pending.hash assert payload.transaction.hash == pending.hash
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 of transaction_hash collated event" do
transaction =
:transaction
|> insert()
|> with_block()
topic = "transactions:#{Hash.to_string(transaction.hash)}"
@endpoint.subscribe(topic)
Notifier.handle_event({:chain_event, :transactions, [transaction.hash]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "collated", payload: %{}} ->
assert true
after
5_000 ->
assert false, "Expected message received nothing."
end
end
end end

@ -45,18 +45,20 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do
assert Enum.member?(actual_transaction_hashes, to_transaction.hash) assert Enum.member?(actual_transaction_hashes, to_transaction.hash)
end end
test "does not return related transactions without a block", %{conn: conn} do test "returns pending related transactions", %{conn: conn} do
address = insert(:address) address = insert(:address)
insert(:transaction, from_address: address, to_address: address) pending = insert(:transaction, from_address: address, to_address: address)
conn = get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address)) conn = get(conn, address_transaction_path(BlockScoutWeb.Endpoint, :index, address))
actual_pending_transaction_hashes =
conn.assigns.pending_transactions
|> Enum.map(& &1.hash)
assert html_response(conn, 200) assert html_response(conn, 200)
assert conn.status == 200 assert conn.status == 200
assert Enum.empty?(conn.assigns.transactions) assert Enum.member?(actual_pending_transaction_hashes, pending.hash)
assert conn.status == 200
assert Enum.empty?(conn.assigns.transactions)
end end
test "includes USD exchange rate value for address in assigns", %{conn: conn} do test "includes USD exchange rate value for address in assigns", %{conn: conn} do

@ -71,6 +71,14 @@ 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 first_transaction_hash(session) do
session
|> find(css("[data-transaction-hash]", count: :any))
|> hd
|> find(css("[data-test='transaction_hash_link']"))
|> Element.text()
end
def internal_transaction(%InternalTransaction{id: id}) do def internal_transaction(%InternalTransaction{id: id}) do
css("[data-test='internal_transaction'][data-internal-transaction-id='#{id}']") css("[data-test='internal_transaction'][data-internal-transaction-id='#{id}']")
end end
@ -87,6 +95,12 @@ defmodule BlockScoutWeb.AddressPage 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 pending_transaction(%Transaction{hash: transaction_hash}), do: pending_transaction(transaction_hash)
def pending_transaction(transaction_hash) do
css("[data-selector='pending-transactions-list'] [data-transaction-hash='#{transaction_hash}']")
end
def transaction(%Transaction{hash: transaction_hash}), do: transaction(transaction_hash) def transaction(%Transaction{hash: transaction_hash}), do: transaction(transaction_hash)
def transaction(%Hash{} = hash) do def transaction(%Hash{} = hash) do

@ -123,6 +123,53 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
|> assert_has(AddressPage.transaction_status(transactions.from_lincoln)) |> assert_has(AddressPage.transaction_status(transactions.from_lincoln))
end end
test "sees pending transactions above collated", %{
addresses: addresses,
session: session,
transactions: transactions
} do
pending = insert(:transaction, to_address: addresses.lincoln)
session
|> AddressPage.visit_page(addresses.lincoln)
|> assert_has(AddressPage.pending_transaction(pending))
|> assert_has(AddressPage.transaction(transactions.from_taft))
|> assert_has(AddressPage.transaction(transactions.from_lincoln))
|> assert_has(AddressPage.transaction_status(transactions.from_lincoln))
assert AddressPage.first_transaction_hash(session) == to_string(pending.hash)
end
test "viewing new pending transactions via live update", %{addresses: addresses, session: session} do
pending = insert(:transaction, from_address: addresses.lincoln)
session
|> AddressPage.visit_page(addresses.lincoln)
|> assert_has(AddressPage.pending_transaction(pending))
new_pending = insert(:transaction, from_address: addresses.lincoln)
Notifier.handle_event({:chain_event, :transactions, [new_pending.hash]})
assert_has(session, AddressPage.pending_transaction(new_pending))
end
test "pending transaction is removed via live update", %{addresses: addresses, session: session} do
pending = insert(:transaction, from_address: addresses.lincoln)
session
|> AddressPage.visit_page(addresses.lincoln)
|> assert_has(AddressPage.pending_transaction(pending))
transaction = with_block(pending)
Notifier.handle_event({:chain_event, :transactions, [transaction.hash]})
session
|> refute_has(AddressPage.pending_transaction(pending))
|> assert_has(AddressPage.transaction(transaction))
end
test "can filter to only see transactions from an address", %{ test "can filter to only see transactions from an address", %{
addresses: addresses, addresses: addresses,
session: session, session: session,

@ -21,97 +21,105 @@ defmodule BlockScoutWeb.AddressViewTest do
created_contract_address_hash: nil created_contract_address_hash: nil
) )
assert AddressView.address_partial_selector(internal_transaction, :to, nil) == "Contract Address Pending" assert "Contract Address Pending" == AddressView.address_partial_selector(internal_transaction, :to, nil)
end end
test "will truncate address" do test "will truncate address" do
transaction = %Transaction{to_address: to_address} = insert(:transaction) transaction = %Transaction{to_address: to_address} = insert(:transaction)
assert %{ assert [
view_module: AddressView,
partial: "_link.html", partial: "_link.html",
address: ^to_address, address: ^to_address,
contract: false, contract: false,
truncate: true truncate: true
} = AddressView.address_partial_selector(transaction, :to, nil, true) ] = AddressView.address_partial_selector(transaction, :to, nil, true)
end end
test "for a non-contract to address not on address page" do test "for a non-contract to address not on address page" do
transaction = %Transaction{to_address: to_address} = insert(:transaction) transaction = %Transaction{to_address: to_address} = insert(:transaction)
assert %{ assert [
view_module: AddressView,
partial: "_link.html", partial: "_link.html",
address: ^to_address, address: ^to_address,
contract: false, contract: false,
truncate: false truncate: false
} = AddressView.address_partial_selector(transaction, :to, nil) ] = AddressView.address_partial_selector(transaction, :to, nil)
end end
test "for a non-contract to address non matching address page" do test "for a non-contract to address non matching address page" do
transaction = %Transaction{to_address: to_address} = insert(:transaction) transaction = %Transaction{to_address: to_address} = insert(:transaction)
assert %{ assert [
view_module: AddressView,
partial: "_link.html", partial: "_link.html",
address: ^to_address, address: ^to_address,
contract: false, contract: false,
truncate: false truncate: false
} = AddressView.address_partial_selector(transaction, :to, nil) ] = AddressView.address_partial_selector(transaction, :to, nil)
end end
test "for a non-contract to address matching address page" do test "for a non-contract to address matching address page" do
transaction = %Transaction{to_address: to_address} = insert(:transaction) transaction = %Transaction{to_address: to_address} = insert(:transaction)
assert %{ assert [
view_module: AddressView,
partial: "_responsive_hash.html", partial: "_responsive_hash.html",
address: ^to_address, address: ^to_address,
contract: false, contract: false,
truncate: false truncate: false
} = AddressView.address_partial_selector(transaction, :to, transaction.to_address) ] = AddressView.address_partial_selector(transaction, :to, transaction.to_address)
end end
test "for a contract to address non matching address page" do test "for a contract to address non matching address page" do
contract_address = insert(:contract_address) contract_address = insert(:contract_address)
transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address) transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address)
assert %{ assert [
view_module: AddressView,
partial: "_link.html", partial: "_link.html",
address: ^contract_address, address: ^contract_address,
contract: true, contract: true,
truncate: false truncate: false
} = AddressView.address_partial_selector(transaction, :to, transaction.to_address) ] = AddressView.address_partial_selector(transaction, :to, transaction.to_address)
end end
test "for a contract to address matching address page" do test "for a contract to address matching address page" do
contract_address = insert(:contract_address) contract_address = insert(:contract_address)
transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address) transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address)
assert %{ assert [
view_module: AddressView,
partial: "_responsive_hash.html", partial: "_responsive_hash.html",
address: ^contract_address, address: ^contract_address,
contract: true, contract: true,
truncate: false truncate: false
} = AddressView.address_partial_selector(transaction, :to, contract_address) ] = AddressView.address_partial_selector(transaction, :to, contract_address)
end end
test "for a non-contract from address not on address page" do test "for a non-contract from address not on address page" do
transaction = %Transaction{to_address: to_address} = insert(:transaction) transaction = %Transaction{to_address: to_address} = insert(:transaction)
assert %{ assert [
view_module: AddressView,
partial: "_link.html", partial: "_link.html",
address: ^to_address, address: ^to_address,
contract: false, contract: false,
truncate: false truncate: false
} = AddressView.address_partial_selector(transaction, :to, nil) ] = AddressView.address_partial_selector(transaction, :to, nil)
end end
test "for a non-contract from address matching address page" do test "for a non-contract from address matching address page" do
transaction = %Transaction{from_address: from_address} = insert(:transaction) transaction = %Transaction{from_address: from_address} = insert(:transaction)
assert %{ assert [
view_module: AddressView,
partial: "_responsive_hash.html", partial: "_responsive_hash.html",
address: ^from_address, address: ^from_address,
contract: false, contract: false,
truncate: false truncate: false
} = AddressView.address_partial_selector(transaction, :from, transaction.from_address) ] = AddressView.address_partial_selector(transaction, :from, transaction.from_address)
end end
end end
@ -187,27 +195,6 @@ defmodule BlockScoutWeb.AddressViewTest do
end end
end end
describe "render_partial/1" do
test "renders _link partial" do
address = build(:address)
assert {:safe, _} =
AddressView.render_partial(%{partial: "_link.html", address: address, contract: false, truncate: false})
end
test "renders _responsive_hash partial" do
address = build(:address)
assert {:safe, _} =
AddressView.render_partial(%{
partial: "_responsive_hash.html",
address: address,
contract: false,
truncate: false
})
end
end
describe "smart_contract_verified?/1" do describe "smart_contract_verified?/1" do
test "returns true when smart contract is verified" do test "returns true when smart contract is verified" do
smart_contract = insert(:smart_contract) smart_contract = insert(:smart_contract)

@ -3,7 +3,49 @@ defmodule BlockScoutWeb.TransactionViewTest do
alias Explorer.Chain.Wei alias Explorer.Chain.Wei
alias Explorer.Repo alias Explorer.Repo
alias BlockScoutWeb.TransactionView alias BlockScoutWeb.{BlockView, TransactionView}
describe "block_number/1" do
test "returns pending text for pending transaction" do
pending = insert(:transaction)
assert "Block Pending" == TransactionView.block_number(pending)
end
test "returns block number for collated transaction" do
block = insert(:block)
transaction =
:transaction
|> insert()
|> with_block(block)
assert [
view_module: BlockView,
partial: "_link.html",
block: _block
] = TransactionView.block_number(transaction)
end
end
describe "block_timestamp/1" do
test "returns timestamp of transaction for pending transaction" do
pending = insert(:transaction)
assert pending.inserted_at == TransactionView.block_timestamp(pending)
end
test "returns timestamp for block for collacted transaction" do
block = insert(:block)
transaction =
:transaction
|> insert()
|> with_block(block)
assert block.timestamp == TransactionView.block_timestamp(transaction)
end
end
describe "confirmations/2" do describe "confirmations/2" do
test "returns 0 if pending transaction" do test "returns 0 if pending transaction" do

@ -0,0 +1,48 @@
defmodule BlockScoutWeb.ViewHelpersTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.{AddressView, BlockView, ViewHelpers}
describe "render_partial/1" do
test "renders text" do
assert "test" == ViewHelpers.render_partial("test")
end
test "renders address _link partial" do
address = build(:address)
assert {:safe, _} =
ViewHelpers.render_partial(
view_module: AddressView,
partial: "_link.html",
address: address,
contract: false,
truncate: false
)
end
test "renders address _responsive_hash partial" do
address = build(:address)
assert {:safe, _} =
ViewHelpers.render_partial(
view_module: AddressView,
partial: "_responsive_hash.html",
address: address,
contract: false,
truncate: false
)
end
test "renders block _link partial" do
block = build(:block)
assert {:safe, _} =
ViewHelpers.render_partial(
view_module: BlockView,
partial: "_link.html",
block: block
)
end
end
end

@ -134,6 +134,44 @@ defmodule Explorer.Chain do
|> Repo.all() |> Repo.all()
end end
@doc """
Pending `t:Explorer.Chain.Transaction/0`s from `address`.
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Transaction.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Transaction.t/0` will not be included in the page `entries`.
"""
@spec address_to_pending_transactions(Address.t(), [necessity_by_association_option]) :: [Transaction.t()]
def address_to_pending_transactions(
%Address{hash: %Hash{byte_count: unquote(Hash.Address.byte_count())} = address_hash},
options \\ []
)
when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
options
|> Keyword.get(:direction)
|> case do
:from -> [:from_address_hash]
:to -> [:to_address_hash]
_ -> [:from_address_hash, :to_address_hash]
end
|> Enum.map(fn address_field ->
Transaction
|> Transaction.where_address_fields_match(address_hash, address_field)
|> join_associations(necessity_by_association)
|> where([transaction], is_nil(transaction.block_number))
|> order_by([transaction], desc: transaction.inserted_at, desc: transaction.hash)
|> Repo.all()
|> MapSet.new()
end)
|> Enum.reduce(MapSet.new(), &MapSet.union/2)
|> MapSet.to_list()
end
@doc """ @doc """
Gets an estimated count of `t:Explorer.Chain.Transaction.t/0` to or from the `address` based on the estimated rows Gets an estimated count of `t:Explorer.Chain.Transaction.t/0` to or from the `address` based on the estimated rows
resulting in an EXPLAIN of the query plan for the count query. resulting in an EXPLAIN of the query plan for the count query.

@ -32,6 +32,70 @@ defmodule Explorer.ChainTest do
end end
end end
describe "address_to_pending_transactions/2" do
test "without pending transactions" do
address = insert(:address)
assert Repo.aggregate(Transaction, :count, :hash) == 0
assert [] == Chain.address_to_pending_transactions(address)
end
test "with from pending transactions" do
address = insert(:address)
transaction = insert(:transaction, from_address: address)
assert [transaction] ==
Chain.address_to_pending_transactions(address, direction: :from)
|> Repo.preload([:to_address, :from_address])
end
test "with to transactions" do
address = insert(:address)
transaction = insert(:transaction, to_address: address)
assert [transaction] ==
Chain.address_to_pending_transactions(address, direction: :to)
|> Repo.preload([:to_address, :from_address])
end
test "with to and from transactions and direction: :from" do
address = insert(:address)
transaction = insert(:transaction, from_address: address)
insert(:transaction, to_address: address)
# only contains "from" transaction
assert [transaction] ==
Chain.address_to_pending_transactions(address, direction: :from)
|> Repo.preload([:to_address, :from_address])
end
test "with to and from transactions and direction: :to" do
address = insert(:address)
transaction = insert(:transaction, to_address: address)
insert(:transaction, from_address: address)
assert [transaction] ==
Chain.address_to_pending_transactions(address, direction: :to)
|> Repo.preload([:to_address, :from_address])
end
test "with to and from transactions and no :direction option" do
address = insert(:address)
transaction1 = insert(:transaction, from_address: address)
transaction2 = insert(:transaction, to_address: address)
assert [transaction1, transaction2] ==
Chain.address_to_pending_transactions(address)
|> Repo.preload([:to_address, :from_address])
end
end
describe "address_to_transactions/2" do describe "address_to_transactions/2" do
test "without transactions" do test "without transactions" do
address = insert(:address) address = insert(:address)

Loading…
Cancel
Save