diff --git a/apps/explorer_web/assets/__tests__/pages/transaction.js b/apps/explorer_web/assets/__tests__/pages/transaction.js index 55aa68f4b4..c7c2364cfd 100644 --- a/apps/explorer_web/assets/__tests__/pages/transaction.js +++ b/apps/explorer_web/assets/__tests__/pages/transaction.js @@ -12,3 +12,119 @@ test('RECEIVED_NEW_BLOCK', () => { expect(output.confirmations).toBe(4) }) + +describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { + test('single transaction', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual(['test']) + expect(output.batchCountAccumulator).toEqual(0) + }) + test('large batch of transactions', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 1' + },{ + transactionHtml: 'test 2' + },{ + transactionHtml: 'test 3' + },{ + transactionHtml: 'test 4' + },{ + transactionHtml: 'test 5' + },{ + transactionHtml: 'test 6' + },{ + transactionHtml: 'test 7' + },{ + transactionHtml: 'test 8' + },{ + transactionHtml: 'test 9' + },{ + transactionHtml: 'test 10' + },{ + transactionHtml: 'test 11' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(11) + }) + test('single transaction after single transaction', () => { + const state = Object.assign({}, initialState, { + newTransactions: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 2' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual(['test 1', 'test 2']) + expect(output.batchCountAccumulator).toEqual(0) + }) + test('single transaction after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + newTransactions: [], + batchCountAccumulator: 11 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 12' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(12) + }) + test('large batch of transactions after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + newTransactions: [], + batchCountAccumulator: 11 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 12' + },{ + transactionHtml: 'test 13' + },{ + transactionHtml: 'test 14' + },{ + transactionHtml: 'test 15' + },{ + transactionHtml: 'test 16' + },{ + transactionHtml: 'test 17' + },{ + transactionHtml: 'test 18' + },{ + transactionHtml: 'test 19' + },{ + transactionHtml: 'test 20' + },{ + transactionHtml: 'test 21' + },{ + transactionHtml: 'test 22' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(22) + }) +}) diff --git a/apps/explorer_web/assets/js/pages/chain.js b/apps/explorer_web/assets/js/pages/chain.js index 31d0297b3a..dbe1873731 100644 --- a/apps/explorer_web/assets/js/pages/chain.js +++ b/apps/explorer_web/assets/js/pages/chain.js @@ -11,7 +11,6 @@ const BATCH_THRESHOLD = 10 export const initialState = { batchCountAccumulator: 0, - channelDisconnected: false, newBlock: null, newTransactions: [] } diff --git a/apps/explorer_web/assets/js/pages/transaction.js b/apps/explorer_web/assets/js/pages/transaction.js index 53a4b88f76..7a9410c190 100644 --- a/apps/explorer_web/assets/js/pages/transaction.js +++ b/apps/explorer_web/assets/js/pages/transaction.js @@ -4,11 +4,16 @@ import numeral from 'numeral' import 'numeral/locales' import socket from '../socket' import router from '../router' -import { initRedux } from '../utils' +import { updateAllAges } from '../lib/from_now' +import { batchChannel, initRedux } from '../utils' + +const BATCH_THRESHOLD = 10 export const initialState = { + batchCountAccumulator: 0, blockNumber: null, - confirmations: null + confirmations: null, + newTransactions: [] } export function reducer (state = initialState, action) { @@ -25,6 +30,21 @@ export function reducer (state = initialState, action) { }) } else return state } + case 'RECEIVED_NEW_TRANSACTION_BATCH': { + debugger + if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { + return Object.assign({}, state, { + newTransactions: [ + ...state.newTransactions, + ...action.msgs.map(({transactionHtml}) => transactionHtml) + ] + }) + } else { + return Object.assign({}, state, { + batchCountAccumulator: state.batchCountAccumulator + action.msgs.length + }) + } + } default: return state } @@ -41,8 +61,41 @@ router.when('/transactions/:transactionHash').then(({ locale }) => initRedux(red }, render (state, oldState) { const $blockConfirmations = $('[data-selector="block-confirmations"]') + if (oldState.confirmations !== state.confirmations) { $blockConfirmations.empty().append(numeral(state.confirmations).format()) } } })) + +router.when('/transactions', { exactPathMatch: true }).then(({ locale }) => initRedux(reducer, { + main (store) { + const transactionsChannel = socket.channel(`transactions:new_transaction`) + transactionsChannel.join() + transactionsChannel.on('new_transaction', batchChannel((msgs) => + store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) })) + ) + }, + render (state, oldState) { + const $channelBatching = $('[data-selector="channel-batching-message"]') + const $channelBatchingCount = $('[data-selector="channel-batching-count"]') + const $transactionsList = $('[data-selector="transactions-list"]') + + if (state.batchCountAccumulator) { + $channelBatching.show() + $channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() + } else { + $channelBatching.hide() + } + if (oldState.newTransactions !== state.newTransactions) { + const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length) + $transactionsList + .children() + .slice($transactionsList.children().length - newTransactionsToInsert.length, $transactionsList.children().length) + .remove() + $transactionsList.prepend(newTransactionsToInsert.reverse().join('')) + + updateAllAges() + } + } +})) diff --git a/apps/explorer_web/lib/explorer_web/channels/transaction_channel.ex b/apps/explorer_web/lib/explorer_web/channels/transaction_channel.ex index 1bc8883898..516170a185 100644 --- a/apps/explorer_web/lib/explorer_web/channels/transaction_channel.ex +++ b/apps/explorer_web/lib/explorer_web/channels/transaction_channel.ex @@ -4,7 +4,7 @@ defmodule ExplorerWeb.TransactionChannel do """ use ExplorerWeb, :channel - alias ExplorerWeb.ChainView + alias ExplorerWeb.{ChainView, TransactionView} alias Phoenix.View intercept(["new_transaction"]) @@ -24,8 +24,17 @@ defmodule ExplorerWeb.TransactionChannel do transaction: transaction ) + rendered_transaction = + View.render_to_string( + TransactionView, + "_transaction.html", + locale: socket.assigns.locale, + transaction: transaction + ) + push(socket, "new_transaction", %{ - homepage_transaction_html: rendered_homepage_transaction + homepage_transaction_html: rendered_homepage_transaction, + transaction_html: rendered_transaction }) {:noreply, socket} diff --git a/apps/explorer_web/lib/explorer_web/templates/transaction/_transaction.html.eex b/apps/explorer_web/lib/explorer_web/templates/transaction/_transaction.html.eex new file mode 100644 index 0000000000..a358702df2 --- /dev/null +++ b/apps/explorer_web/lib/explorer_web/templates/transaction/_transaction.html.eex @@ -0,0 +1,33 @@ +
+
+
+
+ <%= ExplorerWeb.TransactionView.transaction_display_type(@transaction) %> +
<%= ExplorerWeb.TransactionView.formatted_status(@transaction) %>
+
+
+
+ <%= render ExplorerWeb.TransactionView, "_link.html", locale: @locale, transaction_hash: @transaction.hash %> + + <%= render ExplorerWeb.AddressView, "_link.html", address_hash: @transaction.from_address_hash, contract: ExplorerWeb.AddressView.contract?(@transaction.from_address), locale: @locale %> + → + <%= render ExplorerWeb.AddressView, "_link.html", address_hash: ExplorerWeb.TransactionView.to_address_hash(@transaction), contract: ExplorerWeb.AddressView.contract?(@transaction.to_address), locale: @locale %> + + + + + <%= link( + gettext("Block #%{number}", number: to_string(@transaction.block.number)), + to: block_path(ExplorerWeb.Endpoint, :show, @locale, @transaction.block) + ) %> + + +
+
+ + <%= ExplorerWeb.TransactionView.value(@transaction, include_label: false) %> <%= gettext "Ether" %> + + <%= ExplorerWeb.TransactionView.formatted_fee(@transaction, denomination: :ether) %> <%= gettext "Fee" %> +
+
+
diff --git a/apps/explorer_web/lib/explorer_web/templates/transaction/index.html.eex b/apps/explorer_web/lib/explorer_web/templates/transaction/index.html.eex index eff3762f30..1ad86ed911 100644 --- a/apps/explorer_web/lib/explorer_web/templates/transaction/index.html.eex +++ b/apps/explorer_web/lib/explorer_web/templates/transaction/index.html.eex @@ -43,42 +43,19 @@
+
+ +

<%= gettext "Transactions" %>

<%= gettext("Showing %{count} Validated Transactions", count: Cldr.Number.to_string!(@transaction_estimated_count, format: "#,###")) %>

+ + <%= for transaction <- @transactions do %> + <%= render ExplorerWeb.TransactionView, "_transaction.html", locale: @locale, transaction: transaction %> + <% end %> + - <%= for transaction <- @transactions do %> -
-
-
-
- <%= ExplorerWeb.TransactionView.transaction_display_type(transaction) %> -
<%= ExplorerWeb.TransactionView.formatted_status(transaction) %>
-
-
-
- <%= render ExplorerWeb.TransactionView, "_link.html", locale: @locale, transaction_hash: transaction.hash %> - - <%= render ExplorerWeb.AddressView, "_link.html", address_hash: transaction.from_address_hash, contract: ExplorerWeb.AddressView.contract?(transaction.from_address), locale: @locale %> - → - <%= render ExplorerWeb.AddressView, "_link.html", address_hash: ExplorerWeb.TransactionView.to_address_hash(transaction), contract: ExplorerWeb.AddressView.contract?(transaction.to_address), locale: @locale %> - - - - - <%= link( - gettext("Block #%{number}", number: to_string(transaction.block.number)), - to: block_path(@conn, :show, @conn.assigns.locale, transaction.block) - ) %> - - -
-
- <%= ExplorerWeb.TransactionView.value(transaction, include_label: false) %> <%= gettext "Ether" %> - <%= ExplorerWeb.TransactionView.formatted_fee(transaction, denomination: :ether) %> <%= gettext "Fee" %> -
-
-
- <% end %> <%= if @next_page_params do %> <%= link( gettext("Older"), diff --git a/apps/explorer_web/test/explorer_web/features/pages/transaction_list_page.ex b/apps/explorer_web/test/explorer_web/features/pages/transaction_list_page.ex index 68963594e8..53927f15b5 100644 --- a/apps/explorer_web/test/explorer_web/features/pages/transaction_list_page.ex +++ b/apps/explorer_web/test/explorer_web/features/pages/transaction_list_page.ex @@ -19,6 +19,10 @@ defmodule ExplorerWeb.TransactionListPage do css("[data-transaction-hash='#{hash}'] [data-test='transaction_type']", text: "Contract Creation") end + def non_loaded_transaction_count(count) do + css("[data-selector='channel-batching-count']", text: count) + end + def transaction(%Transaction{hash: transaction_hash}) do css("[data-transaction-hash='#{transaction_hash}']") end diff --git a/apps/explorer_web/test/explorer_web/features/viewing_transactions_test.exs b/apps/explorer_web/test/explorer_web/features/viewing_transactions_test.exs index 4691c6ebec..e395a75081 100644 --- a/apps/explorer_web/test/explorer_web/features/viewing_transactions_test.exs +++ b/apps/explorer_web/test/explorer_web/features/viewing_transactions_test.exs @@ -113,8 +113,7 @@ defmodule ExplorerWeb.ViewingTransactionsTest do Notifier.handle_event({:chain_event, :transactions, transaction_hashes}) - session - |> assert_has(AddressPage.non_loaded_transaction_count("30")) + assert_has(session, AddressPage.non_loaded_transaction_count("30")) end test "contract creation is shown for to_address on home page", %{session: session} do @@ -163,6 +162,33 @@ defmodule ExplorerWeb.ViewingTransactionsTest do |> TransactionListPage.visit_page() |> assert_has(TransactionListPage.contract_creation(transaction)) end + + test "viewing new transactions via live update on list page", %{session: session} do + TransactionListPage.visit_page(session) + + transaction = + :transaction + |> insert() + |> with_block() + + Notifier.handle_event({:chain_event, :transactions, [transaction.hash]}) + + assert_has(session, TransactionListPage.transaction(transaction)) + end + + test "count of non-loaded transactions on list page live update when batch overflow", %{session: session} do + transaction_hashes = + 30 + |> insert_list(:transaction) + |> with_block() + |> Enum.map(& &1.hash) + + TransactionListPage.visit_page(session) + + Notifier.handle_event({:chain_event, :transactions, transaction_hashes}) + + assert_has(session, TransactionListPage.non_loaded_transaction_count("30")) + end end describe "viewing a transaction page" do