diff --git a/apps/explorer_web/assets/__tests__/pages/transaction.js b/apps/explorer_web/assets/__tests__/pages/transaction.js new file mode 100644 index 0000000000..ee4cf72e33 --- /dev/null +++ b/apps/explorer_web/assets/__tests__/pages/transaction.js @@ -0,0 +1,14 @@ +import { reducer, initialState } from '../../js/pages/transaction' + +test('RECEIVED_UPDATED_CONFIRMATIONS', () => { + const state = initialState + const action = { + type: 'RECEIVED_UPDATED_CONFIRMATIONS', + msg: { + confirmations: 5 + } + } + const output = reducer(state, action) + + expect(output.confirmations).toBe(5) +}) diff --git a/apps/explorer_web/assets/js/app.js b/apps/explorer_web/assets/js/app.js index 70766708c7..5b071039f0 100644 --- a/apps/explorer_web/assets/js/app.js +++ b/apps/explorer_web/assets/js/app.js @@ -25,3 +25,4 @@ import './lib/reload_button' import './lib/tooltip' import './pages/address' +import './pages/transaction' diff --git a/apps/explorer_web/assets/js/pages/transaction.js b/apps/explorer_web/assets/js/pages/transaction.js new file mode 100644 index 0000000000..79cafefc6c --- /dev/null +++ b/apps/explorer_web/assets/js/pages/transaction.js @@ -0,0 +1,38 @@ +import $ from 'jquery' +import numeral from 'numeral' +import 'numeral/locales' +import socket from '../socket' +import router from '../router' +import { initRedux } from '../utils' + +export const initialState = {confirmations: null} + +export function reducer (state = initialState, action) { + switch (action.type) { + case 'RECEIVED_UPDATED_CONFIRMATIONS': { + return Object.assign({}, state, { + confirmations: action.msg.confirmations + }) + } + default: + return state + } +} + +router.when('/transactions/:transactionHash').then((params) => initRedux(reducer, { + main (store) { + const { transactionHash, locale } = params + const channel = socket.channel(`transactions:${transactionHash}`, {}) + numeral.locale(locale) + channel.join() + .receive('ok', resp => { console.log('Joined successfully', `transactions:${transactionHash}`, resp) }) + .receive('error', resp => { console.log('Unable to join', `transactions:${transactionHash}`, resp) }) + channel.on('confirmations', (msg) => store.dispatch({ type: 'RECEIVED_UPDATED_CONFIRMATIONS', msg })) + }, + render (state, oldState) { + const $blockConfirmations = $('[data-selector="block_confirmations"]') + if (oldState.confirmations !== state.confirmations) { + $blockConfirmations.empty().append(numeral(msg.confirmations).format()) + } + } +})) diff --git a/apps/explorer_web/lib/explorer_web/channels/transaction_channel.ex b/apps/explorer_web/lib/explorer_web/channels/transaction_channel.ex new file mode 100644 index 0000000000..b382d86c3d --- /dev/null +++ b/apps/explorer_web/lib/explorer_web/channels/transaction_channel.ex @@ -0,0 +1,32 @@ +defmodule ExplorerWeb.TransactionChannel do + @moduledoc """ + Establishes pub/sub channel for transaction page live updates. + """ + use ExplorerWeb, :channel + + alias ExplorerWeb.TransactionView + alias Phoenix.View + + intercept(["confirmations"]) + + def join("transactions:" <> _transaction_hash, _params, socket) do + {:ok, %{}, socket} + end + + def handle_out("confirmations", %{max_block_number: max_block_number, transaction: transaction}, socket) do + Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale) + + rendered = + View.render_to_string( + TransactionView, + "_confirmations.html", + locale: socket.assigns.locale, + max_block_number: max_block_number, + transaction: transaction + ) + + push(socket, "confirmations", %{confirmations: rendered}) + + {:noreply, socket} + end +end diff --git a/apps/explorer_web/lib/explorer_web/channels/user_socket.ex b/apps/explorer_web/lib/explorer_web/channels/user_socket.ex index 1c76a2124c..68a13fbd38 100644 --- a/apps/explorer_web/lib/explorer_web/channels/user_socket.ex +++ b/apps/explorer_web/lib/explorer_web/channels/user_socket.ex @@ -2,6 +2,7 @@ defmodule ExplorerWeb.UserSocket do use Phoenix.Socket channel("addresses:*", ExplorerWeb.AddressChannel) + channel("transactions:*", ExplorerWeb.TransactionChannel) transport(:websocket, Phoenix.Transports.WebSocket, timeout: 45_000) # transport :longpoll, Phoenix.Transports.LongPoll diff --git a/apps/explorer_web/lib/explorer_web/templates/transaction/_confirmations.html.eex b/apps/explorer_web/lib/explorer_web/templates/transaction/_confirmations.html.eex new file mode 100644 index 0000000000..bb2e6d779f --- /dev/null +++ b/apps/explorer_web/lib/explorer_web/templates/transaction/_confirmations.html.eex @@ -0,0 +1 @@ +(<%= gettext "%{confirmations} block confirmations", confirmations: confirmations(@transaction, max_block_number: @max_block_number) %>) diff --git a/apps/explorer_web/lib/explorer_web/templates/transaction/overview.html.eex b/apps/explorer_web/lib/explorer_web/templates/transaction/overview.html.eex index 3005f7a1f8..7e6d4dffdb 100644 --- a/apps/explorer_web/lib/explorer_web/templates/transaction/overview.html.eex +++ b/apps/explorer_web/lib/explorer_web/templates/transaction/overview.html.eex @@ -41,7 +41,9 @@ to: block_path(@conn, :show, @conn.assigns.locale, block) ) %> <% end %> - (<%= gettext "%{confirmations} block confirmations", confirmations: confirmations(@transaction, max_block_number: @max_block_number) %>) + + <%= render "_confirmations.html", max_block_number: @max_block_number, transaction: @transaction %> + diff --git a/apps/explorer_web/test/explorer_web/channels/transaction_channel_test.exs b/apps/explorer_web/test/explorer_web/channels/transaction_channel_test.exs new file mode 100644 index 0000000000..82a24b9eec --- /dev/null +++ b/apps/explorer_web/test/explorer_web/channels/transaction_channel_test.exs @@ -0,0 +1,31 @@ +defmodule ExplorerWeb.AddressTransactionTest do + use ExplorerWeb.ChannelCase + + describe "transactions channel tests" do + test "subscribed user can receive block confirmations event" do + channel = "transactions" + @endpoint.subscribe(channel) + + block = insert(:block, number: 1) + + transaction = + :transaction + |> insert() + |> with_block(block) + + ExplorerWeb.Endpoint.broadcast(channel, "confirmations", %{max_block_number: 3, transaction: transaction}) + + receive do + %Phoenix.Socket.Broadcast{ + event: "confirmations", + topic: ^channel, + payload: %{max_block_number: 3, transaction: ^transaction} + } -> + assert true + after + 5_000 -> + assert false, "Expected message received nothing." + end + end + end +end diff --git a/apps/explorer_web/test/explorer_web/features/pages/transaction_page.ex b/apps/explorer_web/test/explorer_web/features/pages/transaction_page.ex index 7892e50a6e..5e365489f0 100644 --- a/apps/explorer_web/test/explorer_web/features/pages/transaction_page.ex +++ b/apps/explorer_web/test/explorer_web/features/pages/transaction_page.ex @@ -7,6 +7,10 @@ defmodule ExplorerWeb.TransactionPage do alias Explorer.Chain.{InternalTransaction, Transaction, Hash} + def block_confirmations() do + css("[data-selector='block_confirmations']") + end + def click_logs(session) do click(session, css("[data-test='transaction_logs_link']")) 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 72111249bd..26c7b55ff5 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 @@ -160,5 +160,17 @@ defmodule ExplorerWeb.ViewingTransactionsTest do |> TransactionLogsPage.click_address(lincoln) |> assert_has(AddressPage.detail_hash(lincoln)) end + + test "block confirmations via live update", %{session: session, transaction: transaction} do + session + |> TransactionPage.visit_page(transaction) + + ExplorerWeb.Endpoint.broadcast!("transactions:#{transaction.hash}", "confirmations", %{ + max_block_number: transaction.block_number + 3, + transaction: transaction + }) + + assert_text(session, TransactionPage.block_confirmations(), "(3 block confirmations)") + end end end