pubsub for new token transfers

pull/3216/head
Victor Baranov 4 years ago
parent b20995a5e4
commit 7227fc971a
  1. 1
      CHANGELOG.md
  2. 87
      apps/block_scout_web/assets/js/pages/address/token_transfers.js
  3. 80
      apps/block_scout_web/assets/js/pages/tokens.js
  4. 2
      apps/block_scout_web/assets/webpack.config.js
  5. 52
      apps/block_scout_web/lib/block_scout_web/channels/address_channel.ex
  6. 45
      apps/block_scout_web/lib/block_scout_web/channels/token_transfer_channel.ex
  7. 1
      apps/block_scout_web/lib/block_scout_web/channels/user_socket.ex
  8. 47
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  9. 4
      apps/block_scout_web/lib/block_scout_web/templates/address_token_transfer/index.html.eex
  10. 3
      apps/block_scout_web/lib/block_scout_web/templates/tokens/transfer/index.html.eex

@ -1,6 +1,7 @@
## Current ## Current
### Features ### Features
- [#3216](https://github.com/poanetwork/blockscout/pull/3216) - Display new token transfers at token page without refreshing the page
- [#3199](https://github.com/poanetwork/blockscout/pull/3199) - Show compilation error at contract verification - [#3199](https://github.com/poanetwork/blockscout/pull/3199) - Show compilation error at contract verification
- [#3193](https://github.com/poanetwork/blockscout/pull/3193) - Raw trace copy button - [#3193](https://github.com/poanetwork/blockscout/pull/3193) - Raw trace copy button
- [#3184](https://github.com/poanetwork/blockscout/pull/3184) - Apps navbar menu item - [#3184](https://github.com/poanetwork/blockscout/pull/3184) - Apps navbar menu item

@ -0,0 +1,87 @@
import $ from 'jquery'
import omit from 'lodash/omit'
import URI from 'urijs'
import humps from 'humps'
import { subscribeChannel } from '../../socket'
import { connectElements } from '../../lib/redux_helpers.js'
import { createAsyncLoadStore } from '../../lib/async_listing_load'
import '../address'
export const initialState = {
addressHash: null,
channelDisconnected: false,
filter: null
}
export function reducer (state, action) {
switch (action.type) {
case 'PAGE_LOAD':
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
if (state.beyondPageOne) return state
return Object.assign({}, state, { channelDisconnected: true })
}
case 'RECEIVED_NEW_TOKEN_TRANSFER': {
if (state.channelDisconnected) return state
if (state.beyondPageOne ||
(state.filter === 'to' && action.msg.toAddressHash !== state.addressHash) ||
(state.filter === 'from' && action.msg.fromAddressHash !== state.addressHash)) {
return state
}
return Object.assign({}, state, { items: [action.msg.tokenTransferHtml, ...state.items] })
}
case 'RECEIVED_NEW_REWARD': {
if (state.channelDisconnected) return state
return Object.assign({}, state, { items: [action.msg.rewardHtml, ...state.items] })
}
default:
return state
}
}
const elements = {
'[data-selector="channel-disconnected-message"]': {
render ($el, state) {
if (state.channelDisconnected) $el.show()
}
}
}
if ($('[data-page="address-token-transfers"]').length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash')
const addressHash = $('[data-page="address-details"]')[0].dataset.pageAddressHash
const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true))
connectElements({ store, elements })
store.dispatch({
type: 'PAGE_LOAD',
addressHash,
filter,
beyondPageOne: !!blockNumber
})
const addressChannel = subscribeChannel(`addresses:${addressHash}`)
addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
addressChannel.on('token_transfer', (msg) => {
store.dispatch({
type: 'RECEIVED_NEW_TOKEN_TRANSFER',
msg: humps.camelizeKeys(msg)
})
})
const rewardsChannel = subscribeChannel(`rewards:${addressHash}`)
rewardsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
rewardsChannel.on('new_reward', (msg) => {
store.dispatch({
type: 'RECEIVED_NEW_REWARD',
msg: humps.camelizeKeys(msg)
})
})
}

@ -0,0 +1,80 @@
import $ from 'jquery'
import omit from 'lodash/omit'
import humps from 'humps'
import socket from '../socket'
import { createAsyncLoadStore } from '../lib/async_listing_load'
import { batchChannel } from '../lib/utils'
import '../app'
const BATCH_THRESHOLD = 10
export const initialState = {
channelDisconnected: false,
tokenTransferCount: null,
tokenTransfersBatch: []
}
export function reducer (state = initialState, action) {
switch (action.type) {
case 'ELEMENTS_LOAD': {
return Object.assign({}, state, omit(action, 'type'))
}
case 'CHANNEL_DISCONNECTED': {
return Object.assign({}, state, {
channelDisconnected: true,
tokenTransfersBatch: []
})
}
case 'RECEIVED_NEW_TOKEN_TRANSFER_BATCH': {
if (state.channelDisconnected) return state
const tokenTransferCount = state.tokenTransferCount + action.msgs.length
const tokenTransfersLength = state.items.length + action.msgs.length
if (tokenTransfersLength < BATCH_THRESHOLD) {
return Object.assign({}, state, {
items: [
...action.msgs.map(msg => msg.tokenTransferHtml).reverse(),
...state.items
],
tokenTransferCount
})
} else if (!state.tokenTransfersBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
items: [
...action.msgs.map(msg => msg.tokenTransferHtml).reverse(),
...state.items.slice(0, -1 * action.msgs.length)
],
tokenTransferCount
})
} else {
return Object.assign({}, state, {
tokenTransfersBatch: [
...action.msgs.reverse(),
...state.tokenTransfersBatch
],
tokenTransferCount
})
}
}
default:
return state
}
}
const $tokenTransferListPage = $('[data-page="token-transfer-list"]')
if ($tokenTransferListPage.length) {
const store = createAsyncLoadStore(reducer, initialState, 'dataset.identifierHash')
const tokenTransfersChannel = socket.channel('token_transfers:new_token_transfer')
tokenTransfersChannel.join()
tokenTransfersChannel.onError(() => store.dispatch({
type: 'CHANNEL_DISCONNECTED'
}))
tokenTransfersChannel.on('token_transfer', batchChannel((msgs) => {
store.dispatch({
type: 'RECEIVED_NEW_TOKEN_TRANSFER_BATCH',
msgs: humps.camelizeKeys(msgs)
})
}))
}

@ -78,6 +78,7 @@ const appJs =
'blocks': './js/pages/blocks.js', 'blocks': './js/pages/blocks.js',
'address': './js/pages/address.js', 'address': './js/pages/address.js',
'address-transactions': './js/pages/address/transactions.js', 'address-transactions': './js/pages/address/transactions.js',
'address-token-transfers': './js/pages/address/token_transfers.js',
'address-coin-balances': './js/pages/address/coin_balances.js', 'address-coin-balances': './js/pages/address/coin_balances.js',
'address-internal-transactions': './js/pages/address/internal_transactions.js', 'address-internal-transactions': './js/pages/address/internal_transactions.js',
'address-logs': './js/pages/address/logs.js', 'address-logs': './js/pages/address/logs.js',
@ -87,6 +88,7 @@ const appJs =
'transaction': './js/pages/transaction.js', 'transaction': './js/pages/transaction.js',
'verification-form': './js/pages/verification_form.js', 'verification-form': './js/pages/verification_form.js',
'token-counters': './js/pages/token_counters.js', 'token-counters': './js/pages/token_counters.js',
'tokens': './js/pages/tokens.js',
'admin-tasks': './js/pages/admin/tasks.js', 'admin-tasks': './js/pages/admin/tasks.js',
'read-token-contract': './js/pages/read_token_contract.js', 'read-token-contract': './js/pages/read_token_contract.js',
'smart-contract-helpers': './js/lib/smart_contract/index.js', 'smart-contract-helpers': './js/lib/smart_contract/index.js',

@ -4,14 +4,28 @@ defmodule BlockScoutWeb.AddressChannel do
""" """
use BlockScoutWeb, :channel use BlockScoutWeb, :channel
alias BlockScoutWeb.{AddressCoinBalanceView, AddressView, InternalTransactionView, TransactionView} alias BlockScoutWeb.{
alias Explorer.{Chain, Market} AddressCoinBalanceView,
alias Explorer.Chain.Hash AddressView,
InternalTransactionView,
TransactionView
}
alias Explorer.{Chain, Market, Repo}
alias Explorer.Chain.{Hash, Transaction}
alias Explorer.Chain.Hash.Address, as: AddressHash alias Explorer.Chain.Hash.Address, as: AddressHash
alias Explorer.ExchangeRates.Token alias Explorer.ExchangeRates.Token
alias Phoenix.View alias Phoenix.View
intercept(["balance_update", "coin_balance", "count", "internal_transaction", "transaction", "verification_result"]) intercept([
"balance_update",
"coin_balance",
"count",
"internal_transaction",
"transaction",
"verification_result",
"token_transfer"
])
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000") {:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash @burn_address_hash burn_address_hash
@ -103,6 +117,8 @@ defmodule BlockScoutWeb.AddressChannel do
def handle_out("transaction", data, socket), do: handle_transaction(data, socket, "transaction") def handle_out("transaction", data, socket), do: handle_transaction(data, socket, "transaction")
def handle_out("token_transfer", data, socket), do: handle_token_transfer(data, socket, "token_transfer")
def handle_out("coin_balance", %{block_number: block_number}, socket) do def handle_out("coin_balance", %{block_number: block_number}, socket) do
coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number) coin_balance = Chain.get_coin_balance(socket.assigns.address_hash, block_number)
@ -146,6 +162,34 @@ defmodule BlockScoutWeb.AddressChannel do
{:noreply, socket} {:noreply, socket}
end end
def handle_token_transfer(%{address: address, token_transfer: token_transfer}, socket, event) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
transaction =
Transaction
|> Repo.get_by(hash: token_transfer.transaction_hash)
|> Repo.preload([:from_address, :to_address, :block, token_transfers: [:from_address, :to_address, :token]])
rendered =
View.render_to_string(
TransactionView,
"_tile.html",
current_address: address,
transaction: transaction,
burn_address_hash: @burn_address_hash,
conn: socket
)
push(socket, event, %{
to_address_hash: to_string(token_transfer.to_address_hash),
from_address_hash: to_string(token_transfer.from_address_hash),
token_transfer_hash: Hash.to_string(token_transfer.transaction_hash),
token_transfer_html: rendered
})
{:noreply, socket}
end
defp render_balance_card(address, exchange_rate, locale) do defp render_balance_card(address, exchange_rate, locale) do
Gettext.put_locale(BlockScoutWeb.Gettext, locale) Gettext.put_locale(BlockScoutWeb.Gettext, locale)

@ -0,0 +1,45 @@
defmodule BlockScoutWeb.TokenTransferChannel do
@moduledoc """
Establishes pub/sub channel for live updates of token transfer events.
"""
use BlockScoutWeb, :channel
alias BlockScoutWeb.Tokens.TransferView
alias Explorer.Chain
alias Explorer.Chain.Hash
alias Phoenix.View
intercept(["token_transfer"])
{:ok, burn_address_hash} = Chain.string_to_address_hash("0x0000000000000000000000000000000000000000")
@burn_address_hash burn_address_hash
def join("token_transfers:new_token_transfer", _params, socket) do
{:ok, %{}, socket}
end
def join("token_transfers:" <> _transaction_hash, _params, socket) do
{:ok, %{}, socket}
end
def handle_out("token_transfer", %{token_transfer: token_transfer}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
rendered_token_transfer =
View.render_to_string(
TransferView,
"_token_transfer.html",
conn: socket,
token: token_transfer.token,
token_transfer: token_transfer,
burn_address_hash: @burn_address_hash
)
push(socket, "token_transfer", %{
token_transfer_hash: Hash.to_string(token_transfer.transaction_hash),
token_transfer_html: rendered_token_transfer
})
{:noreply, socket}
end
end

@ -7,6 +7,7 @@ defmodule BlockScoutWeb.UserSocket do
channel("exchange_rate:*", BlockScoutWeb.ExchangeRateChannel) channel("exchange_rate:*", BlockScoutWeb.ExchangeRateChannel)
channel("rewards:*", BlockScoutWeb.RewardChannel) channel("rewards:*", BlockScoutWeb.RewardChannel)
channel("transactions:*", BlockScoutWeb.TransactionChannel) channel("transactions:*", BlockScoutWeb.TransactionChannel)
channel("token_transfers:*", BlockScoutWeb.TokenTransferChannel)
def connect(%{"locale" => locale}, socket) do def connect(%{"locale" => locale}, socket) do
{:ok, assign(socket, :locale, locale)} {:ok, assign(socket, :locale, locale)}

@ -6,7 +6,7 @@ defmodule BlockScoutWeb.Notifier do
alias Absinthe.Subscription alias Absinthe.Subscription
alias BlockScoutWeb.{AddressContractVerificationView, Endpoint} alias BlockScoutWeb.{AddressContractVerificationView, Endpoint}
alias Explorer.{Chain, Market, Repo} alias Explorer.{Chain, Market, Repo}
alias Explorer.Chain.{Address, InternalTransaction, Transaction} alias Explorer.Chain.{Address, InternalTransaction, TokenTransfer, Transaction}
alias Explorer.Chain.Supply.RSK alias Explorer.Chain.Supply.RSK
alias Explorer.Chain.Transaction.History.TransactionStats alias Explorer.Chain.Transaction.History.TransactionStats
alias Explorer.Counters.AverageBlockTime alias Explorer.Counters.AverageBlockTime
@ -112,6 +112,20 @@ defmodule BlockScoutWeb.Notifier do
token_transfers, token_transfers,
token_transfers: token_contract_address_hash token_transfers: token_contract_address_hash
) )
token_transfers_full =
token_transfers
|> Stream.map(
&(TokenTransfer
|> Repo.get_by(
transaction_hash: &1.transaction_hash,
token_contract_address_hash: &1.token_contract_address_hash
)
|> Repo.preload([:from_address, :to_address, :token, transaction: :block]))
)
token_transfers_full
|> Enum.each(&broadcast_token_transfer/1)
end end
end end
@ -123,10 +137,13 @@ defmodule BlockScoutWeb.Notifier do
:block => :optional, :block => :optional,
[created_contract_address: :names] => :optional, [created_contract_address: :names] => :optional,
[from_address: :names] => :optional, [from_address: :names] => :optional,
[to_address: :names] => :optional, [to_address: :names] => :optional
:token_transfers => :optional
} }
) )
|> Enum.map(fn tx ->
# Disable parsing of token transfers from websocket for transaction tab because we display token transfers at a separate tab
Map.put(tx, :token_transfers, [])
end)
|> Enum.each(&broadcast_transaction/1) |> Enum.each(&broadcast_transaction/1)
end end
@ -248,4 +265,28 @@ defmodule BlockScoutWeb.Notifier do
}) })
end end
end end
defp broadcast_token_transfer(token_transfer) do
broadcast_token_transfer(token_transfer, "token_transfers:new_token_transfer", "token_transfer")
end
defp broadcast_token_transfer(token_transfer, token_transfer_channel, event) do
Endpoint.broadcast("token_transfers:#{token_transfer.transaction_hash}", event, %{})
Endpoint.broadcast(token_transfer_channel, event, %{
token_transfer: token_transfer
})
Endpoint.broadcast("addresses:#{token_transfer.from_address_hash}", event, %{
address: token_transfer.from_address,
token_transfer: token_transfer
})
if token_transfer.to_address_hash != token_transfer.from_address_hash do
Endpoint.broadcast("addresses:#{token_transfer.to_address_hash}", event, %{
address: token_transfer.to_address,
token_transfer: token_transfer
})
end
end
end end

@ -1,7 +1,7 @@
<section class="container"> <section class="container">
<%= render BlockScoutWeb.AddressView, "overview.html", assigns %> <%= render BlockScoutWeb.AddressView, "overview.html", assigns %>
<section> <section data-page="address-token-transfers">
<div class="card"> <div class="card">
<%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %> <%= render BlockScoutWeb.AddressView, "_tabs.html", assigns %>
<div data-async-load data-async-listing="<%= @current_path %>" class="card-body"> <div data-async-load data-async-listing="<%= @current_path %>" class="card-body">
@ -74,6 +74,6 @@
</div> </div>
</div> </div>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/address.js") %>"></script> <script defer data-cfasync="false" src="<%= static_path(@conn, "/js/address-token-transfers.js") %>"></script>
</section> </section>
</section> </section>

@ -7,7 +7,7 @@
conn: @conn conn: @conn
) %> ) %>
<section> <section data-page="token-transfer-list">
<div class="card"> <div class="card">
<%= render OverviewView, "_tabs.html", assigns %> <%= render OverviewView, "_tabs.html", assigns %>
<div class="card-body" data-async-load data-async-listing="<%= @current_path %>"> <div class="card-body" data-async-load data-async-listing="<%= @current_path %>">
@ -35,4 +35,5 @@
</div> </div>
</section> </section>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/token-counters.js") %>"></script> <script defer data-cfasync="false" src="<%= static_path(@conn, "/js/token-counters.js") %>"></script>
<script defer data-cfasync="false" src="<%= static_path(@conn, "/js/tokens.js") %>"></script>
</section> </section>

Loading…
Cancel
Save