From 95d01e177f16dd710c9b30fbcf5fdb02b069d1b8 Mon Sep 17 00:00:00 2001 From: Stamates Date: Mon, 24 Sep 2018 11:17:59 -0400 Subject: [PATCH] Add pending transactions to top of address_transactions list. Standardize to transaction tile for pending transactions. Live load pending transactions and remove when collated. --- .../assets/__tests__/pages/address.js | 263 +++++++++++++++++- .../assets/js/pages/address.js | 84 +++++- .../assets/js/pages/transaction.js | 4 +- .../channels/address_channel.ex | 11 +- .../channels/transaction_channel.ex | 2 +- .../address_transaction_controller.ex | 9 +- .../lib/block_scout_web/notifier.ex | 18 +- .../templates/address/index.html.eex | 2 +- .../address_transaction/index.html.eex | 9 +- .../templates/block/_link.html.eex | 4 + .../internal_transaction/_tile.html.eex | 4 +- .../pending_transaction/index.html.eex | 2 +- .../templates/transaction/_tile.html.eex | 11 +- .../transaction/_token_transfer.html.eex | 4 +- .../templates/transaction/overview.html.eex | 4 +- .../lib/block_scout_web/views/address_view.ex | 10 +- .../block_scout_web/views/transaction_view.ex | 8 +- .../lib/block_scout_web/views/view_helpers.ex | 16 ++ .../channels/address_channel_test.exs | 15 + .../channels/transaction_channel_test.exs | 29 +- .../address_transaction_controller_test.exs | 12 +- .../features/pages/address_page.ex | 14 + .../features/viewing_addresses_test.exs | 47 ++++ .../views/address_view_test.exs | 63 ++--- .../views/transaction_view_test.exs | 44 ++- .../views/view_helpers_test.exs | 48 ++++ apps/explorer/lib/explorer/chain.ex | 38 +++ apps/explorer/test/explorer/chain_test.exs | 64 +++++ 28 files changed, 744 insertions(+), 95 deletions(-) create mode 100644 apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex create mode 100644 apps/block_scout_web/lib/block_scout_web/views/view_helpers.ex create mode 100644 apps/block_scout_web/test/block_scout_web/views/view_helpers_test.exs diff --git a/apps/block_scout_web/assets/__tests__/pages/address.js b/apps/block_scout_web/assets/__tests__/pages/address.js index 4a8c9a28ec..be8b48c46f 100644 --- a/apps/block_scout_web/assets/__tests__/pages/address.js +++ b/apps/block_scout_web/assets/__tests__/pages/address.js @@ -5,16 +5,18 @@ describe('PAGE_LOAD', () => { const state = initialState const action = { type: 'PAGE_LOAD', + addressHash: '1234', beyondPageOne: false, - addressHash: '1234' + pendingTransactionHashes: ['0x00'] } const output = reducer(state, action) expect(output.addressHash).toBe('1234') - expect(output.filter).toBe(undefined) 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 action = { type: 'PAGE_LOAD', @@ -41,7 +43,7 @@ describe('PAGE_LOAD', () => { expect(output.filter).toBe('to') expect(output.beyondPageOne).toBe(false) }) - test('page 2+ with "to" filter', () => { + test('page 2 with "to" filter', () => { const state = initialState const action = { type: 'PAGE_LOAD', @@ -81,6 +83,192 @@ test('RECEIVED_UPDATED_BALANCE', () => { 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', () => { test('single transaction', () => { const state = initialState @@ -212,7 +400,7 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { expect(output.newTransactions).toEqual([]) expect(output.batchCountAccumulator).toEqual(0) }) - test('on page 2+', () => { + test('on page 2', () => { const state = Object.assign({}, initialState, { beyondPageOne: true }) @@ -291,4 +479,69 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { 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) + }) }) diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index c7aecc7b55..31f722874b 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -1,4 +1,5 @@ import $ from 'jquery' +import _ from 'lodash' import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' @@ -12,13 +13,17 @@ const BATCH_THRESHOLD = 10 export const initialState = { addressHash: null, + balance: null, batchCountAccumulator: 0, + batchPendingCountAccumulator: 0, beyondPageOne: null, channelDisconnected: false, filter: null, newInternalTransactions: [], + newPendingTransactions: [], newTransactions: [], - balance: null, + newTransactionHashes: [], + pendingTransactionHashes: [], transactionCount: null } @@ -29,6 +34,7 @@ export function reducer (state = initialState, action) { addressHash: action.addressHash, beyondPageOne: action.beyondPageOne, filter: action.filter, + pendingTransactionHashes: action.pendingTransactionHashes, transactionCount: numeral(action.transactionCount).value() }) } @@ -59,7 +65,7 @@ export function reducer (state = initialState, action) { return Object.assign({}, state, { newInternalTransactions: [ ...state.newInternalTransactions, - ...incomingInternalTransactions.map(({internalTransactionHtml}) => internalTransactionHtml) + ..._.map(incomingInternalTransactions, 'internalTransactionHtml') ] }) } 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': { if (state.channelDisconnected || state.beyondPageOne) return state @@ -78,17 +114,26 @@ export function reducer (state = initialState, action) { (state.filter === 'from' && fromAddressHash === state.addressHash) )) + const updatePendingTransactionHashes = + _.difference(state.pendingTransactionHashes, _.map(incomingTransactions, 'transactionHash')) + if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { return Object.assign({}, state, { + batchPendingCountAccumulator: state.batchPendingCountAccumulator - action.msgs.length, newTransactions: [ ...state.newTransactions, - ...incomingTransactions.map(({transactionHtml}) => transactionHtml) + ..._.map(incomingTransactions, 'transactionHtml') ], + newTransactionHashes: _.map(incomingTransactions, 'transactionHash'), + pendingTransactionHashes: updatePendingTransactionHashes, transactionCount: state.transactionCount + action.msgs.length }) } else { return Object.assign({}, state, { batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, + batchPendingCountAccumulator: state.batchPendingCountAccumulator - action.msgs.length, + newTransactionHashes: _.map(incomingTransactions, 'transactionHash'), + pendingTransactionHashes: updatePendingTransactionHashes, transactionCount: state.transactionCount + action.msgs.length }) } @@ -116,8 +161,12 @@ if ($addressDetailsPage.length) { const state = store.dispatch({ type: 'PAGE_LOAD', addressHash, - filter, 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() }) addressChannel.join() @@ -137,16 +186,25 @@ if ($addressDetailsPage.length) { addressChannel.on('internal_transaction', batchChannel((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) { const $balance = $('[data-selector="balance-card"]') const $channelBatching = $('[data-selector="channel-batching-message"]') 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 $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]') const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]') const $internalTransactionsList = $('[data-selector="internal-transactions-list"]') + const $pendingTransactionsList = $('[data-selector="pending-transactions-list"]') const $transactionCount = $('[data-selector="transaction-count"]') const $transactionsList = $('[data-selector="transactions-list"]') const $validationsList = $('[data-selector="validations-list"]') @@ -166,10 +224,28 @@ if ($addressDetailsPage.length) { } else { $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) { prependWithClingBottom($internalTransactionsList, state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join('')) 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) { prependWithClingBottom($transactionsList, state.newTransactions.slice(oldState.newTransactions.length).reverse().join('')) updateAllAges() diff --git a/apps/block_scout_web/assets/js/pages/transaction.js b/apps/block_scout_web/assets/js/pages/transaction.js index 5468cef4a8..5011192c82 100644 --- a/apps/block_scout_web/assets/js/pages/transaction.js +++ b/apps/block_scout_web/assets/js/pages/transaction.js @@ -91,7 +91,7 @@ export function reducer (state = initialState, action) { return Object.assign({}, state, { newTransactions: [ ...state.newTransactions, - ...action.msgs.map(({transactionHtml}) => transactionHtml) + ..._.map(action.msgs, 'transactionHtml') ], transactionCount }) @@ -147,7 +147,7 @@ if ($transactionPendingListPage.length) { const transactionsChannel = socket.channel(`transactions:new_transaction`) transactionsChannel.join() 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) }) ) const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`) diff --git a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex index cfb1d50fcf..bb3f3d6267 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex @@ -4,10 +4,11 @@ defmodule BlockScoutWeb.AddressChannel do """ use BlockScoutWeb, :channel + alias Explorer.Chain.Hash alias BlockScoutWeb.{InternalTransactionView, AddressView, TransactionView} 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 {:ok, %{}, socket} @@ -60,7 +61,10 @@ defmodule BlockScoutWeb.AddressChannel do {:noreply, socket} 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) rendered = @@ -71,9 +75,10 @@ defmodule BlockScoutWeb.AddressChannel do transaction: transaction ) - push(socket, "transaction", %{ + push(socket, event, %{ to_address_hash: to_string(transaction.to_address_hash), from_address_hash: to_string(transaction.from_address_hash), + transaction_hash: Hash.to_string(transaction.hash), transaction_html: rendered }) diff --git a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex index 1080bde074..0d9dc07e01 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex @@ -28,7 +28,7 @@ defmodule BlockScoutWeb.TransactionChannel do rendered_transaction = View.render_to_string( TransactionView, - "_pending_tile.html", + "_tile.html", transaction: transaction ) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 584d28efba..7233d7eab6 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -14,18 +14,20 @@ defmodule BlockScoutWeb.AddressTransactionController do def index(conn, %{"address_id" => address_hash_string} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.hash_to_address(address_hash) do - full_options = + pending_options = [ necessity_by_association: %{ - :block => :required, [created_contract_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(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, 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), exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), filter: params["filter"], + pending_transactions: Chain.address_to_pending_transactions(address, pending_options), transactions: transactions, transaction_count: transaction_count(address), validation_count: validation_count(address) diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 675f5860ac..cbe6e666c3 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -94,25 +94,27 @@ defmodule BlockScoutWeb.Notifier do end defp broadcast_transaction(%Transaction{block_number: nil} = pending) do - Endpoint.broadcast("transactions:new_pending_transaction", "new_pending_transaction", %{ - transaction: pending - }) + broadcast_transaction(pending, "transactions:new_pending_transaction", "pending_transaction") end defp broadcast_transaction(transaction) do - Endpoint.broadcast("transactions:new_transaction", "new_transaction", %{ - transaction: transaction - }) + broadcast_transaction(transaction, "transactions:new_transaction", "transaction") + end + defp broadcast_transaction(transaction, transaction_channel, event) do 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, transaction: transaction }) 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, transaction: transaction }) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex index 48e5a2bfd9..7fb56b70ac 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex @@ -21,7 +21,7 @@
- <%= address |> BlockScoutWeb.AddressView.address_partial_selector(nil, nil) |> BlockScoutWeb.AddressView.render_partial() %> + <%= address |> BlockScoutWeb.AddressView.address_partial_selector(nil, nil) |> BlockScoutWeb.ViewHelpers.render_partial() %> <%= transaction_count(address) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex index 6287a89ec4..8c923e2f0c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_transaction/index.html.eex @@ -55,8 +55,15 @@ ) %>
- <%= if Enum.count(@transactions) > 0 do %> + <%= if Enum.count(@transactions) > 0 or Enum.count(@pending_transactions) > 0 do %>

<%= gettext "Transactions" %>

+ <% end %> +
+ <%= for pending_transaction <- @pending_transactions do %> + <%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: pending_transaction) %> + <% end %> +
+ <%= if Enum.count(@transactions) > 0 do %> <%= for transaction <- @transactions do %> <%= render(BlockScoutWeb.TransactionView, "_tile.html", current_address: @address, transaction: transaction) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex new file mode 100644 index 0000000000..d910c2ad19 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/block/_link.html.eex @@ -0,0 +1,4 @@ +<%= link( + gettext("Block #%{number}", number: to_string(@block.number)), + to: block_path(BlockScoutWeb.Endpoint, :show, @block) +) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex index f9df99365b..3f2fc3b3d2 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/internal_transaction/_tile.html.eex @@ -6,9 +6,9 @@
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @internal_transaction.transaction_hash %> - <%= @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() %> → - <%= @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() %> <%= BlockScoutWeb.TransactionView.value(@internal_transaction, include_label: false) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex index 4990a69919..70cff64a50 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex @@ -61,7 +61,7 @@

<%= for transaction <- @transactions do %> - <%= render BlockScoutWeb.TransactionView, "_pending_tile.html", transaction: transaction %> + <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex index fb5b733494..7d7262970b 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex @@ -11,9 +11,9 @@
<%= render "_link.html", transaction_hash: @transaction.hash %> - <%= @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() %> → - <%= @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() %> @@ -26,12 +26,9 @@
- <%= link( - gettext("Block #%{number}", number: to_string(@transaction.block_number)), - to: block_path(BlockScoutWeb.Endpoint, :show, @transaction.block) - ) %> + <%= @transaction |> block_number() |> BlockScoutWeb.ViewHelpers.render_partial() %> - + <%= if from_or_to_address?(@transaction, assigns[:current_address]) do %> <%= if @transaction.from_address_hash == assigns[:current_address].hash do %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex index 3ee194e882..cfc62309b6 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_token_transfer.html.eex @@ -11,9 +11,9 @@ <% 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() %> → - <%= @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() %> <%= token_transfer_amount(@token_transfer) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex index 31532b0b14..6b90f00633 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/overview.html.eex @@ -26,9 +26,9 @@

<%= @transaction %>

- <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, nil) |> BlockScoutWeb.AddressView.render_partial() %> + <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:from, nil) |> BlockScoutWeb.ViewHelpers.render_partial() %> - <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, nil) |> BlockScoutWeb.AddressView.render_partial() %> + <%= @transaction |> BlockScoutWeb.AddressView.address_partial_selector(:to, nil) |> BlockScoutWeb.ViewHelpers.render_partial() %>
<%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex index e6074a53b5..c536d52398 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_view.ex @@ -181,21 +181,23 @@ defmodule BlockScoutWeb.AddressView do def trimmed_hash(_), do: "" defp matching_address_check(%Address{hash: hash} = current_address, %Address{hash: hash}, contract?, truncate) do - %{ + [ + view_module: __MODULE__, partial: "_responsive_hash.html", address: current_address, contract: contract?, truncate: truncate - } + ] end defp matching_address_check(_current_address, %Address{} = address, contract?, truncate) do - %{ + [ + view_module: __MODULE__, partial: "_link.html", address: address, contract: contract?, truncate: truncate - } + ] end @doc """ diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 71c035c6ae..2f0d75d90c 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.TransactionView do alias Cldr.Number alias Explorer.Chain - alias Explorer.Chain.{Address, InternalTransaction, Transaction, Wei} + alias Explorer.Chain.{Address, Block, InternalTransaction, Transaction, Wei} alias BlockScoutWeb.{AddressView, BlockView, TabHelpers} import BlockScoutWeb.Gettext @@ -14,6 +14,12 @@ defmodule BlockScoutWeb.TransactionView do 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 case block do nil -> 0 diff --git a/apps/block_scout_web/lib/block_scout_web/views/view_helpers.ex b/apps/block_scout_web/lib/block_scout_web/views/view_helpers.ex new file mode 100644 index 0000000000..48efe374e9 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/views/view_helpers.ex @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs index 33ccf76331..6408cb8ea6 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/address_channel_test.exs @@ -51,6 +51,21 @@ defmodule BlockScoutWeb.AddressChannelTest do 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 transaction = :transaction diff --git a/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs index 3335b859d6..9b4f66f224 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs @@ -1,9 +1,10 @@ defmodule BlockScoutWeb.TransactionChannelTest do use BlockScoutWeb.ChannelCase + alias Explorer.Chain.Hash 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" @endpoint.subscribe(topic) @@ -15,7 +16,7 @@ defmodule BlockScoutWeb.TransactionChannelTest do Notifier.handle_event({:chain_event, :transactions, [transaction.hash]}) 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 after 5_000 -> @@ -23,7 +24,7 @@ defmodule BlockScoutWeb.TransactionChannelTest do 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" @endpoint.subscribe(topic) @@ -32,11 +33,31 @@ defmodule BlockScoutWeb.TransactionChannelTest do Notifier.handle_event({:chain_event, :transactions, [pending.hash]}) 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 after 5_000 -> assert false, "Expected message received nothing." 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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs index 2cb80ae049..e5ae7c5b6e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_transaction_controller_test.exs @@ -45,18 +45,20 @@ defmodule BlockScoutWeb.AddressTransactionControllerTest do assert Enum.member?(actual_transaction_hashes, to_transaction.hash) end - test "does not return related transactions without a block", %{conn: conn} do + test "returns pending related transactions", %{conn: conn} do 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)) + actual_pending_transaction_hashes = + conn.assigns.pending_transactions + |> Enum.map(& &1.hash) + assert html_response(conn, 200) assert conn.status == 200 - assert Enum.empty?(conn.assigns.transactions) - assert conn.status == 200 - assert Enum.empty?(conn.assigns.transactions) + assert Enum.member?(actual_pending_transaction_hashes, pending.hash) end test "includes USD exchange rate value for address in assigns", %{conn: conn} do diff --git a/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex b/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex index d4ad3ae449..e77313b9b6 100644 --- a/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex +++ b/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex @@ -71,6 +71,14 @@ defmodule BlockScoutWeb.AddressPage do css("[data-test='address_detail_hash']", text: to_string(address_hash)) 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 css("[data-test='internal_transaction'][data-internal-transaction-id='#{id}']") 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}']") 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(%Hash{} = hash) do diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs index c89847d0c6..83a93f835f 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs @@ -123,6 +123,53 @@ defmodule BlockScoutWeb.ViewingAddressesTest do |> assert_has(AddressPage.transaction_status(transactions.from_lincoln)) 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", %{ addresses: addresses, session: session, diff --git a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs index c2ccb131b7..41ac8a75a2 100644 --- a/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/address_view_test.exs @@ -21,97 +21,105 @@ defmodule BlockScoutWeb.AddressViewTest do 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 test "will truncate address" do transaction = %Transaction{to_address: to_address} = insert(:transaction) - assert %{ + assert [ + view_module: AddressView, partial: "_link.html", address: ^to_address, contract: false, truncate: true - } = AddressView.address_partial_selector(transaction, :to, nil, true) + ] = AddressView.address_partial_selector(transaction, :to, nil, true) end test "for a non-contract to address not on address page" do transaction = %Transaction{to_address: to_address} = insert(:transaction) - assert %{ + assert [ + view_module: AddressView, partial: "_link.html", address: ^to_address, contract: false, truncate: false - } = AddressView.address_partial_selector(transaction, :to, nil) + ] = AddressView.address_partial_selector(transaction, :to, nil) end test "for a non-contract to address non matching address page" do transaction = %Transaction{to_address: to_address} = insert(:transaction) - assert %{ + assert [ + view_module: AddressView, partial: "_link.html", address: ^to_address, contract: false, truncate: false - } = AddressView.address_partial_selector(transaction, :to, nil) + ] = AddressView.address_partial_selector(transaction, :to, nil) end test "for a non-contract to address matching address page" do transaction = %Transaction{to_address: to_address} = insert(:transaction) - assert %{ + assert [ + view_module: AddressView, partial: "_responsive_hash.html", address: ^to_address, contract: false, truncate: false - } = AddressView.address_partial_selector(transaction, :to, transaction.to_address) + ] = AddressView.address_partial_selector(transaction, :to, transaction.to_address) end test "for a contract to address non matching address page" do contract_address = insert(:contract_address) transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address) - assert %{ + assert [ + view_module: AddressView, partial: "_link.html", address: ^contract_address, contract: true, truncate: false - } = AddressView.address_partial_selector(transaction, :to, transaction.to_address) + ] = AddressView.address_partial_selector(transaction, :to, transaction.to_address) end test "for a contract to address matching address page" do contract_address = insert(:contract_address) transaction = insert(:transaction, to_address: nil, created_contract_address: contract_address) - assert %{ + assert [ + view_module: AddressView, partial: "_responsive_hash.html", address: ^contract_address, contract: true, truncate: false - } = AddressView.address_partial_selector(transaction, :to, contract_address) + ] = AddressView.address_partial_selector(transaction, :to, contract_address) end test "for a non-contract from address not on address page" do transaction = %Transaction{to_address: to_address} = insert(:transaction) - assert %{ + assert [ + view_module: AddressView, partial: "_link.html", address: ^to_address, contract: false, truncate: false - } = AddressView.address_partial_selector(transaction, :to, nil) + ] = AddressView.address_partial_selector(transaction, :to, nil) end test "for a non-contract from address matching address page" do transaction = %Transaction{from_address: from_address} = insert(:transaction) - assert %{ + assert [ + view_module: AddressView, partial: "_responsive_hash.html", address: ^from_address, contract: false, truncate: false - } = AddressView.address_partial_selector(transaction, :from, transaction.from_address) + ] = AddressView.address_partial_selector(transaction, :from, transaction.from_address) end end @@ -187,27 +195,6 @@ defmodule BlockScoutWeb.AddressViewTest do 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 test "returns true when smart contract is verified" do smart_contract = insert(:smart_contract) diff --git a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs index 82df6450a6..6008640d7f 100644 --- a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs @@ -3,7 +3,49 @@ defmodule BlockScoutWeb.TransactionViewTest do alias Explorer.Chain.Wei 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 test "returns 0 if pending transaction" do diff --git a/apps/block_scout_web/test/block_scout_web/views/view_helpers_test.exs b/apps/block_scout_web/test/block_scout_web/views/view_helpers_test.exs new file mode 100644 index 0000000000..8771f85759 --- /dev/null +++ b/apps/block_scout_web/test/block_scout_web/views/view_helpers_test.exs @@ -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 diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index 95f6097c20..d34ff3c7b3 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -134,6 +134,44 @@ defmodule Explorer.Chain do |> Repo.all() 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 """ 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. diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 581bff3f8b..d25c56e212 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -32,6 +32,70 @@ defmodule Explorer.ChainTest do 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 test "without transactions" do address = insert(:address)