Merge pull request #497 from poanetwork/live-reload-transactions-on-homepage-#35
Live reload transactions on homepage #35pull/508/head
commit
4ce9115f9a
@ -1,26 +1,134 @@ |
|||||||
import { reducer, initialState } from '../../js/pages/chain' |
import { reducer, initialState } from '../../js/pages/chain' |
||||||
|
|
||||||
test('CHANNEL_DISCONNECTED', () => { |
test('RECEIVED_NEW_BLOCK', () => { |
||||||
|
const state = Object.assign({}, initialState, { |
||||||
|
newBlock: 'last new block' |
||||||
|
}) |
||||||
|
const action = { |
||||||
|
type: 'RECEIVED_NEW_BLOCK', |
||||||
|
msg: { |
||||||
|
homepageBlockHtml: 'new block' |
||||||
|
} |
||||||
|
} |
||||||
|
const output = reducer(state, action) |
||||||
|
|
||||||
|
expect(output.newBlock).toEqual('new block') |
||||||
|
}) |
||||||
|
|
||||||
|
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { |
||||||
|
test('single transaction', () => { |
||||||
const state = initialState |
const state = initialState |
||||||
const action = { |
const action = { |
||||||
type: 'CHANNEL_DISCONNECTED' |
type: 'RECEIVED_NEW_TRANSACTION_BATCH', |
||||||
|
msgs: [{ |
||||||
|
transactionHtml: 'test' |
||||||
|
}] |
||||||
} |
} |
||||||
const output = reducer(state, action) |
const output = reducer(state, action) |
||||||
|
|
||||||
expect(output.channelDisconnected).toBe(true) |
expect(output.newTransactions).toEqual(['test']) |
||||||
|
expect(output.batchCountAccumulator).toEqual(0) |
||||||
|
expect(output.transactionCount).toEqual(1) |
||||||
}) |
}) |
||||||
|
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) |
||||||
|
|
||||||
test('RECEIVED_NEW_BLOCK', () => { |
expect(output.newTransactions).toEqual([]) |
||||||
|
expect(output.batchCountAccumulator).toEqual(11) |
||||||
|
expect(output.transactionCount).toEqual(11) |
||||||
|
}) |
||||||
|
test('single transaction after single transaction', () => { |
||||||
const state = Object.assign({}, initialState, { |
const state = Object.assign({}, initialState, { |
||||||
newBlock: 'last new block' |
newTransactions: ['test 1'] |
||||||
}) |
}) |
||||||
const action = { |
const action = { |
||||||
type: 'RECEIVED_NEW_BLOCK', |
type: 'RECEIVED_NEW_TRANSACTION_BATCH', |
||||||
msg: { |
msgs: [{ |
||||||
homepageBlockHtml: 'new block' |
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) |
const output = reducer(state, action) |
||||||
|
|
||||||
expect(output.newBlock).toEqual('new block') |
expect(output.newTransactions).toEqual([]) |
||||||
|
expect(output.batchCountAccumulator).toEqual(22) |
||||||
|
}) |
||||||
}) |
}) |
||||||
|
@ -0,0 +1,60 @@ |
|||||||
|
import $ from 'jquery' |
||||||
|
import humps from 'humps' |
||||||
|
import socket from '../socket' |
||||||
|
import router from '../router' |
||||||
|
import { updateAllAges } from '../lib/from_now' |
||||||
|
import { initRedux } from '../utils' |
||||||
|
|
||||||
|
export const initialState = { |
||||||
|
beyondPageOne: null, |
||||||
|
channelDisconnected: false, |
||||||
|
newBlock: null |
||||||
|
} |
||||||
|
|
||||||
|
export function reducer (state = initialState, action) { |
||||||
|
switch (action.type) { |
||||||
|
case 'PAGE_LOAD': { |
||||||
|
return Object.assign({}, state, { |
||||||
|
beyondPageOne: !!action.blockNumber |
||||||
|
}) |
||||||
|
} |
||||||
|
case 'CHANNEL_DISCONNECTED': { |
||||||
|
if (state.beyondPageOne) return state |
||||||
|
|
||||||
|
return Object.assign({}, state, { |
||||||
|
channelDisconnected: true |
||||||
|
}) |
||||||
|
} |
||||||
|
case 'RECEIVED_NEW_BLOCK': { |
||||||
|
if (state.channelDisconnected || state.beyondPageOne) return state |
||||||
|
|
||||||
|
return Object.assign({}, state, { |
||||||
|
newBlock: action.msg.blockHtml |
||||||
|
}) |
||||||
|
} |
||||||
|
default: |
||||||
|
return state |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
router.when('/blocks', { exactPathMatch: true }).then(({ blockNumber }) => initRedux(reducer, { |
||||||
|
main (store) { |
||||||
|
const blocksChannel = socket.channel(`blocks:new_block`, {}) |
||||||
|
store.dispatch({ type: 'PAGE_LOAD', blockNumber }) |
||||||
|
blocksChannel.join() |
||||||
|
blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) |
||||||
|
blocksChannel.on('new_block', (msg) => |
||||||
|
store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) }) |
||||||
|
) |
||||||
|
}, |
||||||
|
render (state, oldState) { |
||||||
|
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') |
||||||
|
const $blocksList = $('[data-selector="blocks-list"]') |
||||||
|
|
||||||
|
if (state.channelDisconnected) $channelDisconnected.show() |
||||||
|
if (oldState.newBlock !== state.newBlock) { |
||||||
|
$blocksList.prepend(state.newBlock) |
||||||
|
updateAllAges() |
||||||
|
} |
||||||
|
} |
||||||
|
})) |
@ -1,46 +1,99 @@ |
|||||||
import $ from 'jquery' |
import $ from 'jquery' |
||||||
import humps from 'humps' |
import humps from 'humps' |
||||||
|
import numeral from 'numeral' |
||||||
|
import 'numeral/locales' |
||||||
import router from '../router' |
import router from '../router' |
||||||
import socket from '../socket' |
import socket from '../socket' |
||||||
import { updateAllAges } from '../lib/from_now' |
import { updateAllAges } from '../lib/from_now' |
||||||
import { initRedux } from '../utils' |
import { batchChannel, initRedux } from '../utils' |
||||||
|
|
||||||
|
const BATCH_THRESHOLD = 10 |
||||||
|
|
||||||
export const initialState = { |
export const initialState = { |
||||||
|
batchCountAccumulator: 0, |
||||||
newBlock: null, |
newBlock: null, |
||||||
channelDisconnected: false |
newTransactions: [], |
||||||
|
transactionCount: null |
||||||
} |
} |
||||||
|
|
||||||
export function reducer (state = initialState, action) { |
export function reducer (state = initialState, action) { |
||||||
switch (action.type) { |
switch (action.type) { |
||||||
case 'CHANNEL_DISCONNECTED': { |
case 'PAGE_LOAD': { |
||||||
return Object.assign({}, state, { |
return Object.assign({}, state, { |
||||||
channelDisconnected: true |
transactionCount: numeral(action.transactionCount).value() |
||||||
}) |
}) |
||||||
} |
} |
||||||
case 'RECEIVED_NEW_BLOCK': { |
case 'RECEIVED_NEW_BLOCK': { |
||||||
return Object.assign({}, state, { |
return Object.assign({}, state, { |
||||||
newBlock: humps.camelizeKeys(action.msg).homepageBlockHtml |
newBlock: action.msg.homepageBlockHtml |
||||||
|
}) |
||||||
|
} |
||||||
|
case 'RECEIVED_NEW_TRANSACTION_BATCH': { |
||||||
|
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) { |
||||||
|
return Object.assign({}, state, { |
||||||
|
newTransactions: [ |
||||||
|
...state.newTransactions, |
||||||
|
...action.msgs.map(({transactionHtml}) => transactionHtml) |
||||||
|
], |
||||||
|
transactionCount: state.transactionCount + action.msgs.length |
||||||
}) |
}) |
||||||
|
} else { |
||||||
|
return Object.assign({}, state, { |
||||||
|
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, |
||||||
|
transactionCount: state.transactionCount + action.msgs.length |
||||||
|
}) |
||||||
|
} |
||||||
} |
} |
||||||
default: |
default: |
||||||
return state |
return state |
||||||
} |
} |
||||||
} |
} |
||||||
|
|
||||||
router.when('', { exactPathMatch: true }).then(() => initRedux(reducer, { |
router.when('', { exactPathMatch: true }).then(({ locale }) => initRedux(reducer, { |
||||||
main (store) { |
main (store) { |
||||||
const blocksChannel = socket.channel(`blocks:new_block`) |
const blocksChannel = socket.channel(`blocks:new_block`) |
||||||
|
numeral.locale(locale) |
||||||
|
store.dispatch({ |
||||||
|
type: 'PAGE_LOAD', |
||||||
|
transactionCount: $('[data-selector="transaction-count"]').text() |
||||||
|
}) |
||||||
blocksChannel.join() |
blocksChannel.join() |
||||||
blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) |
blocksChannel.on('new_block', msg => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) |
||||||
blocksChannel.on('new_block', msg => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg })) |
|
||||||
|
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) { |
render (state, oldState) { |
||||||
const $blockList = $('[data-selector="chain-block-list"]') |
const $blockList = $('[data-selector="chain-block-list"]') |
||||||
|
const $channelBatching = $('[data-selector="channel-batching-message"]') |
||||||
|
const $channelBatchingCount = $('[data-selector="channel-batching-count"]') |
||||||
|
const $transactionsList = $('[data-selector="transactions-list"]') |
||||||
|
const $transactionCount = $('[data-selector="transaction-count"]') |
||||||
|
|
||||||
if (oldState.newBlock !== state.newBlock) { |
if (oldState.newBlock !== state.newBlock) { |
||||||
$blockList.children().last().remove() |
$blockList.children().last().remove() |
||||||
$blockList.prepend(state.newBlock) |
$blockList.prepend(state.newBlock) |
||||||
updateAllAges() |
updateAllAges() |
||||||
} |
} |
||||||
|
if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) |
||||||
|
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() |
||||||
|
} |
||||||
} |
} |
||||||
})) |
})) |
||||||
|
@ -1,10 +1,33 @@ |
|||||||
defmodule ExplorerWeb.TransactionChannel do |
defmodule ExplorerWeb.TransactionChannel do |
||||||
@moduledoc """ |
@moduledoc """ |
||||||
Establishes pub/sub channel for transaction page live updates. |
Establishes pub/sub channel for live updates of transaction events. |
||||||
""" |
""" |
||||||
use ExplorerWeb, :channel |
use ExplorerWeb, :channel |
||||||
|
|
||||||
def join("transactions:confirmations", _params, socket) do |
alias ExplorerWeb.TransactionView |
||||||
|
alias Phoenix.View |
||||||
|
|
||||||
|
intercept(["new_transaction"]) |
||||||
|
|
||||||
|
def join("transactions:new_transaction", _params, socket) do |
||||||
{:ok, %{}, socket} |
{:ok, %{}, socket} |
||||||
end |
end |
||||||
|
|
||||||
|
def handle_out("new_transaction", %{transaction: transaction}, socket) do |
||||||
|
Gettext.put_locale(ExplorerWeb.Gettext, socket.assigns.locale) |
||||||
|
|
||||||
|
rendered_transaction = |
||||||
|
View.render_to_string( |
||||||
|
TransactionView, |
||||||
|
"_tile.html", |
||||||
|
locale: socket.assigns.locale, |
||||||
|
transaction: transaction |
||||||
|
) |
||||||
|
|
||||||
|
push(socket, "new_transaction", %{ |
||||||
|
transaction_html: rendered_transaction |
||||||
|
}) |
||||||
|
|
||||||
|
{:noreply, socket} |
||||||
|
end |
||||||
end |
end |
||||||
|
@ -0,0 +1,48 @@ |
|||||||
|
<div class="tile fade-up"> |
||||||
|
<div class="row"> |
||||||
|
<div class="col-md-6"> |
||||||
|
<!-- block height --> |
||||||
|
<%= link( |
||||||
|
@block, |
||||||
|
class: "tile-title", |
||||||
|
to: block_path(ExplorerWeb.Endpoint, :show, @locale, @block), |
||||||
|
"data-test": "block_number", |
||||||
|
"data-block-number": to_string(@block.number) |
||||||
|
) %> |
||||||
|
<div> |
||||||
|
<!-- transactions --> |
||||||
|
<span class="mr-2"> |
||||||
|
<%= ngettext("%{count} transaction", "%{count} transactions", Enum.count(@block.transactions)) %> |
||||||
|
</span> |
||||||
|
<!-- size --> |
||||||
|
<span class="mr-2"> <%= Cldr.Unit.new(:byte, @block.size) |> Cldr.Unit.to_string! %> </span> |
||||||
|
<!-- age --> |
||||||
|
<span data-from-now="<%= @block.timestamp %>"></span> |
||||||
|
</div> |
||||||
|
<div class=""> |
||||||
|
<!-- validator --> |
||||||
|
<%= gettext "Miner" %> |
||||||
|
<span class="ml-2"> |
||||||
|
<%= link to: address_path(ExplorerWeb.Endpoint, :show, @locale, @block.miner_hash) do %> |
||||||
|
<%= @block.miner_hash %> |
||||||
|
<% end %> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="col-md-6 text-right d-flex flex-column align-items-end justify-content-end"> |
||||||
|
<!-- Gas Used --> |
||||||
|
<div class=""> |
||||||
|
<%= formatted_gas(@block.gas_used) %> |
||||||
|
(<%= formatted_gas(@block.gas_used / @block.gas_limit, format: "#.#%") %>) |
||||||
|
<%= gettext "Gas Used" %> |
||||||
|
</div> |
||||||
|
<div class="progress w-25"> |
||||||
|
<div class="progress-bar" role="progressbar" style="width: <%= formatted_gas(@block.gas_used / @block.gas_limit, format: "#.#%") %>;" aria-valuenow="50" aria-valuemin="0" aria-valuemax="100"> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<!-- Gas Limit --> |
||||||
|
<span> <%= formatted_gas(@block.gas_limit) %> <%= gettext "Gas Limit" %> </span> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
@ -1,42 +0,0 @@ |
|||||||
<div class="card"> |
|
||||||
<div class="card-body"> |
|
||||||
<%= link(gettext("View All Transactions →"), to: transaction_path(@conn, :index, Gettext.get_locale), class: "button button--secondary button--xsmall float-right") %> |
|
||||||
<h2 class="card-title"><%= gettext "Transactions" %></h2> |
|
||||||
<%= for transaction <- @chain.transactions do %> |
|
||||||
<div class="tile tile-type-<%= ExplorerWeb.TransactionView.type_suffix(transaction) %> tile-status--<%= ExplorerWeb.TransactionView.status(transaction) %>" data-test="<%= ExplorerWeb.TransactionView.type_suffix(transaction) %>" data-transaction-hash="<%= transaction.hash %>"> |
|
||||||
<div class="row" data-test="chain_transaction"> |
|
||||||
<div class="col-md-2 d-flex flex-column align-items-left justify-content-start justify-content-lg-center"> |
|
||||||
<div class="ml-4"> |
|
||||||
<span class="tile-label" data-test="transaction_type"> <%= ExplorerWeb.TransactionView.transaction_display_type(transaction) %></span> |
|
||||||
<div class="tile-status-label" data-test="transaction_status"><%= ExplorerWeb.TransactionView.formatted_status(transaction) %></div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<div class="col-md-7 col-lg-8 d-flex flex-column"> |
|
||||||
<%= render ExplorerWeb.TransactionView, "_link.html", locale: @locale, transaction_hash: transaction.hash %> |
|
||||||
<span class="text-nowrap"> |
|
||||||
<%= 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 %> |
|
||||||
</span> |
|
||||||
<span> |
|
||||||
<span class="ml-1" data-from-now="<%= transaction.block.timestamp %>"></span> |
|
||||||
<span class="ml-1"> |
|
||||||
<%= link( |
|
||||||
gettext("Block #%{number}", number: to_string(transaction.block.number)), |
|
||||||
to: block_path(@conn, :show, @conn.assigns.locale, transaction.block) |
|
||||||
) %> |
|
||||||
</span> |
|
||||||
</span> |
|
||||||
</div> |
|
||||||
<div class="col-md-3 col-lg-2 d-flex flex-row flex-md-column justify-content-start text-md-right"> |
|
||||||
<span class="tile-title"> |
|
||||||
<%= ExplorerWeb.TransactionView.value(transaction, include_label: false) %> <%= gettext "Ether" %> |
|
||||||
</span> |
|
||||||
<span><%= ExplorerWeb.TransactionView.formatted_fee(transaction, denomination: :ether) %> <%= gettext "Fee" %></span> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
</div> |
|
||||||
<% end %> |
|
||||||
|
|
||||||
</div> |
|
||||||
</div> |
|
Loading…
Reference in new issue