Merge branch 'master' into frg-fix-token-balance-log-error

pull/815/head
Andrew Cravenho 6 years ago committed by GitHub
commit 3bcaec6850
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 2
      .tool-versions
  2. 229
      apps/block_scout_web/assets/__tests__/pages/transaction.js
  3. 18
      apps/block_scout_web/assets/css/components/_animations.scss
  4. 11
      apps/block_scout_web/assets/css/components/_dropdown.scss
  5. 1
      apps/block_scout_web/assets/js/app.js
  6. 51
      apps/block_scout_web/assets/js/lib/token_balance_dropdown_search.js
  7. 4
      apps/block_scout_web/assets/js/pages/chain.js
  8. 138
      apps/block_scout_web/assets/js/pages/transaction.js
  9. 14
      apps/block_scout_web/assets/js/utils.js
  10. 6
      apps/block_scout_web/assets/package-lock.json
  11. 2
      apps/block_scout_web/assets/package.json
  12. BIN
      apps/block_scout_web/assets/static/android-chrome-192x192.png
  13. BIN
      apps/block_scout_web/assets/static/android-chrome-512x512.png
  14. BIN
      apps/block_scout_web/assets/static/apple-touch-icon.png
  15. 9
      apps/block_scout_web/assets/static/browserconfig.xml
  16. BIN
      apps/block_scout_web/assets/static/favicon-16x16.png
  17. BIN
      apps/block_scout_web/assets/static/favicon-32x32.png
  18. BIN
      apps/block_scout_web/assets/static/favicon.ico
  19. BIN
      apps/block_scout_web/assets/static/mstile-150x150.png
  20. 39
      apps/block_scout_web/assets/static/safari-pinned-tab.svg
  21. 19
      apps/block_scout_web/assets/static/site.webmanifest
  22. 18
      apps/block_scout_web/lib/block_scout_web/chain.ex
  23. 26
      apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex
  24. 4
      apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex
  25. 36
      apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex
  26. 1
      apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex
  27. 1
      apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex
  28. 10
      apps/block_scout_web/lib/block_scout_web/notifier.ex
  29. 2
      apps/block_scout_web/lib/block_scout_web/router.ex
  30. 2
      apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex
  31. 32
      apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex
  32. 16
      apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex
  33. 10
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex
  34. 40
      apps/block_scout_web/lib/block_scout_web/templates/address_token_balance/_tokens.html.eex
  35. 4
      apps/block_scout_web/lib/block_scout_web/templates/block_transaction/index.html.eex
  36. 10
      apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex
  37. 50
      apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex
  38. 24
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex
  39. 6
      apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex
  40. 6
      apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex
  41. 5
      apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex
  42. 4
      apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex
  43. 6
      apps/block_scout_web/lib/phoenix/param.ex
  44. 81
      apps/block_scout_web/priv/gettext/default.pot
  45. 81
      apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po
  46. 17
      apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs
  47. 8
      apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs
  48. 44
      apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs
  49. 20
      apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs
  50. 56
      apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs
  51. 12
      apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs
  52. 28
      apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex
  53. 78
      apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs
  54. 14
      apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs
  55. 38
      apps/block_scout_web/test/block_scout_web/views/error_helpers_test.exs
  56. 6
      apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs
  57. 2
      apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs
  58. 17
      apps/block_scout_web/test/phoenix/param/explorer/chain/block_test.exs
  59. 22
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex
  60. 80
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex
  61. 78
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex
  62. 1
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex
  63. 39
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncle.ex
  64. 35
      apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncles.ex
  65. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncle_test.exs
  66. 5
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncles_test.exs
  67. 6
      apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs
  68. 79
      apps/explorer/lib/explorer/chain.ex
  69. 17
      apps/explorer/lib/explorer/chain/block.ex
  70. 64
      apps/explorer/lib/explorer/chain/block/second_degree_relation.ex
  71. 193
      apps/explorer/lib/explorer/chain/import.ex
  72. 27
      apps/explorer/lib/explorer/chain/transaction.ex
  73. 63
      apps/explorer/lib/explorer/chain/transaction/fork.ex
  74. 12
      apps/explorer/lib/explorer/smart_contract/reader.ex
  75. 5
      apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs
  76. 22
      apps/explorer/priv/repo/migrations/20180917182319_create_block_second_degree_relations.exs
  77. 16
      apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs
  78. 50
      apps/explorer/test/explorer/chain/block/second_degree_relation_test.exs
  79. 72
      apps/explorer/test/explorer/chain/import_test.exs
  80. 23
      apps/explorer/test/explorer/chain/transaction/fork_test.exs
  81. 27
      apps/explorer/test/explorer/chain_test.exs
  82. 18
      apps/explorer/test/explorer/smart_contract/reader_test.exs
  83. 16
      apps/explorer/test/support/factory.ex
  84. 4
      apps/indexer/lib/indexer/address/coin_balances.ex
  85. 10
      apps/indexer/lib/indexer/block/catchup/fetcher.ex
  86. 21
      apps/indexer/lib/indexer/block/fetcher.ex
  87. 10
      apps/indexer/lib/indexer/block/realtime/fetcher.ex
  88. 5
      apps/indexer/lib/indexer/block/supervisor.ex
  89. 154
      apps/indexer/lib/indexer/block/uncle/fetcher.ex
  90. 38
      apps/indexer/lib/indexer/block/uncle/supervisor.ex
  91. 1
      apps/indexer/lib/indexer/pending_transaction/fetcher.ex
  92. 10
      apps/indexer/test/indexer/address/coin_balances_test.exs
  93. 13
      apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs
  94. 115
      apps/indexer/test/indexer/block/catchup/fetcher_test.exs
  95. 6
      apps/indexer/test/indexer/block/fetcher_test.exs
  96. 12
      apps/indexer/test/indexer/block/realtime/fetcher_test.exs
  97. 126
      apps/indexer/test/indexer/block/uncle/fetcher_test.exs
  98. 18
      apps/indexer/test/support/indexer/block/uncle/supervisor/case.ex

@ -1,3 +1,3 @@
elixir 1.7.1
erlang 21.0.4
nodejs 10.5.0
nodejs 10.11.0

@ -13,6 +13,229 @@ test('RECEIVED_NEW_BLOCK', () => {
expect(output.confirmations).toBe(4)
})
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.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(1)
})
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.newPendingTransactionHashesBatch.length).toEqual(11)
expect(output.pendingTransactionCount).toEqual(11)
})
test('single transaction after single transaction', () => {
const state = Object.assign({}, initialState, {
newPendingTransactions: ['test 1'],
pendingTransactionCount: 1
})
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.newPendingTransactionHashesBatch.length).toEqual(0)
expect(output.pendingTransactionCount).toEqual(2)
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '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.newPendingTransactionHashesBatch.length).toEqual(12)
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '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.newPendingTransactionHashesBatch.length).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.batchCountAccumulator).toEqual(0)
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
pendingTransactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH',
msgs: [{
transactionHash: '0x00',
transactionHtml: 'test'
}]
}
const output = reducer(state, action)
expect(output.newPendingTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.pendingTransactionCount).toEqual(2)
})
})
describe('RECEIVED_NEW_TRANSACTION', () => {
test('single transaction collated', () => {
const state = { ...initialState, pendingTransactionCount: 2 }
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x00'
}
}
const output = reducer(state, action)
expect(output.pendingTransactionCount).toBe(1)
expect(output.newTransactionHashes).toEqual(['0x00'])
})
test('single transaction collated after batch', () => {
const state = Object.assign({}, initialState, {
newPendingTransactionHashesBatch: ['0x01', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11']
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x01'
}
}
const output = reducer(state, action)
expect(output.newPendingTransactionHashesBatch.length).toEqual(10)
expect(output.newPendingTransactionHashesBatch).not.toContain('0x01')
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true,
pendingTransactionCount: 2
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION',
msg: {
transactionHash: '0x01'
}
}
const output = reducer(state, action)
expect(output.pendingTransactionCount).toEqual(1)
})
})
describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
test('single transaction', () => {
const state = initialState
@ -79,7 +302,6 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
})
test('single transaction after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newTransactions: [],
batchCountAccumulator: 11
})
const action = {
@ -95,7 +317,6 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
})
test('large batch of transactions after large batch of transactions', () => {
const state = Object.assign({}, initialState, {
newTransactions: [],
batchCountAccumulator: 11
})
const action = {
@ -146,7 +367,8 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
})
test('on page 2+', () => {
const state = Object.assign({}, initialState, {
beyondPageOne: true
beyondPageOne: true,
transactionCount: 1
})
const action = {
type: 'RECEIVED_NEW_TRANSACTION_BATCH',
@ -158,5 +380,6 @@ describe('RECEIVED_NEW_TRANSACTION_BATCH', () => {
expect(output.newTransactions).toEqual([])
expect(output.batchCountAccumulator).toEqual(0)
expect(output.transactionCount).toEqual(2)
})
})

@ -60,8 +60,20 @@
}
}
@keyframes shrink-out {
0% {
transform: scale(1);
opacity: 1;
}
100% {
opacity: 0;
transform: scale(0.5);
}
}
.fade-in {
animation: fade-in 0.6s ease-out forwards;
opacity: 0;
animation: fade-in 0.6s ease-out 0.4s forwards;
}
.fade-up-blocks-chain {
@ -84,3 +96,7 @@
animation: fade-up--mobile 0.6s cubic-bezier(0.455, 0.03, 0.515, 0.955);
}
}
.shrink-out {
animation: shrink-out 0.45s cubic-bezier(0.55, 0.055, 0.675, 0.19) forwards;
}

@ -4,3 +4,14 @@
width: 100%;
box-shadow: $box-shadow;
}
.dropdown-search-icon {
top: 0.5rem;
left: 0.625rem;
pointer-events: none;
color: $gray-300;
}
.dropdown-search-field {
padding-left: 2rem;
}

@ -32,6 +32,7 @@ import './lib/smart_contract/wei_ether_converter'
import './lib/pretty_json'
import './lib/try_api'
import './lib/token_balance_dropdown'
import './lib/token_balance_dropdown_search'
import './lib/token_transfers_toggle'
import './lib/stop_propagation'

@ -0,0 +1,51 @@
import $ from 'jquery'
const stringContains = (query, string) => {
return string.toLowerCase().search(query) === -1
}
const hideUnmatchedToken = (query, token) => {
const $token = $(token)
const tokenName = $token.data('token-name')
const tokenSymbol = $token.data('token-symbol')
if (stringContains(query, tokenName) && stringContains(query, tokenSymbol)) {
$token.addClass('d-none')
} else {
$token.removeClass('d-none')
}
}
const hideEmptyType = (container) => {
const $container = $(container)
const type = $container.data('token-type')
const countVisibleTokens = $container.children('[data-token-name]:not(.d-none)').length
if (countVisibleTokens === 0) {
$container.addClass('d-none')
} else {
$(`[data-number-of-tokens-by-type='${type}']`).empty().append(countVisibleTokens)
$container.removeClass('d-none')
}
}
const TokenBalanceDropdownSearch = (element, event) => {
const $element = $(element)
const $tokensCount = $element.find('[data-tokens-count]')
const $tokens = $element.find('[data-token-name]')
const $tokenTypes = $element.find('[data-token-type]')
const query = event.target.value.toLowerCase()
$tokens.each((_index, token) => hideUnmatchedToken(query, token))
$tokenTypes.each((_index, container) => hideEmptyType(container))
$tokensCount.html($tokensCount.html().replace(/\d+/g, $tokens.not('.d-none').length))
}
$('[data-token-balance-dropdown]').on('hidden.bs.dropdown', _event => {
$('[data-filter-dropdown-tokens]').val('').trigger('input')
})
$('[data-token-balance-dropdown]').on('input', function (event) {
TokenBalanceDropdownSearch(this, event)
})

@ -4,7 +4,7 @@ import numeral from 'numeral'
import socket from '../socket'
import { updateAllAges } from '../lib/from_now'
import { exchangeRateChannel, formatUsdValue } from '../lib/currency'
import { batchChannel, initRedux } from '../utils'
import { batchChannel, initRedux, slideDownPrepend } from '../utils'
import { createMarketHistoryChart } from '../lib/market_history_chart'
const BATCH_THRESHOLD = 10
@ -131,7 +131,7 @@ if ($chainDetailsPage.length) {
.children()
.slice($transactionsList.children().length - newTransactionsToInsert.length, $transactionsList.children().length)
.remove()
$transactionsList.prepend(newTransactionsToInsert.reverse().join(''))
slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}

@ -1,4 +1,5 @@
import $ from 'jquery'
import _ from 'lodash'
import URI from 'urijs'
import humps from 'humps'
import numeral from 'numeral'
@ -10,12 +11,16 @@ const BATCH_THRESHOLD = 10
export const initialState = {
batchCountAccumulator: 0,
newPendingTransactionHashesBatch: [],
beyondPageOne: null,
blockNumber: null,
channelDisconnected: false,
confirmations: null,
newPendingTransactions: [],
newTransactions: [],
transactionCount: null
newTransactionHashes: [],
transactionCount: null,
pendingTransactionCount: null
}
export function reducer (state = initialState, action) {
@ -24,7 +29,8 @@ export function reducer (state = initialState, action) {
return Object.assign({}, state, {
beyondPageOne: action.beyondPageOne,
blockNumber: parseInt(action.blockNumber, 10),
transactionCount: numeral(action.transactionCount).value()
transactionCount: numeral(action.transactionCount).value(),
pendingTransactionCount: numeral(action.pendingTransactionCount).value()
})
}
case 'CHANNEL_DISCONNECTED': {
@ -40,8 +46,46 @@ export function reducer (state = initialState, action) {
})
} else return state
}
case 'RECEIVED_NEW_TRANSACTION': {
if (state.channelDisconnected) return state
return Object.assign({}, state, {
newPendingTransactionHashesBatch: _.without(state.newPendingTransactionHashesBatch, action.msg.transactionHash),
pendingTransactionCount: state.pendingTransactionCount - 1,
newTransactionHashes: [action.msg.transactionHash]
})
}
case 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH': {
if (state.channelDisconnected) return state
const pendingTransactionCount = state.pendingTransactionCount + action.msgs.length
if (state.beyondPageOne) return Object.assign({}, state, { pendingTransactionCount })
if (!state.newPendingTransactionHashesBatch.length && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
newPendingTransactions: [
...state.newPendingTransactions,
..._.map(action.msgs, 'transactionHtml')
],
pendingTransactionCount
})
} else {
return Object.assign({}, state, {
newPendingTransactionHashesBatch: [
...state.newPendingTransactionHashesBatch,
..._.map(action.msgs, 'transactionHash')
],
pendingTransactionCount
})
}
}
case 'RECEIVED_NEW_TRANSACTION_BATCH': {
if (state.channelDisconnected || state.beyondPageOne) return state
if (state.channelDisconnected) return state
const transactionCount = state.transactionCount + action.msgs.length
if (state.beyondPageOne) return Object.assign({}, state, { transactionCount })
if (!state.batchCountAccumulator && action.msgs.length < BATCH_THRESHOLD) {
return Object.assign({}, state, {
@ -49,12 +93,12 @@ export function reducer (state = initialState, action) {
...state.newTransactions,
...action.msgs.map(({transactionHtml}) => transactionHtml)
],
transactionCount: state.transactionCount + action.msgs.length
transactionCount
})
} else {
return Object.assign({}, state, {
batchCountAccumulator: state.batchCountAccumulator + action.msgs.length,
transactionCount: state.transactionCount + action.msgs.length
transactionCount
})
}
}
@ -91,23 +135,89 @@ if ($transactionDetailsPage.length) {
})
}
const $transactionPendingListPage = $('[data-page="transaction-pending-list"]')
if ($transactionPendingListPage.length) {
initRedux(reducer, {
main (store) {
store.dispatch({
type: 'PAGE_LOAD',
pendingTransactionCount: $('[data-selector="transaction-pending-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).insertedAt
})
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('new_transaction', (msg) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })
)
const pendingTransactionsChannel = socket.channel(`transactions:new_pending_transaction`)
pendingTransactionsChannel.join()
pendingTransactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
pendingTransactionsChannel.on('new_pending_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
},
render (state, oldState) {
const $channelBatching = $('[data-selector="channel-batching-message"]')
const $channelBatchingCount = $('[data-selector="channel-batching-count"]')
const $channelDisconnected = $('[data-selector="channel-disconnected-message"]')
const $pendingTransactionsList = $('[data-selector="transactions-pending-list"]')
const $pendingTransactionsCount = $('[data-selector="transaction-pending-count"]')
if (state.channelDisconnected) $channelDisconnected.show()
if (oldState.pendingTransactionCount !== state.pendingTransactionCount) {
$pendingTransactionsCount.empty().append(numeral(state.pendingTransactionCount).format())
}
if (oldState.newTransactionHashes !== state.newTransactionHashes && state.newTransactionHashes.length > 0) {
const $transaction = $(`[data-transaction-hash="${state.newTransactionHashes[0]}"]`)
$transaction.addClass('shrink-out')
setTimeout(() => $transaction.slideUp({
complete: () => {
if ($pendingTransactionsList.children().length < 2 && state.pendingTransactionCount > 0) {
window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString()
} else {
$transaction.remove()
}
}
}), 400)
}
if (state.newPendingTransactionHashesBatch.length) {
$channelBatching.show()
$channelBatchingCount[0].innerHTML = numeral(state.newPendingTransactionHashesBatch.length).format()
} else {
$channelBatching.hide()
}
if (oldState.newPendingTransactions !== state.newPendingTransactions) {
const newTransactionsToInsert = state.newPendingTransactions.slice(oldState.newPendingTransactions.length)
$pendingTransactionsList
.children()
.slice($pendingTransactionsList.children().length - newTransactionsToInsert.length,
$pendingTransactionsList.children().length
)
.remove()
prependWithClingBottom($pendingTransactionsList, newTransactionsToInsert.reverse().join(''))
updateAllAges()
}
}
})
}
const $transactionListPage = $('[data-page="transaction-list"]')
if ($transactionListPage.length) {
initRedux(reducer, {
main (store) {
const state = store.dispatch({
store.dispatch({
type: 'PAGE_LOAD',
transactionCount: $('[data-selector="transaction-count"]').text(),
beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index
})
if (!state.beyondPageOne) {
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
transactionsChannel.on('new_transaction', batchChannel((msgs) =>
store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }))
)
}
const transactionsChannel = socket.channel(`transactions:new_transaction`)
transactionsChannel.join()
transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' }))
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"]')

@ -34,11 +34,16 @@ export function initRedux (reducer, { main, render, debug } = {}) {
if (main) main(store)
}
export function slideDownPrepend ($el, content, callback) {
const $content = $(content)
$el.prepend($content.hide())
$content.slideDown({ complete: callback })
}
export function prependWithClingBottom ($el, content) {
function userAtTop () {
return window.scrollY < $('[data-selector="navbar"]').outerHeight()
}
if (userAtTop()) return $el.prepend(content)
if (userAtTop()) return slideDownPrepend($el, content)
let isAnimating
function setIsAnimating () {
@ -67,8 +72,9 @@ export function prependWithClingBottom ($el, content) {
$el.off('animationstart', setIsAnimating)
$el.off('animationend animationcancel', stopClinging)
}
$el.on('animationend animationcancel', stopClinging)
setTimeout(() => !isAnimating && stopClinging(), 100)
return $el.prepend(content)
return slideDownPrepend($el, content, () => {
$el.on('animationend animationcancel', stopClinging)
setTimeout(() => !isAnimating && stopClinging(), 100)
})
}

@ -1481,9 +1481,9 @@
"dev": true
},
"bootstrap": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.1.tgz",
"integrity": "sha512-SpiDSOcbg4J/PjVSt4ny5eY6j74VbVSjROY4Fb/WIUXBV9cnb5luyR4KnPvNoXuGnBK1T+nJIWqRsvU3yP8Mcg=="
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-4.1.3.tgz",
"integrity": "sha512-rDFIzgXcof0jDyjNosjv4Sno77X4KuPeFxG2XZZv1/Kc8DRVGVADdoQyyOVDwPqL36DDmtCQbrpMCqvpPLJQ0w=="
},
"brace-expansion": {
"version": "1.1.11",

@ -21,7 +21,7 @@
"dependencies": {
"@fortawesome/fontawesome-free": "^5.1.0-4",
"bignumber.js": "^7.2.1",
"bootstrap": "^4.1.0",
"bootstrap": "^4.1.3",
"chart.js": "^2.7.2",
"clipboard": "^2.0.1",
"humps": "^2.0.1",

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 42 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<browserconfig>
<msapplication>
<tile>
<square150x150logo src="/mstile-150x150.png"/>
<TileColor>#da532c</TileColor>
</tile>
</msapplication>
</browserconfig>

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 10 KiB

@ -0,0 +1,39 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
width="512.000000pt" height="512.000000pt" viewBox="0 0 512.000000 512.000000"
preserveAspectRatio="xMidYMid meet">
<metadata>
Created by potrace 1.11, written by Peter Selinger 2001-2013
</metadata>
<g transform="translate(0.000000,512.000000) scale(0.100000,-0.100000)"
fill="#000000" stroke="none">
<path d="M1240 4188 c-123 -42 -202 -96 -276 -193 -33 -43 -78 -132 -89 -178
-4 -15 -19 -36 -34 -47 -70 -54 -86 -67 -116 -100 -42 -44 -85 -96 -113 -136
-34 -50 -168 -323 -163 -333 2 -2 -1 -7 -6 -10 -4 -3 -15 -22 -25 -41 -9 -19
-40 -84 -68 -145 -29 -60 -74 -154 -101 -208 -27 -54 -49 -104 -49 -112 0 -8
-4 -15 -9 -15 -5 0 -12 -10 -16 -22 -4 -13 -8 -25 -9 -28 -1 -3 -19 -38 -39
-78 -37 -72 -91 -220 -101 -277 -3 -16 -10 -52 -15 -80 -9 -49 -8 -290 2 -330
2 -11 8 -40 12 -65 4 -25 10 -49 13 -55 4 -5 8 -21 10 -35 2 -13 7 -29 11 -35
4 -5 13 -29 21 -52 7 -24 17 -43 22 -43 4 0 8 -8 8 -18 0 -10 14 -39 30 -65
17 -26 30 -52 30 -57 0 -6 5 -10 10 -10 6 0 10 -5 10 -11 0 -6 6 -16 12 -23 7
-7 24 -28 38 -47 134 -182 382 -339 623 -394 102 -23 315 -39 342 -26 6 3 19
6 30 6 11 1 45 5 75 10 30 5 57 9 60 10 3 0 11 4 18 8 6 5 12 6 12 3 0 -3 30
6 68 20 37 14 72 27 77 28 16 3 115 56 172 92 178 114 345 316 423 514 32 80
59 178 66 235 7 68 11 77 31 74 10 0 20 0 23 1 44 17 303 25 460 15 190 -12
186 -11 188 -52 1 -28 7 -61 16 -91 3 -9 7 -28 10 -42 15 -84 85 -240 156
-345 49 -73 175 -207 251 -267 56 -45 212 -127 296 -157 186 -66 380 -80 592
-45 55 9 181 53 261 91 216 103 405 288 511 503 22 43 39 82 39 87 0 4 6 20
14 34 17 33 25 64 50 193 13 65 15 294 4 356 -14 77 -37 163 -55 210 -4 8 -7
17 -8 20 -1 3 -6 16 -12 30 -31 72 -56 127 -113 245 -34 72 -86 180 -115 240
-29 61 -81 169 -116 240 -34 72 -63 132 -64 135 -5 22 -87 176 -118 222 -47
70 -146 173 -209 217 -28 20 -49 44 -53 60 -3 14 -22 59 -43 100 -128 256
-448 362 -713 236 -117 -55 -228 -177 -271 -299 -21 -59 -19 -56 -54 -61 -16
-2 -49 -8 -74 -14 -49 -10 -165 -25 -192 -23 -22 1 -25 -3 -32 -62 -11 -77
-37 -129 -89 -175 -50 -44 -63 -51 -108 -66 -100 -34 -197 -19 -291 43 -65 42
-118 141 -120 223 -1 16 -6 29 -12 30 -6 0 -20 2 -31 5 -11 2 -40 6 -65 9 -25
3 -61 8 -80 12 -19 3 -44 7 -55 8 -89 13 -89 13 -124 95 -51 120 -148 221
-261 273 -100 47 -108 48 -235 51 -76 2 -113 -2 -155 -16z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.4 KiB

@ -0,0 +1,19 @@
{
"name": "",
"short_name": "",
"icons": [
{
"src": "/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/android-chrome-512x512.png",
"sizes": "512x512",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

@ -6,9 +6,11 @@ defmodule BlockScoutWeb.Chain do
import Explorer.Chain,
only: [
hash_to_address: 1,
hash_to_block: 1,
hash_to_transaction: 1,
number_to_block: 1,
string_to_address_hash: 1,
string_to_block_hash: 1,
string_to_transaction_hash: 1
]
@ -53,7 +55,7 @@ defmodule BlockScoutWeb.Chain do
def from_param("0x" <> number_string = param) do
case String.length(number_string) do
40 -> address_from_param(param)
64 -> transaction_from_param(param)
64 -> block_or_transaction_from_param(param)
_ -> {:error, :not_found}
end
end
@ -196,6 +198,12 @@ defmodule BlockScoutWeb.Chain do
%{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)}
end
defp block_or_transaction_from_param(param) do
with {:error, :not_found} <- transaction_from_param(param) do
hash_string_to_block(param)
end
end
defp transaction_from_param(param) do
with {:ok, hash} <- string_to_transaction_hash(param) do
hash_to_transaction(hash)
@ -203,4 +211,12 @@ defmodule BlockScoutWeb.Chain do
:error -> {:error, :not_found}
end
end
defp hash_string_to_block(hash_string) do
with {:ok, hash} <- string_to_block_hash(hash_string) do
hash_to_block(hash)
else
:error -> {:error, :not_found}
end
end
end

@ -5,18 +5,41 @@ defmodule BlockScoutWeb.TransactionChannel do
use BlockScoutWeb, :channel
alias BlockScoutWeb.TransactionView
alias Explorer.Chain.Hash
alias Phoenix.View
intercept(["new_transaction"])
intercept(["new_pending_transaction", "new_transaction"])
def join("transactions:new_transaction", _params, socket) do
{:ok, %{}, socket}
end
def join("transactions:new_pending_transaction", _params, socket) do
{:ok, %{}, socket}
end
def join("transactions:" <> _transaction_hash, _params, socket) do
{:ok, %{}, socket}
end
def handle_out("new_pending_transaction", %{transaction: transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
rendered_transaction =
View.render_to_string(
TransactionView,
"_pending_tile.html",
transaction: transaction
)
push(socket, "new_pending_transaction", %{
transaction_hash: Hash.to_string(transaction.hash),
transaction_html: rendered_transaction
})
{:noreply, socket}
end
def handle_out("new_transaction", %{transaction: transaction}, socket) do
Gettext.put_locale(BlockScoutWeb.Gettext, socket.assigns.locale)
@ -28,6 +51,7 @@ defmodule BlockScoutWeb.TransactionChannel do
)
push(socket, "new_transaction", %{
transaction_hash: Hash.to_string(transaction.hash),
transaction_html: rendered_transaction
})

@ -23,7 +23,7 @@ defmodule BlockScoutWeb.BlockController do
render(conn, "index.html", blocks: blocks, next_page_params: next_page_params(next_page, blocks, params))
end
def show(conn, %{"id" => number}) do
redirect(conn, to: block_transaction_path(conn, :index, number))
def show(conn, %{"hash_or_number" => hash_or_number}) do
redirect(conn, to: block_transaction_path(conn, :index, hash_or_number))
end
end

@ -1,15 +1,18 @@
defmodule BlockScoutWeb.BlockTransactionController do
use BlockScoutWeb, :controller
import BlockScoutWeb.Chain,
only: [paging_options: 1, param_to_block_number: 1, next_page_params: 3, split_list_by_page: 1]
import BlockScoutWeb.Chain, only: [paging_options: 1, next_page_params: 3, split_list_by_page: 1]
import Explorer.Chain, only: [hash_to_block: 2, number_to_block: 2, string_to_block_hash: 1]
alias Explorer.Chain
def index(conn, %{"block_id" => formatted_block_number} = params) do
with {:ok, block_number} <- param_to_block_number(formatted_block_number),
{:ok, block} <- Chain.number_to_block(block_number, necessity_by_association: %{miner: :required}),
block_transaction_count <- Chain.block_to_transaction_count(block) do
def index(conn, %{"block_hash_or_number" => formatted_block_hash_or_number} = params) do
with {:ok, block} <-
param_block_hash_or_number_to_block(formatted_block_hash_or_number,
necessity_by_association: %{miner: :required}
) do
block_transaction_count = Chain.block_to_transaction_count(block)
full_options =
Keyword.merge(
[
@ -36,11 +39,30 @@ defmodule BlockScoutWeb.BlockTransactionController do
transactions: transactions
)
else
{:error, :invalid} ->
{:error, {:invalid, :hash}} ->
not_found(conn)
{:error, {:invalid, :number}} ->
not_found(conn)
{:error, :not_found} ->
not_found(conn)
end
end
defp param_block_hash_or_number_to_block("0x" <> _ = param, options) do
with {:ok, hash} <- string_to_block_hash(param) do
hash_to_block(hash, options)
else
:error -> {:error, {:invalid, :hash}}
end
end
defp param_block_hash_or_number_to_block(number_string, options) when is_binary(number_string) do
with {:ok, number} <- BlockScoutWeb.Chain.param_to_block_number(number_string) do
number_to_block(number, options)
else
{:error, :invalid} -> {:error, {:invalid, :number}}
end
end
end

@ -33,6 +33,7 @@ defmodule BlockScoutWeb.SmartContractController do
def show(conn, params) do
with true <- ajax?(conn),
{:ok, address_hash} <- Chain.string_to_address_hash(params["id"]),
{:ok, _address} <- Chain.find_contract_address(address_hash),
outputs =
Reader.query_function(
address_hash,

@ -13,6 +13,7 @@ defmodule BlockScoutWeb.TransactionLogController do
transaction_hash,
necessity_by_association: %{
:block => :optional,
[created_contract_address: :names] => :optional,
[from_address: :names] => :required,
[to_address: :names] => :optional,
:token_transfers => :optional

@ -4,7 +4,7 @@ defmodule BlockScoutWeb.Notifier do
"""
alias Explorer.{Chain, Market, Repo}
alias Explorer.Chain.{Address, InternalTransaction}
alias Explorer.Chain.{Address, InternalTransaction, Transaction}
alias Explorer.ExchangeRates.Token
alias BlockScoutWeb.Endpoint
@ -49,7 +49,7 @@ defmodule BlockScoutWeb.Notifier do
transaction_hashes
|> Chain.hashes_to_transactions(
necessity_by_association: %{
:block => :required,
:block => :optional,
[created_contract_address: :names] => :optional,
[from_address: :names] => :optional,
[to_address: :names] => :optional,
@ -93,6 +93,12 @@ defmodule BlockScoutWeb.Notifier do
end
end
defp broadcast_transaction(%Transaction{block_number: nil} = pending) do
Endpoint.broadcast("transactions:new_pending_transaction", "new_pending_transaction", %{
transaction: pending
})
end
defp broadcast_transaction(transaction) do
Endpoint.broadcast("transactions:new_transaction", "new_transaction", %{
transaction: transaction

@ -40,7 +40,7 @@ defmodule BlockScoutWeb.Router do
resources("/", ChainController, only: [:show], singleton: true, as: :chain)
resources "/blocks", BlockController, only: [:index, :show] do
resources "/blocks", BlockController, only: [:index, :show], param: "hash_or_number" do
resources("/transactions", BlockTransactionController, only: [:index], as: :transaction)
end

@ -1,4 +1,4 @@
<div class="card card-primary">
<div class="card card-primary" data-test="outside_of_dropdown">
<div class="card-body">
<h2 class="card-title text-white"><%= gettext "Balance" %></h2>
<span></span>

@ -32,24 +32,24 @@
<%= link(token_title(@address.token), to: token_path(@conn, :show, @address.hash), "data-test": "token_hash_link" ) %>
</span>
<% end %>
<%= if contract?(@address) do %>
<span data-test="address_contract_creator">
<%= gettext "Contract created by" %>
<%= link(
trimmed_hash(@address.contracts_creation_internal_transaction.from_address_hash),
to: address_path(@conn, :show, @address.contracts_creation_internal_transaction.from_address_hash)
) %>
</div>
<%= if contract?(@address) do %>
<span class="text-muted" data-test="address_contract_creator">
<%= gettext "Created by" %>
<%= link(
trimmed_hash(@address.contracts_creation_internal_transaction.from_address_hash),
to: address_path(@conn, :show, @address.contracts_creation_internal_transaction.from_address_hash)
) %>
<%= gettext "at" %>
<%= gettext "at" %>
<%= link(
trimmed_hash(@address.contracts_creation_internal_transaction.transaction_hash),
to: transaction_path(@conn, :show, @address.contracts_creation_internal_transaction.transaction_hash),
"data-test": "transaction_hash_link"
) %>
</span>
<% end %>
</div>
<%= link(
trimmed_hash(@address.contracts_creation_internal_transaction.transaction_hash),
to: transaction_path(@conn, :show, @address.contracts_creation_internal_transaction.transaction_hash),
"data-test": "transaction_hash_link"
) %>
</span>
<% end %>
</div>
</div>
</div>

@ -35,12 +35,12 @@
class: "nav-link active") do %>
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<%= if BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
</li>
<%= if smart_contract_with_read_only_functions?(@address) do %>
<%= if BlockScoutWeb.AddressView.smart_contract_with_read_only_functions?(@address) do %>
<li class="nav-item">
<%= link(
gettext("Read Contract"),
@ -56,7 +56,7 @@
<a class="nav-link active dropdown-toggle" data-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<%= if BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
</a>
@ -82,11 +82,11 @@
class: "dropdown-item active") do %>
<%= gettext("Code") %>
<%= if smart_contract_verified?(@address) do %>
<%= if BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %>
<i class="far fa-check-circle"></i>
<% end %>
<% end %>
<%= if smart_contract_with_read_only_functions?(@address) do %>
<%= if BlockScoutWeb.AddressView.smart_contract_with_read_only_functions?(@address) do %>
<%= link(
gettext("Read Contract"),
to: address_read_contract_path(@conn, :index, @address.hash),
@ -98,7 +98,7 @@
</div>
<div class="card-body">
<%= if !smart_contract_verified?(@address) do %>
<%= if !BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %>
<%= link(
gettext("Verify & Publish"),
to: address_verify_contract_path(@conn, :new, @address.hash),
@ -107,7 +107,7 @@
) %>
<% end %>
<%= if smart_contract_verified?(@address) do %>
<%= if BlockScoutWeb.AddressView.smart_contract_verified?(@address) do %>
<div class="mb-4">
<dl class="row">
<dt class="col-sm-4 col-md-2 text-muted"><%= gettext "Contract name:" %></dt>
@ -115,7 +115,7 @@
</dl>
<dl class="row">
<dt class="col-sm-4 col-md-2 text-muted"><%= gettext "Optimization enabled" %></dt>
<dd class="col-sm-8 col-md-10"><%= format_optimization(@address.smart_contract.optimization) %></dd>
<dd class="col-sm-8 col-md-10"><%= gettext("%{}", @address.smart_contract.optimization) %></dd>
</dl>
<dl class="row">
<dt class="col-sm-4 col-md-2 text-muted"><%= gettext "Compiler version" %></dt>

@ -16,6 +16,16 @@
<div class="dropdown-menu dropdown-menu-right token-balance-dropdown p-0" aria-labelledby="dropdown-tokens">
<div data-dropdown-items class="dropdown-items">
<div class="m-3 position-relative">
<i class="fas fa-search position-absolute dropdown-search-icon"></i>
<%= text_input(
:token_search,
:name,
class: "w-100 dropdown-search-field",
'data-filter-dropdown-tokens': true,
placeholder: gettext("Search tokens")
) %>
</div>
<%= if Enum.any?(@token_balances, & &1.token.type == "ERC-721") do %>
<%= render(
"_tokens.html",

@ -1,16 +1,24 @@
<h6 class="dropdown-header">
<%= @type %> (<%= Enum.count(@token_balances)%>)
</h6>
<%= for token_balance <- sort_by_name(@token_balances) do %>
<div class="border-bottom">
<%= link(
to: token_path(@conn, :show, token_balance.token.contract_address_hash),
class: "dropdown-item"
) do %>
<p class="mb-0"><%= token_name(token_balance.token) %></p>
<p class="mb-0">
<%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_balance.token.symbol %>
</p>
<% end %>
</div>
<% end %>
<div data-token-type="<%= @type %>">
<h6 class="dropdown-header">
<%= @type %> (<span data-number-of-tokens-by-type="<%= @type %>"><%= Enum.count(@token_balances)%></span>)
</h6>
<%= for token_balance <- sort_by_name(@token_balances) do %>
<div
class="border-bottom"
data-dropdown-token-balance-test
data-token-name="<%= token_name(token_balance.token) %>"
data-token-symbol="<%= token_balance.token.symbol %>"
>
<%= link(
to: token_path(@conn, :show, token_balance.token.contract_address_hash),
class: "dropdown-item"
) do %>
<p class="mb-0"><%= token_name(token_balance.token) %></p>
<p class="mb-0">
<%= format_according_to_decimals(token_balance.value, token_balance.token.decimals) %> <%= token_balance.token.symbol %>
</p>
<% end %>
</div>
<% end %>
</div>

@ -12,7 +12,7 @@
<%= link(
gettext("Transactions"),
class: "nav-link active",
to: block_transaction_path(@conn, :index, @conn.params["block_id"])
to: block_transaction_path(@conn, :index, @conn.params["block_hash_or_number"])
) %>
</li>
</ul>
@ -25,7 +25,7 @@
<%= link(
gettext("Transactions"),
class: "dropdown-item active",
to: block_transaction_path(@conn, :index, @conn.params["block_id"])
to: block_transaction_path(@conn, :index, @conn.params["block_hash_or_number"])
) %>
</div>
</li>

@ -6,6 +6,16 @@
<meta name="viewport" content="width=device-width, initial-scale=1">
<title><%= gettext "BlockScout" %></title>
<link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>">
<link rel="apple-touch-icon" sizes="180x180" href="<%= static_path(@conn, "/apple-touch-icon.png") %>">
<link rel="icon" type="image/png" sizes="32x32" href="<%= static_path(@conn, "/favicon-32x32.png") %>">
<link rel="icon" type="image/png" sizes="16x16" href="<%= static_path(@conn, "/favicon-16x16.png") %>">
<link rel="manifest" href="<%= static_path(@conn, "/site.webmanifest") %>">
<link rel="mask-icon" href="<%= static_path(@conn, "/safari-pinned-tab.svg") %>" color="#5bbad5">
<link rel="shortcut icon" href="<%= static_path(@conn, "/favicon.ico") %>">
<meta name="msapplication-TileColor" content="#7dd79f">
<meta name="msapplication-config" content="<%= static_path(@conn, "/browserconfig.xml") %>">
<meta name="theme-color" content="#ffffff">
</head>
<body>

@ -1,4 +1,4 @@
<section class="container">
<section class="container" data-page="transaction-pending-list">
<div class="card">
<div class="card-header">
@ -43,36 +43,28 @@
</div>
<div class="card-body">
<div data-selector="channel-batching-message" style="display:none;">
<div data-selector="reload-button" class="alert alert-info">
<a href="#" class="alert-link"><span data-selector="channel-batching-count"></span> <%= gettext "More transactions have come in" %></a>
</div>
</div>
<div data-selector="channel-disconnected-message" style="display:none;">
<div data-selector="reload-button" class="alert alert-danger">
<a href="#" class="alert-link"><%= gettext "Connection Lost, click to load newer transactions" %></a>
</div>
</div>
<h2 class="card-title mb-0"><%= gettext "Transactions" %></h2>
<p><%= gettext("Showing %{count} Pending Transactions", count: @pending_transaction_count) %></p>
<%= for transaction <- @transactions do %>
<div class="tile tile-type-<%= BlockScoutWeb.TransactionView.type_suffix(transaction) %>" data-test="<%= BlockScoutWeb.TransactionView.type_suffix(transaction) %>" data-transaction-hash="<%= transaction.hash %>">
<div class="row" data-test="chain_transaction">
<div class="col-md-2 d-flex flex-row flex-md-column align-items-left justify-content-start justify-content-lg-center mb-1 mb-md-0 pl-md-4">
<span class="tile-label" data-test="transaction_type"> <%= BlockScoutWeb.TransactionView.transaction_display_type(transaction) %></span>
<div class="tile-status-label ml-2 ml-md-0" data-test="transaction_status"><%= BlockScoutWeb.TransactionView.formatted_status(transaction) %></div>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: transaction.hash %>
<span class="text-nowrap">
<%= render BlockScoutWeb.AddressView, "_link.html", address: transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(transaction.from_address) %>
&rarr;
<%= if transaction.to_address_hash do %>
<%= render BlockScoutWeb.AddressView, "_link.html", address: transaction.to_address, contract: BlockScoutWeb.AddressView.contract?(transaction.to_address) %>
<% else %>
<%= gettext("Contract Address Pending") %>
<% end %>
</span>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="tile-title"><%= BlockScoutWeb.TransactionView.value(transaction, include_label: false) %> <%= gettext "Ether" %></span>
<span class="ml-0 ml-md-1 text-nowrap"> <%= BlockScoutWeb.TransactionView.formatted_fee(transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %></span>
</span>
<p>
<%= gettext("Showing") %>
<span data-selector="transaction-pending-count"><%= Cldr.Number.to_string!(@pending_transaction_count, format: "#,###") %></span>
<%= gettext("Pending Transactions") %>
</p>
<span data-selector="transactions-pending-list">
<%= for transaction <- @transactions do %>
<%= render BlockScoutWeb.TransactionView, "_pending_tile.html", transaction: transaction %>
<% end %>
</span>
</div>
</div>
</div>
<% end %>
<%= if @next_page_params do %>
<%= link(
gettext("Older"),

@ -0,0 +1,24 @@
<div class="tile tile-type-<%= BlockScoutWeb.TransactionView.type_suffix(@transaction) %> fade-in" data-test="<%= BlockScoutWeb.TransactionView.type_suffix(@transaction) %>" data-transaction-hash="<%= @transaction.hash %>">
<div class="row" data-test="chain_transaction">
<div class="col-md-2 d-flex flex-row flex-md-column align-items-left justify-content-start justify-content-lg-center mb-1 mb-md-0 pl-md-4">
<span class="tile-label" data-test="transaction_type"> <%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %></span>
<div class="tile-status-label ml-2 ml-md-0" data-test="transaction_status"><%= BlockScoutWeb.TransactionView.formatted_status(@transaction) %></div>
</div>
<div class="col-md-7 col-lg-8 d-flex flex-column pr-2 pr-sm-2 pr-md-0">
<%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @transaction.hash %>
<span class="text-nowrap">
<%= render BlockScoutWeb.AddressView, "_link.html", address: @transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@transaction.from_address) %>
&rarr;
<%= if @transaction.to_address_hash do %>
<%= render BlockScoutWeb.AddressView, "_link.html", address: @transaction.to_address, contract: BlockScoutWeb.AddressView.contract?(@transaction.to_address) %>
<% else %>
<%= gettext("Contract Address Pending") %>
<% end %>
</span>
<span class="d-flex flex-md-row flex-column mt-3 mt-md-0">
<span class="tile-title"><%= BlockScoutWeb.TransactionView.value(@transaction, include_label: false) %> <%= gettext "Ether" %></span>
<span class="ml-0 ml-md-1 text-nowrap"> <%= BlockScoutWeb.TransactionView.formatted_fee(@transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %></span>
</span>
</div>
</div>
</div>

@ -51,7 +51,7 @@
<div class="offset-md-2 col-md-10 col-lg-8 d-flex flex-column mt-2 mb-2">
<% [first_token_transfer | remaining_token_transfers]= @transaction.token_transfers %>
<%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: first_token_transfer %>
<div class="collapse token-transfer-toggle" id="<%= @transaction.hash %>">
<div class="collapse token-transfer-toggle" id="transaction-<%= @transaction.hash %>">
<%= for token_transfer <- remaining_token_transfers do %>
<%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: token_transfer %>
<% end %>
@ -60,8 +60,8 @@
<%= if Enum.any?(remaining_token_transfers) do %>
<div class="col-md-12 d-flex flex-column mt-1 mb-2 text-center token-tile-view-more">
<span class="token-tile-more-lines">
<%= link gettext("View More Transfers"), to: "##{@transaction.hash}", "data-toggle": "collapse", "data-selector": "token-transfer-open", "data-test": "token_transfers_expansion" %>
<%= link gettext("View Less Transfers"), class: "d-none", to: "##{@transaction.hash}", "data-toggle": "collapse", "data-selector": "token-transfer-close" %>
<%= link gettext("View More Transfers"), to: "#transaction-#{@transaction.hash}", "data-toggle": "collapse", "data-selector": "token-transfer-open", "data-test": "token_transfers_expansion" %>
<%= link gettext("View Less Transfers"), class: "d-none", to: "#transaction-#{@transaction.hash}", "data-toggle": "collapse", "data-selector": "token-transfer-close" %>
</span>
</div>
<% end %>

@ -54,7 +54,11 @@
</div>
</div>
<h1><%= gettext "Transactions" %></h1>
<p><%= gettext("Showing") %> <span data-selector="transaction-count"><%= Cldr.Number.to_string!(@transaction_estimated_count, format: "#,###") %></span> <%= gettext("Validated Transactions") %></p>
<p>
<%= gettext("Showing") %>
<span data-selector="transaction-count"><%= Cldr.Number.to_string!(@transaction_estimated_count, format: "#,###") %></span>
<%= gettext("Validated Transactions") %>
</p>
<span data-selector="transactions-list">
<%= for transaction <- @transactions do %>
<%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %>

@ -1,10 +1,5 @@
defmodule BlockScoutWeb.AddressContractView do
use BlockScoutWeb, :view
import BlockScoutWeb.AddressView, only: [smart_contract_verified?: 1, smart_contract_with_read_only_functions?: 1]
def format_smart_contract_abi(abi), do: Poison.encode!(abi, pretty: false)
def format_optimization(true), do: gettext("true")
def format_optimization(false), do: gettext("false")
end

@ -25,10 +25,6 @@ defmodule BlockScoutWeb.TransactionView do
def contract_creation?(_), do: false
def to_address(%Transaction{to_address: nil, created_contract_address: %Address{} = address}), do: address
def to_address(%Transaction{to_address: %Address{} = address}), do: address
def fee(%Transaction{} = transaction) do
{_, value} = Chain.fee(transaction, :wei)
value

@ -7,9 +7,13 @@ defimpl Phoenix.Param, for: [Address, Transaction] do
end
defimpl Phoenix.Param, for: Block do
def to_param(%@for{number: number}) do
def to_param(%@for{consensus: true, number: number}) do
to_string(number)
end
def to_param(%@for{consensus: false, hash: hash}) do
to_string(hash)
end
end
defimpl Phoenix.Param, for: Hash do

@ -55,7 +55,7 @@ msgstr ""
#: lib/block_scout_web/templates/block_transaction/index.html.eex:36
#: lib/block_scout_web/templates/chain/show.html.eex:73
#: lib/block_scout_web/templates/layout/_topnav.html.eex:24
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:46
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:56
#: lib/block_scout_web/templates/transaction/index.html.eex:56
msgid "Transactions"
msgstr ""
@ -157,7 +157,7 @@ msgstr ""
msgid "Overview"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:57
#: lib/block_scout_web/views/transaction_view.ex:53
msgid "Success"
msgstr ""
@ -209,8 +209,8 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/index.html.eex:16
#: lib/block_scout_web/templates/transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/overview.html.eex:54
#: lib/block_scout_web/views/transaction_view.ex:55
#: lib/block_scout_web/views/transaction_view.ex:81
#: lib/block_scout_web/views/transaction_view.ex:51
#: lib/block_scout_web/views/transaction_view.ex:77
msgid "Pending"
msgstr ""
@ -225,7 +225,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:21
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:48
#: lib/block_scout_web/templates/transaction_log/index.html.eex:10
#: lib/block_scout_web/views/transaction_view.ex:175
#: lib/block_scout_web/views/transaction_view.ex:171
msgid "Logs"
msgstr ""
@ -294,7 +294,7 @@ msgid "Showing #%{number}"
msgstr ""
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:15
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:68
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:19
#: lib/block_scout_web/templates/transaction/_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/overview.html.eex:98
#: lib/block_scout_web/views/wei_helpers.ex:72
@ -323,7 +323,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:14
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:43
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:10
#: lib/block_scout_web/views/transaction_view.ex:174
#: lib/block_scout_web/views/transaction_view.ex:170
msgid "Internal Transactions"
msgstr ""
@ -399,13 +399,13 @@ msgid "Avg Block Time"
msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:17
#: lib/block_scout_web/templates/layout/app.html.eex:24
#: lib/block_scout_web/templates/layout/app.html.eex:34
#: lib/block_scout_web/views/address_view.ex:99
msgid "Market Cap"
msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:10
#: lib/block_scout_web/templates/layout/app.html.eex:25
#: lib/block_scout_web/templates/layout/app.html.eex:35
msgid "Price"
msgstr ""
@ -464,7 +464,7 @@ msgstr ""
msgid "Total Gas Used"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:126
#: lib/block_scout_web/views/transaction_view.ex:122
msgid "Transaction"
msgstr ""
@ -477,7 +477,7 @@ msgstr ""
msgid "View All"
msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/_tile.html.eex:23
#: lib/block_scout_web/templates/transaction/overview.html.eex:71
msgid "TX Fee"
@ -504,9 +504,9 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:162
#: lib/block_scout_web/templates/block/index.html.eex:20
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:78
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:70
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:34
#: lib/block_scout_web/templates/transaction/index.html.eex:66
#: lib/block_scout_web/templates/transaction/index.html.eex:70
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:24
msgid "Older"
msgstr ""
@ -565,7 +565,7 @@ msgid "Newer"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:124
#: lib/block_scout_web/views/transaction_view.ex:120
msgid "Contract Creation"
msgstr ""
@ -617,6 +617,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:108
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:53
#: lib/block_scout_web/templates/transaction/index.html.eex:53
msgid "Connection Lost, click to load newer transactions"
msgstr ""
@ -655,24 +656,19 @@ msgid "There are no transactions for this address."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:64
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:15
#: lib/block_scout_web/views/address_view.ex:21
#: lib/block_scout_web/views/address_view.ex:55
msgid "Contract Address Pending"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:125
#: lib/block_scout_web/views/transaction_view.ex:121
msgid "Contract Call"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:37
msgid "Contract created by"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:43
#: lib/block_scout_web/templates/address/overview.html.eex:44
msgid "at"
msgstr ""
@ -774,7 +770,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4
#: lib/block_scout_web/views/transaction_view.ex:123
#: lib/block_scout_web/views/transaction_view.ex:119
msgid "Token Transfer"
msgstr ""
@ -793,17 +789,19 @@ msgstr[1] ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:103
#: lib/block_scout_web/templates/chain/show.html.eex:69
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:48
#: lib/block_scout_web/templates/transaction/index.html.eex:48
msgid "More transactions have come in"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/index.html.eex:57
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:58
#: lib/block_scout_web/templates/transaction/index.html.eex:58
msgid "Showing"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/index.html.eex:57
#: lib/block_scout_web/templates/transaction/index.html.eex:60
msgid "Validated Transactions"
msgstr ""
@ -824,7 +822,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:36
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:10
#: lib/block_scout_web/views/tokens/overview_view.ex:34
#: lib/block_scout_web/views/transaction_view.ex:173
#: lib/block_scout_web/views/transaction_view.ex:169
msgid "Token Transfers"
msgstr ""
@ -1038,12 +1036,12 @@ msgid "View Less Transfers"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:23
#: lib/block_scout_web/templates/layout/app.html.eex:33
msgid "Less than"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:47
#: lib/block_scout_web/views/transaction_view.ex:43
msgid "Max of"
msgstr ""
@ -1200,17 +1198,17 @@ msgid "Connection Lost, click to load newer blocks"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:56
#: lib/block_scout_web/views/transaction_view.ex:52
msgid "(Awaiting internal transactions for status)"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:60
#: lib/block_scout_web/views/transaction_view.ex:56
msgid "Error: %{reason}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:58
#: lib/block_scout_web/views/transaction_view.ex:54
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
@ -1260,3 +1258,24 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:23
msgid "This transaction is pending confirmation."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:38
msgid "Created by"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:118
msgid "%{}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:60
msgid "Pending Transactions"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:26
msgid "Search tokens"
msgstr ""

@ -67,7 +67,7 @@ msgstr "BlockScout"
#: lib/block_scout_web/templates/block_transaction/index.html.eex:36
#: lib/block_scout_web/templates/chain/show.html.eex:73
#: lib/block_scout_web/templates/layout/_topnav.html.eex:24
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:46
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:56
#: lib/block_scout_web/templates/transaction/index.html.eex:56
msgid "Transactions"
msgstr "Transactions"
@ -169,7 +169,7 @@ msgstr "From"
msgid "Overview"
msgstr "Overview"
#: lib/block_scout_web/views/transaction_view.ex:57
#: lib/block_scout_web/views/transaction_view.ex:53
msgid "Success"
msgstr "Success"
@ -221,8 +221,8 @@ msgstr "Showing %{count} Transactions"
#: lib/block_scout_web/templates/transaction/index.html.eex:16
#: lib/block_scout_web/templates/transaction/index.html.eex:35
#: lib/block_scout_web/templates/transaction/overview.html.eex:54
#: lib/block_scout_web/views/transaction_view.ex:55
#: lib/block_scout_web/views/transaction_view.ex:81
#: lib/block_scout_web/views/transaction_view.ex:51
#: lib/block_scout_web/views/transaction_view.ex:77
msgid "Pending"
msgstr "Pending"
@ -237,7 +237,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:21
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:48
#: lib/block_scout_web/templates/transaction_log/index.html.eex:10
#: lib/block_scout_web/views/transaction_view.ex:175
#: lib/block_scout_web/views/transaction_view.ex:171
msgid "Logs"
msgstr ""
@ -306,7 +306,7 @@ msgid "Showing #%{number}"
msgstr ""
#: lib/block_scout_web/templates/internal_transaction/_tile.html.eex:15
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:68
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:19
#: lib/block_scout_web/templates/transaction/_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/overview.html.eex:98
#: lib/block_scout_web/views/wei_helpers.ex:72
@ -335,7 +335,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:14
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:43
#: lib/block_scout_web/templates/transaction_internal_transaction/index.html.eex:10
#: lib/block_scout_web/views/transaction_view.ex:174
#: lib/block_scout_web/views/transaction_view.ex:170
msgid "Internal Transactions"
msgstr ""
@ -411,13 +411,13 @@ msgid "Avg Block Time"
msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:17
#: lib/block_scout_web/templates/layout/app.html.eex:24
#: lib/block_scout_web/templates/layout/app.html.eex:34
#: lib/block_scout_web/views/address_view.ex:99
msgid "Market Cap"
msgstr ""
#: lib/block_scout_web/templates/chain/show.html.eex:10
#: lib/block_scout_web/templates/layout/app.html.eex:25
#: lib/block_scout_web/templates/layout/app.html.eex:35
msgid "Price"
msgstr ""
@ -476,7 +476,7 @@ msgstr ""
msgid "Total Gas Used"
msgstr ""
#: lib/block_scout_web/views/transaction_view.ex:126
#: lib/block_scout_web/views/transaction_view.ex:122
msgid "Transaction"
msgstr ""
@ -489,7 +489,7 @@ msgstr ""
msgid "View All"
msgstr ""
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:69
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:20
#: lib/block_scout_web/templates/transaction/_tile.html.eex:23
#: lib/block_scout_web/templates/transaction/overview.html.eex:71
msgid "TX Fee"
@ -516,9 +516,9 @@ msgstr ""
#: lib/block_scout_web/templates/address_transaction/index.html.eex:162
#: lib/block_scout_web/templates/block/index.html.eex:20
#: lib/block_scout_web/templates/block_transaction/index.html.eex:50
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:78
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:70
#: lib/block_scout_web/templates/tokens/transfer/index.html.eex:34
#: lib/block_scout_web/templates/transaction/index.html.eex:66
#: lib/block_scout_web/templates/transaction/index.html.eex:70
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:24
msgid "Older"
msgstr ""
@ -577,7 +577,7 @@ msgid "Newer"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:124
#: lib/block_scout_web/views/transaction_view.ex:120
msgid "Contract Creation"
msgstr ""
@ -629,6 +629,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:108
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:53
#: lib/block_scout_web/templates/transaction/index.html.eex:53
msgid "Connection Lost, click to load newer transactions"
msgstr ""
@ -667,24 +668,19 @@ msgid "There are no transactions for this address."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:64
#: lib/block_scout_web/templates/transaction/_pending_tile.html.eex:15
#: lib/block_scout_web/views/address_view.ex:21
#: lib/block_scout_web/views/address_view.ex:55
msgid "Contract Address Pending"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:125
#: lib/block_scout_web/views/transaction_view.ex:121
msgid "Contract Call"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:37
msgid "Contract created by"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:43
#: lib/block_scout_web/templates/address/overview.html.eex:44
msgid "at"
msgstr ""
@ -786,7 +782,7 @@ msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/tokens/transfer/_token_transfer.html.eex:4
#: lib/block_scout_web/templates/transaction_token_transfer/_token_transfer.html.eex:4
#: lib/block_scout_web/views/transaction_view.ex:123
#: lib/block_scout_web/views/transaction_view.ex:119
msgid "Token Transfer"
msgstr ""
@ -805,17 +801,19 @@ msgstr[1] ""
#, elixir-format
#: lib/block_scout_web/templates/address_transaction/index.html.eex:103
#: lib/block_scout_web/templates/chain/show.html.eex:69
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:48
#: lib/block_scout_web/templates/transaction/index.html.eex:48
msgid "More transactions have come in"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/index.html.eex:57
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:58
#: lib/block_scout_web/templates/transaction/index.html.eex:58
msgid "Showing"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/transaction/index.html.eex:57
#: lib/block_scout_web/templates/transaction/index.html.eex:60
msgid "Validated Transactions"
msgstr ""
@ -836,7 +834,7 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/_tabs.html.eex:36
#: lib/block_scout_web/templates/transaction_token_transfer/index.html.eex:10
#: lib/block_scout_web/views/tokens/overview_view.ex:34
#: lib/block_scout_web/views/transaction_view.ex:173
#: lib/block_scout_web/views/transaction_view.ex:169
msgid "Token Transfers"
msgstr ""
@ -1050,12 +1048,12 @@ msgid "View Less Transfers"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/layout/app.html.eex:23
#: lib/block_scout_web/templates/layout/app.html.eex:33
msgid "Less than"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:47
#: lib/block_scout_web/views/transaction_view.ex:43
msgid "Max of"
msgstr ""
@ -1212,17 +1210,17 @@ msgid "Connection Lost, click to load newer blocks"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:56
#: lib/block_scout_web/views/transaction_view.ex:52
msgid "(Awaiting internal transactions for status)"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:60
#: lib/block_scout_web/views/transaction_view.ex:56
msgid "Error: %{reason}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/views/transaction_view.ex:58
#: lib/block_scout_web/views/transaction_view.ex:54
msgid "Error: (Awaiting internal transactions for reason)"
msgstr ""
@ -1272,3 +1270,24 @@ msgstr ""
#: lib/block_scout_web/templates/transaction/overview.html.eex:23
msgid "This transaction is pending confirmation."
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address/overview.html.eex:38
msgid "Created by"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/address_contract/index.html.eex:118
msgid "%{}"
msgstr ""
#, elixir-format
#: lib/block_scout_web/templates/pending_transaction/index.html.eex:60
msgid "Pending Transactions"
msgstr ""
#, elixir-format
#:
#: lib/block_scout_web/templates/address_token_balance/_token_balances.html.eex:26
msgid "Search tokens"
msgstr ""

@ -22,4 +22,21 @@ defmodule BlockScoutWeb.TransactionChannelTest do
assert false, "Expected message received nothing."
end
end
test "subscribed user is notified of new_pending_transaction event" do
topic = "transactions:new_pending_transaction"
@endpoint.subscribe(topic)
pending = insert(:transaction)
Notifier.handle_event({:chain_event, :transactions, [pending.hash]})
receive do
%Phoenix.Socket.Broadcast{topic: ^topic, event: "new_pending_transaction", payload: payload} ->
assert payload.transaction.hash == pending.hash
after
5_000 ->
assert false, "Expected message received nothing."
end
end
end

@ -3,15 +3,15 @@ defmodule BlockScoutWeb.AddressContractControllerTest do
import BlockScoutWeb.Router.Helpers, only: [address_contract_path: 3]
alias Explorer.Factory
alias Explorer.Chain.Hash
alias Explorer.ExchangeRates.Token
alias Explorer.Factory
describe "GET index/3" do
test "returns not found for unexistent address", %{conn: conn} do
unexistent_address_hash = Hash.to_string(Factory.address_hash())
test "returns not found for nonexistent address", %{conn: conn} do
nonexistent_address_hash = Hash.to_string(Factory.address_hash())
conn = get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, unexistent_address_hash))
conn = get(conn, address_contract_path(BlockScoutWeb.Endpoint, :index, nonexistent_address_hash))
assert html_response(conn, 404)
end

@ -35,6 +35,50 @@ defmodule BlockScoutWeb.BlockTransactionControllerTest do
assert 2 == Enum.count(conn.assigns.transactions)
end
test "does not return transactions for non-consensus block number", %{conn: conn} do
block = insert(:block, consensus: false)
:transaction
|> insert()
|> with_block(block)
conn = get(conn, block_transaction_path(conn, :index, block.number))
assert html_response(conn, 404)
end
test "returns transactions for consensus block hash", %{conn: conn} do
block = insert(:block, consensus: true)
:transaction
|> insert()
|> with_block(block)
conn = get(conn, block_transaction_path(conn, :index, block.hash))
assert html_response(conn, 200)
assert Enum.count(conn.assigns.transactions) == 1
end
test "returns transactions for non-consensus block hash", %{conn: conn} do
block = insert(:block, consensus: false)
:transaction
|> insert()
|> with_block(block)
conn = get(conn, block_transaction_path(conn, :index, block.hash))
assert html_response(conn, 200)
assert Enum.count(conn.assigns.transactions) == 1
end
test "does not return transactions for invalid block hash", %{conn: conn} do
conn = get(conn, block_transaction_path(conn, :index, "0x0"))
assert html_response(conn, 404)
end
test "does not return unrelated transactions", %{conn: conn} do
insert(:transaction)
block = insert(:block)

@ -3,6 +3,8 @@ defmodule BlockScoutWeb.ChainControllerTest do
import BlockScoutWeb.Router.Helpers, only: [chain_path: 2, block_path: 3, transaction_path: 3, address_path: 3]
alias Explorer.Chain.Block
describe "GET index/2" do
test "returns a welcome message", %{conn: conn} do
conn = get(conn, chain_path(BlockScoutWeb.Endpoint, :show))
@ -64,13 +66,29 @@ defmodule BlockScoutWeb.ChainControllerTest do
end
describe "GET q/2" do
test "finds a block by block number", %{conn: conn} do
test "finds a consensus block by block number", %{conn: conn} do
insert(:block, number: 37)
conn = get(conn, "/search?q=37")
assert redirected_to(conn) == block_path(conn, :show, "37")
end
test "does not find non-consensus block by number", %{conn: conn} do
%Block{number: number} = insert(:block, consensus: false)
conn = get(conn, "/search?q=#{number}")
assert conn.status == 404
end
test "finds non-consensus block by hash", %{conn: conn} do
%Block{hash: hash} = insert(:block, consensus: false)
conn = get(conn, "/search?q=#{hash}")
assert redirected_to(conn) == block_path(conn, :show, hash)
end
test "finds a transaction by hash", %{conn: conn} do
transaction =
:transaction

@ -3,15 +3,33 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
import Mox
alias Explorer.Chain.Hash
alias Explorer.Factory
setup :verify_on_exit!
describe "GET index/3" do
test "error for invalid address", %{conn: conn} do
test "returns not found for nonexistent address" do
nonexistent_address_hash = Hash.to_string(Factory.address_hash())
path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: nonexistent_address_hash)
conn =
build_conn()
|> put_req_header("x-requested-with", "xmlhttprequest")
|> get(path)
assert html_response(conn, 404)
end
test "error for invalid address" do
path = smart_contract_path(BlockScoutWeb.Endpoint, :index, hash: "0x00")
conn = get(conn, path)
conn =
build_conn()
|> put_req_header("x-requested-with", "xmlhttprequest")
|> get(path)
assert conn.status == 404
assert conn.status == 422
end
test "only responds to ajax requests", %{conn: conn} do
@ -44,7 +62,27 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
end
describe "GET show/3" do
test "error for invalid address", %{conn: conn} do
test "returns not found for nonexistent address" do
nonexistent_address_hash = Hash.to_string(Factory.address_hash())
path =
smart_contract_path(
BlockScoutWeb.Endpoint,
:show,
nonexistent_address_hash,
function_name: "get",
args: []
)
conn =
build_conn()
|> put_req_header("x-requested-with", "xmlhttprequest")
|> get(path)
assert html_response(conn, 404)
end
test "error for invalid address" do
path =
smart_contract_path(
BlockScoutWeb.Endpoint,
@ -54,9 +92,12 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
args: []
)
conn = get(conn, path)
conn =
build_conn()
|> put_req_header("x-requested-with", "xmlhttprequest")
|> get(path)
assert conn.status == 404
assert conn.status == 422
end
test "only responds to ajax requests", %{conn: conn} do
@ -77,7 +118,8 @@ defmodule BlockScoutWeb.SmartContractControllerTest do
end
test "fetch the function value from the blockchain" do
smart_contract = insert(:smart_contract)
address = insert(:contract_address)
smart_contract = insert(:smart_contract, address_hash: address.hash)
blockchain_get_function_mock()

@ -119,4 +119,16 @@ defmodule BlockScoutWeb.TransactionLogControllerTest do
assert %Token{} = conn.assigns.exchange_rate
end
test "loads for transcations that created a contract", %{conn: conn} do
contract_address = insert(:contract_address)
transaction =
:transaction
|> insert(to_address: nil)
|> with_contract_creation(contract_address)
conn = get(conn, transaction_log_path(conn, :index, transaction))
assert html_response(conn, 200)
end
end

@ -15,6 +15,22 @@ defmodule BlockScoutWeb.AddressPage do
css("[data-test='address_balance']")
end
def token_balance(count: count) do
css("[data-dropdown-token-balance-test]", count: count)
end
def token_balance_counter(text) do
css("[data-tokens-count]", text: "#{text} tokens")
end
def token_type(count: count) do
css("[data-token-type]", count: count)
end
def token_type_count(type: type, text: text) do
css("[data-number-of-tokens-by-type='#{type}']", text: text)
end
def address(%Address{hash: hash}) do
css("[data-address-hash='#{hash}']", text: to_string(hash))
end
@ -31,6 +47,18 @@ defmodule BlockScoutWeb.AddressPage do
click(session, css("[data-test='tokens_tab_link']"))
end
def click_balance_dropdown_toggle(session) do
click(session, css("[data-dropdown-toggle]"))
end
def fill_balance_dropdown_search(session, text) do
fill_in(session, css("[data-filter-dropdown-tokens]"), with: text)
end
def click_outside_of_the_dropdown(session) do
click(session, css("[data-test='outside_of_dropdown']"))
end
def click_token_transfers(session, %Token{contract_address_hash: contract_address_hash}) do
click(session, css("[data-test='token_transfers_#{contract_address_hash}']"))
end

@ -460,4 +460,82 @@ defmodule BlockScoutWeb.ViewingAddressesTest do
|> refute_has(AddressPage.token_transfers_expansion(transaction))
end
end
describe "viewing token balances" do
setup do
block = insert(:block)
lincoln = insert(:address, fetched_coin_balance: 5)
taft = insert(:address, fetched_coin_balance: 5)
contract_address = insert(:contract_address)
insert(:token, name: "atoken", symbol: "AT", contract_address: contract_address, type: "ERC-721")
transaction =
:transaction
|> insert(from_address: lincoln, to_address: contract_address)
|> with_block(block)
insert(
:token_transfer,
from_address: lincoln,
to_address: taft,
transaction: transaction,
token_contract_address: contract_address
)
insert(:token_balance, address: lincoln, token_contract_address_hash: contract_address.hash)
contract_address_2 = insert(:contract_address)
insert(:token, name: "token2", symbol: "T2", contract_address: contract_address_2, type: "ERC-20")
transaction_2 =
:transaction
|> insert(from_address: lincoln, to_address: contract_address_2)
|> with_block(block)
insert(
:token_transfer,
from_address: lincoln,
to_address: taft,
transaction: transaction_2,
token_contract_address: contract_address_2
)
insert(:token_balance, address: lincoln, token_contract_address_hash: contract_address_2.hash)
{:ok, lincoln: lincoln}
end
test "filter tokens balances by token name", %{session: session, lincoln: lincoln} do
session
|> AddressPage.visit_page(lincoln)
|> AddressPage.click_balance_dropdown_toggle()
|> AddressPage.fill_balance_dropdown_search("ato")
|> assert_has(AddressPage.token_balance(count: 1))
|> assert_has(AddressPage.token_type(count: 1))
|> assert_has(AddressPage.token_type_count(type: "ERC-721", text: "1"))
|> assert_has(AddressPage.token_balance_counter("1"))
end
test "filter token balances by token symbol", %{session: session, lincoln: lincoln} do
session
|> AddressPage.visit_page(lincoln)
|> AddressPage.click_balance_dropdown_toggle()
|> AddressPage.fill_balance_dropdown_search("T2")
|> assert_has(AddressPage.token_balance(count: 1))
|> assert_has(AddressPage.token_type(count: 1))
|> assert_has(AddressPage.token_type_count(type: "ERC-20", text: "1"))
|> assert_has(AddressPage.token_balance_counter("1"))
end
test "reset token balances filter when dropdown closes", %{session: session, lincoln: lincoln} do
session
|> AddressPage.visit_page(lincoln)
|> AddressPage.click_balance_dropdown_toggle()
|> AddressPage.fill_balance_dropdown_search("ato")
|> assert_has(AddressPage.token_balance_counter("1"))
|> AddressPage.click_outside_of_the_dropdown()
|> assert_has(AddressPage.token_balance_counter("2"))
end
end
end

@ -4,7 +4,7 @@ defmodule BlockScoutWeb.ViewingTransactionsTest do
use BlockScoutWeb.FeatureCase, async: true
alias Explorer.Chain.Wei
alias BlockScoutWeb.{AddressPage, Notifier, TransactionListPage, TransactionLogsPage, TransactionPage}
alias BlockScoutWeb.{AddressPage, TransactionListPage, TransactionLogsPage, TransactionPage}
setup do
block =
@ -109,18 +109,6 @@ defmodule BlockScoutWeb.ViewingTransactionsTest do
|> assert_has(TransactionPage.detail_hash(pending))
|> assert_has(TransactionPage.is_pending())
end
test "pending transactions live update once collated", %{session: session, pending: pending} do
session
|> TransactionPage.visit_page(pending)
transaction = with_block(pending)
Notifier.handle_event({:chain_event, :transactions, [transaction.hash]})
session
|> refute_has(TransactionPage.is_pending())
end
end
describe "viewing a transaction page" do

@ -10,21 +10,33 @@ defmodule BlockScoutWeb.ErrorHelpersTest do
]
}
test "error_tag/2 renders spans with default options" do
assert ErrorHelpers.error_tag(@changeset, :contract_code) == [
content_tag(:span, "has already been taken", class: "has-error")
]
end
describe "error_tag tests" do
test "error_tag/2 renders spans with default options" do
assert ErrorHelpers.error_tag(@changeset, :contract_code) == [
content_tag(:span, "has already been taken", class: "has-error")
]
end
test "error_tag/3 overrides default options" do
assert ErrorHelpers.error_tag(@changeset, :contract_code, class: "something-else") == [
content_tag(:span, "has already been taken", class: "something-else")
]
end
test "error_tag/3 overrides default options" do
assert ErrorHelpers.error_tag(@changeset, :contract_code, class: "something-else") == [
content_tag(:span, "has already been taken", class: "something-else")
]
test "error_tag/3 merges given options with default ones" do
assert ErrorHelpers.error_tag(@changeset, :contract_code, data_hidden: true) == [
content_tag(:span, "has already been taken", class: "has-error", data_hidden: true)
]
end
end
test "error_tag/3 merges given options with default ones" do
assert ErrorHelpers.error_tag(@changeset, :contract_code, data_hidden: true) == [
content_tag(:span, "has already been taken", class: "has-error", data_hidden: true)
]
describe "translate_error/1 tests" do
test "returns errors" do
assert ErrorHelpers.translate_error({"test", []}) == "test"
end
test "returns errors with count" do
assert ErrorHelpers.translate_error({"%{count} test", [count: 1]}) == "1 test"
end
end
end

@ -1,3 +1,9 @@
defmodule BlockScoutWeb.LayoutViewTest do
use BlockScoutWeb.ConnCase, async: true
alias BlockScoutWeb.LayoutView
test "configured_social_media_services/0" do
assert length(LayoutView.configured_social_media_services()) > 0
end
end

@ -143,7 +143,7 @@ defmodule BlockScoutWeb.TransactionViewTest do
|> insert(to_address: build(:address), created_contract_address: nil)
|> Repo.preload([:created_contract_address, :to_address])
assert TransactionView.to_address(transaction) == transaction.to_address
assert TransactionView.to_address_hash(transaction) == transaction.to_address_hash
end
end

@ -0,0 +1,17 @@
defmodule Phoenix.Param.Explorer.Chain.BlockTest do
use ExUnit.Case
import Explorer.Factory
test "without consensus" do
block = build(:block, consensus: false)
assert Phoenix.Param.to_param(block) == to_string(block.hash)
end
test "with consensus" do
block = build(:block, consensus: true)
assert Phoenix.Param.to_param(block) == to_string(block.number)
end
end

@ -17,7 +17,7 @@ defmodule EthereumJSONRPC do
"""
alias Explorer.Chain.Block
alias EthereumJSONRPC.{Blocks, Receipts, Subscription, Transactions, Transport, Variant}
alias EthereumJSONRPC.{Blocks, Receipts, Subscription, Transactions, Transport, Uncles, Variant}
@typedoc """
Truncated 20-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash encoded as a hexadecimal number in a
@ -63,6 +63,11 @@ defmodule EthereumJSONRPC do
{:transport, Transport.t()} | {:transport_options, Transport.options()} | {:variant, Variant.t()}
]
@typedoc """
If there are more blocks.
"""
@type next :: :end_of_chain | :more
@typedoc """
8 byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash of the proof-of-work.
"""
@ -206,6 +211,14 @@ defmodule EthereumJSONRPC do
@doc """
Fetches blocks by block number range.
"""
@spec fetch_blocks_by_range(Range.t(), json_rpc_named_arguments) ::
{:ok, next,
%{
blocks: Blocks.params(),
block_second_degree_relations: Uncles.params(),
transactions: Transactions.params()
}}
| {:error, [reason :: term, ...]}
def fetch_blocks_by_range(_first.._last = range, json_rpc_named_arguments) do
id_to_params =
range
@ -482,13 +495,18 @@ defmodule EthereumJSONRPC do
defp handle_get_blocks({:ok, results}, id_to_params) when is_list(results) do
with {:ok, next, blocks} <- reduce_results(results, id_to_params) do
elixir_blocks = Blocks.to_elixir(blocks)
elixir_uncles = Blocks.elixir_to_uncles(elixir_blocks)
elixir_transactions = Blocks.elixir_to_transactions(elixir_blocks)
blocks_params = Blocks.elixir_to_params(elixir_blocks)
block_second_degree_relations_params = Uncles.elixir_to_params(elixir_uncles)
transactions_params = Transactions.elixir_to_params(elixir_transactions)
blocks_params = Blocks.elixir_to_params(elixir_blocks)
{:ok, next,
%{
blocks: blocks_params,
block_second_degree_relations: block_second_degree_relations_params,
transactions: transactions_params
}}
end

@ -6,10 +6,23 @@ defmodule EthereumJSONRPC.Block do
import EthereumJSONRPC, only: [quantity_to_integer: 1, timestamp_to_datetime: 1]
alias EthereumJSONRPC
alias EthereumJSONRPC.Transactions
alias EthereumJSONRPC.{Transactions, Uncles}
@type elixir :: %{String.t() => non_neg_integer | DateTime.t() | String.t() | nil}
@type params :: %{
difficulty: pos_integer(),
gas_limit: non_neg_integer(),
gas_used: non_neg_integer(),
hash: EthereumJSONRPC.hash(),
miner_hash: EthereumJSONRPC.hash(),
nonce: EthereumJSONRPC.hash(),
number: non_neg_integer(),
parent_hash: EthereumJSONRPC.hash(),
size: non_neg_integer(),
timestamp: DateTime.t(),
total_difficulty: non_neg_integer(),
uncles: [EthereumJSONRPC.hash()]
}
@typedoc """
* `"author"` - `t:EthereumJSONRPC.address/0` that created the block. Aliased by `"miner"`.
@ -91,7 +104,8 @@ defmodule EthereumJSONRPC.Block do
parent_hash: "0x5b28c1bfd3a15230c9a46b399cd0f9a6920d432e85381cc6a140b06e8410112f",
size: 576,
timestamp: Timex.parse!("2017-12-15T21:03:30Z", "{ISO:Extended:Z}"),
total_difficulty: 340282366920938463463374607431465668165
total_difficulty: 340282366920938463463374607431465668165,
uncles: []
}
[Geth] `elixir` can be converted to params
@ -131,11 +145,12 @@ defmodule EthereumJSONRPC.Block do
parent_hash: "0xcd5b5c4cecd7f18a13fe974255badffd58e737dc67596d56bc01f063dd282e9e",
size: 542,
timestamp: Timex.parse!("2015-07-30T15:32:07Z", "{ISO:Extended:Z}"),
total_difficulty: 1039309006117
total_difficulty: 1039309006117,
uncles: []
}
"""
@spec elixir_to_params(elixir) :: map
@spec elixir_to_params(elixir) :: params
def elixir_to_params(
%{
"difficulty" => difficulty,
@ -147,7 +162,8 @@ defmodule EthereumJSONRPC.Block do
"parentHash" => parent_hash,
"size" => size,
"timestamp" => timestamp,
"totalDifficulty" => total_difficulty
"totalDifficulty" => total_difficulty,
"uncles" => uncles
} = elixir
) do
%{
@ -156,13 +172,14 @@ defmodule EthereumJSONRPC.Block do
gas_used: gas_used,
hash: hash,
miner_hash: miner_hash,
nonce: Map.get(elixir, "nonce", 0),
number: number,
parent_hash: parent_hash,
size: size,
timestamp: timestamp,
total_difficulty: total_difficulty
total_difficulty: total_difficulty,
uncles: uncles
}
|> Map.put(:nonce, Map.get(elixir, "nonce", 0))
end
@doc """
@ -249,6 +266,53 @@ defmodule EthereumJSONRPC.Block do
@spec elixir_to_transactions(elixir) :: Transactions.elixir()
def elixir_to_transactions(%{"transactions" => transactions}), do: transactions
@doc """
Get `t:EthereumJSONRPC.Uncles.elixir/0` from `t:elixir/0`.
Because an uncle can have multiple nephews, the `t:elixir/0` `"hash"` value is included as the `"nephewHash"` value.
iex> EthereumJSONRPC.Block.elixir_to_uncles(
...> %{
...> "author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "difficulty" => 340282366920938463463374607431768211454,
...> "extraData" => "0xd5830108048650617269747986312e32322e31826c69",
...> "gasLimit" => 6926030,
...> "gasUsed" => 269607,
...> "hash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "number" => 34,
...> "parentHash" => "0x106d528393159b93218dd410e2a778f083538098e46f1a44902aa67a164aed0b",
...> "receiptsRoot" => "0xf45ed4ab910504ffe231230879c86e32b531bb38a398a7c9e266b4a992e12dfb",
...> "sealFields" => [
...> "0x84120a71db",
...> "0xb8417ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501"
...> ],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "7ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501",
...> "size" => 1493,
...> "stateRoot" => "0x6eaa6281df37b9b010f4779affc25ee059088240547ce86cf7ca7b7acd952d4f",
...> "step" => "302674395",
...> "timestamp" => Timex.parse!("2017-12-15T21:06:15Z", "{ISO:Extended:Z}"),
...> "totalDifficulty" => 11569600475311907757754736652679816646147,
...> "transactions" => [],
...> "transactionsRoot" => "0x2c2e243e9735f6d0081ffe60356c0e4ec4c6a9064c68d10bf8091ff896f33087",
...> "uncles" => ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"]
...> }
...> )
[
%{
"hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
"nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47"
}
]
"""
@spec elixir_to_uncles(elixir) :: Uncles.elixir()
def elixir_to_uncles(%{"hash" => nephew_hash, "uncles" => uncles}) do
Enum.map(uncles, &%{"hash" => &1, "nephewHash" => nephew_hash})
end
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0` and the timestamps to `t:DateTime.t/0`

@ -4,9 +4,10 @@ defmodule EthereumJSONRPC.Blocks do
and [`eth_getBlockByNumber`](https://github.com/ethereum/wiki/wiki/JSON-RPC#eth_getblockbynumber) from batch requests.
"""
alias EthereumJSONRPC.{Block, Transactions}
alias EthereumJSONRPC.{Block, Transactions, Uncles}
@type elixir :: [Block.elixir()]
@type params :: [Block.params()]
@type t :: [Block.t()]
@doc """
@ -37,7 +38,7 @@ defmodule EthereumJSONRPC.Blocks do
...> "totalDifficulty" => 131072,
...> "transactions" => [],
...> "transactionsRoot" => "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421",
...> "uncles" => []
...> "uncles" => ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"]
...> }
...> ]
...> )
@ -53,12 +54,13 @@ defmodule EthereumJSONRPC.Blocks do
parent_hash: "0x0000000000000000000000000000000000000000000000000000000000000000",
size: 533,
timestamp: Timex.parse!("1970-01-01T00:00:00Z", "{ISO:Extended:Z}"),
total_difficulty: 131072
total_difficulty: 131072,
uncles: ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"]
}
]
"""
@spec elixir_to_params(elixir) :: [map]
@spec elixir_to_params(elixir) :: params()
def elixir_to_params(elixir) when is_list(elixir) do
Enum.map(elixir, &Block.elixir_to_params/1)
end
@ -142,11 +144,77 @@ defmodule EthereumJSONRPC.Blocks do
]
"""
@spec elixir_to_transactions(t) :: Transactions.elixir()
@spec elixir_to_transactions(elixir) :: Transactions.elixir()
def elixir_to_transactions(elixir) when is_list(elixir) do
Enum.flat_map(elixir, &Block.elixir_to_transactions/1)
end
@doc """
Extracts the `t:EthereumJSONRPC.Uncles.elixir/0` from the `t:elixir/0`.
iex> EthereumJSONRPC.Blocks.elixir_to_uncles([
...> %{
...> "author" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "difficulty" => 340282366920938463463374607431768211454,
...> "extraData" => "0xd5830108048650617269747986312e32322e31826c69",
...> "gasLimit" => 6926030,
...> "gasUsed" => 269607,
...> "hash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "logsBloom" => "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
...> "miner" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "number" => 34,
...> "parentHash" => "0x106d528393159b93218dd410e2a778f083538098e46f1a44902aa67a164aed0b",
...> "receiptsRoot" => "0xf45ed4ab910504ffe231230879c86e32b531bb38a398a7c9e266b4a992e12dfb",
...> "sealFields" => ["0x84120a71db",
...> "0xb8417ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501"],
...> "sha3Uncles" => "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347",
...> "signature" => "7ad0ecca535f81e1807dac338a57c7ccffd05d3e7f0687944650cd005360a192205df306a68eddfe216e0674c6b113050d90eff9b627c1762d43657308f986f501",
...> "size" => 1493,
...> "stateRoot" => "0x6eaa6281df37b9b010f4779affc25ee059088240547ce86cf7ca7b7acd952d4f",
...> "step" => "302674395",
...> "timestamp" => Timex.parse!("2017-12-15T21:06:15Z", "{ISO:Extended:Z}"),
...> "totalDifficulty" => 11569600475311907757754736652679816646147,
...> "transactions" => [
...> %{
...> "blockHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
...> "blockNumber" => 34,
...> "chainId" => 77,
...> "condition" => nil,
...> "creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
...> "from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
...> "gas" => 4700000,
...> "gasPrice" => 100000000000,
...> "hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> "input" => "0x6060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b0029",
...> "nonce" => 0,
...> "publicKey" => "0xe5d196ad4ceada719d9e592f7166d0c75700f6eab2e3c3de34ba751ea786527cb3f6eb96ad9fdfdb9989ff572df50f1c42ef800af9c5207a38b929aff969b5c9",
...> "r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
...> "raw" => "0xf9038d8085174876e8008347b7608080b903396060604052341561000f57600080fd5b336000806101000a81548173ffffffffffffffffffffffffffffffffffffffff021916908373ffffffffffffffffffffffffffffffffffffffff1602179055506102db8061005e6000396000f300606060405260043610610062576000357c0100000000000000000000000000000000000000000000000000000000900463ffffffff1680630900f01014610067578063445df0ac146100a05780638da5cb5b146100c9578063fdacd5761461011e575b600080fd5b341561007257600080fd5b61009e600480803573ffffffffffffffffffffffffffffffffffffffff16906020019091905050610141565b005b34156100ab57600080fd5b6100b3610224565b6040518082815260200191505060405180910390f35b34156100d457600080fd5b6100dc61022a565b604051808273ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200191505060405180910390f35b341561012957600080fd5b61013f600480803590602001909190505061024f565b005b60008060009054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff161415610220578190508073ffffffffffffffffffffffffffffffffffffffff1663fdacd5766001546040518263ffffffff167c010000000000000000000000000000000000000000000000000000000002815260040180828152602001915050600060405180830381600087803b151561020b57600080fd5b6102c65a03f1151561021c57600080fd5b5050505b5050565b60015481565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1681565b6000809054906101000a900473ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff1614156102ac57806001819055505b505600a165627a7a72305820a9c628775efbfbc17477a472413c01ee9b33881f550c59d21bee9928835c854b002981bda0ad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75a072caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
...> "standardV" => "0x0",
...> "to" => nil,
...> "transactionIndex" => 0,
...> "v" => "0xbd",
...> "value" => 0
...> }
...> ],
...> "transactionsRoot" => "0x2c2e243e9735f6d0081ffe60356c0e4ec4c6a9064c68d10bf8091ff896f33087",
...> "uncles" => ["0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"]
...> }
...> ])
[
%{
"hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
"nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47"
}
]
"""
@spec elixir_to_uncles(elixir) :: Uncles.elixir()
def elixir_to_uncles(elixir) do
Enum.flat_map(elixir, &Block.elixir_to_uncles/1)
end
@doc """
Decodes the stringly typed numerical fields to `t:non_neg_integer/0` and the timestamps to `t:DateTime.t/0`

@ -8,6 +8,7 @@ defmodule EthereumJSONRPC.Transactions do
alias EthereumJSONRPC.Transaction
@type elixir :: [Transaction.elixir()]
@type params :: [Transaction.params()]
@type t :: [Transaction.t()]
@doc """

@ -0,0 +1,39 @@
defmodule EthereumJSONRPC.Uncle do
@moduledoc """
[Uncle](https://github.com/ethereum/wiki/wiki/Glossary#ethereum-blockchain).
An uncle is a block that didn't make the main chain due to them being validated slightly behind what became the main
chain.
"""
@type elixir :: %{String.t() => EthereumJSONRPC.hash()}
@typedoc """
* `"hash"` - the hash of the uncle block.
* `"nephewHash"` - the hash of the nephew block that included `"hash` as an uncle.
"""
@type t :: %{String.t() => EthereumJSONRPC.hash()}
@type params :: %{nephew_hash: EthereumJSONRPC.hash(), uncle_hash: EthereumJSONRPC.hash()}
@doc """
Converts each entry in `t:elixir/0` to `t:params/0` used in `Explorer.Chain.Uncle.changeset/2`.
iex> EthereumJSONRPC.Uncle.elixir_to_params(
...> %{
...> "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
...> "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47"
...> }
...> )
%{
nephew_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47",
uncle_hash: "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311"
}
"""
@spec elixir_to_params(elixir) :: params
def elixir_to_params(%{"hash" => uncle_hash, "nephewHash" => nephew_hash})
when is_binary(uncle_hash) and is_binary(nephew_hash) do
%{nephew_hash: nephew_hash, uncle_hash: uncle_hash}
end
end

@ -0,0 +1,35 @@
defmodule EthereumJSONRPC.Uncles do
@moduledoc """
List of [uncles](https://github.com/ethereum/wiki/wiki/Glossary#ethereum-blockchain). Uncles are blocks that didn't
make the main chain due to them being validated slightly behind what became the main chain.
"""
alias EthereumJSONRPC.Uncle
@type elixir :: [Uncle.elixir()]
@type params :: [Uncle.params()]
@doc """
Converts each entry in `elixir` to params used in `Explorer.Chain.Uncle.changeset/2`.
iex> EthereumJSONRPC.Uncles.elixir_to_params(
...> [
...> %{
...> "hash" => "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
...> "nephewHash" => "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47"
...> }
...> ]
...> )
[
%{
uncle_hash: "0xe670ec64341771606e55d6b4ca35a1a6b75ee3d5145a99d05921026d15273311",
nephew_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b47"
}
]
"""
@spec elixir_to_params(elixir) :: params
def elixir_to_params(elixir) when is_list(elixir) do
Enum.map(elixir, &Uncle.elixir_to_params/1)
end
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.UncleTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Uncle
end

@ -0,0 +1,5 @@
defmodule EthereumJSONRPC.UnclesTest do
use ExUnit.Case, async: true
doctest EthereumJSONRPC.Uncles
end

@ -216,7 +216,8 @@ defmodule EthereumJSONRPCTest do
"v" => "0x0",
"value" => "0x0"
}
]
],
"uncles" => []
}
}
]}
@ -358,7 +359,8 @@ defmodule EthereumJSONRPCTest do
"size" => "0x0",
"timestamp" => "0x0",
"totalDifficulty" => "0x0",
"transactions" => []
"transactions" => [],
"uncles" => []
},
jsonrpc: "2.0"
},

@ -585,6 +585,50 @@ defmodule Explorer.Chain do
end
end
@doc """
Converts `t:Explorer.Chain.Block.t/0` `hash` to the `t:Explorer.Chain.Block.t/0` with that `hash`.
Unlike `number_to_block/1`, both consensus and non-consensus blocks can be returned when looked up by `hash`.
Returns `{:ok, %Explorer.Chain.Block{}}` if found
iex> %Block{hash: hash} = insert(:block, consensus: false)
iex> {:ok, %Explorer.Chain.Block{hash: found_hash}} = Explorer.Chain.hash_to_block(hash)
iex> found_hash == hash
true
Returns `{:error, :not_found}` if not found
iex> {:ok, hash} = Explorer.Chain.string_to_block_hash(
...> "0x9fc76417374aa880d4449a1f7f31ec597f00b1f6f3dd2d66f4c9c6c445836d8b"
...> )
iex> Explorer.Chain.hash_to_block(hash)
{:error, :not_found}
## Options
* `:necessity_by_association` - use to load `t:association/0` as `:required` or `:optional`. If an association is
`:required`, and the `t:Explorer.Chain.Block.t/0` has no associated record for that association, then the
`t:Explorer.Chain.Block.t/0` will not be included in the page `entries`.
"""
@spec hash_to_block(Hash.Full.t(), [necessity_by_association_option]) :: {:ok, Block.t()} | {:error, :not_found}
def hash_to_block(%Hash{byte_count: unquote(Hash.Full.byte_count())} = hash, options \\ []) when is_list(options) do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
Block
|> where(hash: ^hash)
|> join_associations(necessity_by_association)
|> Repo.one()
|> case do
nil ->
{:error, :not_found}
block ->
{:ok, block}
end
end
@doc """
Converts the `Explorer.Chain.Hash.t:t/0` to `iodata` representation that can be written efficiently to users.
@ -932,6 +976,37 @@ defmodule Explorer.Chain do
)
end
@doc """
Returns a stream of all `t:Explorer.Chain.Block.t/0` `hash`es that are marked as unfetched in
`t:Explorer.Chain.Block.SecondDegreeRelation.t/0`.
When a block is fetched, its uncles are transformed into `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` and can be
returned. Once the uncle is imported its corresponding `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`
`uncle_fetched_at` will be set and it won't be returned anymore.
"""
@spec stream_unfetched_uncle_hashes(
initial :: accumulator,
reducer :: (entry :: Hash.Full.t(), accumulator -> accumulator)
) :: {:ok, accumulator}
when accumulator: term()
def stream_unfetched_uncle_hashes(initial, reducer) when is_function(reducer, 2) do
Repo.transaction(
fn ->
query =
from(bsdr in Block.SecondDegreeRelation,
where: is_nil(bsdr.uncle_fetched_at),
select: bsdr.uncle_hash,
group_by: bsdr.uncle_hash
)
query
|> Repo.stream(timeout: :infinity)
|> Enum.reduce(initial, reducer)
end,
timeout: :infinity
)
end
@doc """
The number of `t:Explorer.Chain.Log.t/0`.
@ -1062,7 +1137,7 @@ defmodule Explorer.Chain do
end
@doc """
Finds `t:Explorer.Chain.Block.t/0` with `number`
Finds consensus `t:Explorer.Chain.Block.t/0` with `number`.
## Options
@ -1077,7 +1152,7 @@ defmodule Explorer.Chain do
necessity_by_association = Keyword.get(options, :necessity_by_association, %{})
Block
|> where(number: ^number)
|> where(consensus: true, number: ^number)
|> join_associations(necessity_by_association)
|> Repo.one()
|> case do

@ -7,9 +7,9 @@ defmodule Explorer.Chain.Block do
use Explorer.Schema
alias Explorer.Chain.{Address, Gas, Hash, Transaction}
alias Explorer.Chain.{Address, Block.SecondDegreeRelation, Gas, Hash, Transaction}
@required_attrs ~w(difficulty gas_limit gas_used hash miner_hash nonce number parent_hash size timestamp
@required_attrs ~w(consensus difficulty gas_limit gas_used hash miner_hash nonce number parent_hash size timestamp
total_difficulty)a
@typedoc """
@ -25,6 +25,9 @@ defmodule Explorer.Chain.Block do
@type block_number :: non_neg_integer()
@typedoc """
* `consensus`
* `true` - this is a block on the longest consensus agreed upon chain.
* `false` - this is an uncle block from a fork.
* `difficulty` - how hard the block was to mine.
* `gas_limit` - If the total number of gas used by the computation spawned by the transaction, including the
original message and any sub-messages that may be triggered, is less than or equal to the gas limit, then the
@ -43,6 +46,7 @@ defmodule Explorer.Chain.Block do
* `transactions` - the `t:Explorer.Chain.Transaction.t/0` in this block.
"""
@type t :: %__MODULE__{
consensus: boolean(),
difficulty: difficulty(),
gas_limit: Gas.t(),
gas_used: Gas.t(),
@ -60,6 +64,7 @@ defmodule Explorer.Chain.Block do
@primary_key {:hash, Hash.Full, autogenerate: false}
schema "blocks" do
field(:consensus, :boolean)
field(:difficulty, :decimal)
field(:gas_limit, :decimal)
field(:gas_used, :decimal)
@ -72,7 +77,15 @@ defmodule Explorer.Chain.Block do
timestamps()
belongs_to(:miner, Address, foreign_key: :miner_hash, references: :hash, type: Hash.Address)
has_many(:nephew_relations, SecondDegreeRelation, foreign_key: :uncle_hash)
has_many(:nephews, through: [:nephew_relations, :nephew])
belongs_to(:parent, __MODULE__, foreign_key: :parent_hash, references: :hash, type: Hash.Full)
has_many(:uncle_relations, SecondDegreeRelation, foreign_key: :nephew_hash)
has_many(:uncles, through: [:uncle_relations, :uncle])
has_many(:transactions, Transaction)
end

@ -0,0 +1,64 @@
defmodule Explorer.Chain.Block.SecondDegreeRelation do
@moduledoc """
A [second-degree relative](https://en.wikipedia.org/wiki/Second-degree_relative) is a relative where the share
point is the parent's parent block in the chain.
For Ethereum, nephews are rewarded for included their uncles.
Uncles occur when a Proof-of-Work proof is completed slightly late, but before the next block is completes, so the
network knows about the late proof and can credit as an uncle in the next block.
This schema is the join schema between the `nephew` and the `uncle` it is is including the `uncle`. The actual
`uncle` block is still a normal `t:Explorer.Chain.Block.t/0`.
"""
use Explorer.Schema
alias Explorer.Chain.{Block, Hash}
@optional_fields ~w(uncle_fetched_at)a
@required_fields ~w(nephew_hash uncle_hash)a
@allowed_fields @optional_fields ++ @required_fields
@typedoc """
* `nephew` - `t:Explorer.Chain.Block.t/0` that included `hash` as an uncle.
* `nephew_hash` - foreign key for `nephew_block`.
* `uncle` - the uncle block. Maybe `nil` when `uncle_fetched_at` is `nil`. It could not be `nil` if the
`uncle_hash` was fetched for some other reason already.
* `uncle_fetched_at` - when `t:Explorer.Chain.Block.t/0` for `uncle_hash` was confirmed as fetched.
* `uncle_hash` - foreign key for `uncle`.
"""
@type t ::
%__MODULE__{
nephew: %Ecto.Association.NotLoaded{} | Block.t(),
nephew_hash: Hash.Full.t(),
uncle: %Ecto.Association.NotLoaded{} | Block.t() | nil,
uncle_fetched_at: nil,
uncle_hash: Hash.Full.t()
}
| %__MODULE__{
nephew: %Ecto.Association.NotLoaded{} | Block.t(),
nephew_hash: Hash.Full.t(),
uncle: %Ecto.Association.NotLoaded{} | Block.t(),
uncle_fetched_at: DateTime.t(),
uncle_hash: Hash.Full.t()
}
@primary_key false
schema "block_second_degree_relations" do
field(:uncle_fetched_at, :utc_datetime)
belongs_to(:nephew, Block, foreign_key: :nephew_hash, references: :hash, type: Hash.Full)
belongs_to(:uncle, Block, foreign_key: :uncle_hash, references: :hash, type: Hash.Full)
end
def changeset(%__MODULE__{} = uncle, params) do
uncle
|> cast(params, @allowed_fields)
|> validate_required(@required_fields)
|> foreign_key_constraint(:nephew_hash)
|> unique_constraint(:nephew_hash, name: :uncle_hash_to_nephew_hash)
|> unique_constraint(:nephew_hash, name: :unfetched_uncles)
|> unique_constraint(:uncle_hash, name: :nephew_hash_to_uncle_hash)
end
end

@ -39,6 +39,10 @@ defmodule Explorer.Chain.Import do
required(:params) => params,
optional(:timeout) => timeout
}
@type block_second_degree_relations_options :: %{
required(:params) => params,
optional(:timeout) => timeout
}
@type internal_transactions_options :: %{
required(:params) => params,
optional(:timeout) => timeout
@ -66,6 +70,10 @@ defmodule Explorer.Chain.Import do
optional(:on_conflict) => :nothing | :replace_all,
optional(:timeout) => timeout
}
@type transaction_forks_options :: %{
required(:params) => params,
optional(:timeout) => timeout
}
@type token_balances_options :: %{
required(:params) => params,
optional(:timeout) => timeout
@ -74,6 +82,7 @@ defmodule Explorer.Chain.Import do
optional(:addresses) => addresses_options,
optional(:balances) => balances_options,
optional(:blocks) => blocks_options,
optional(:block_second_degree_relations) => block_second_degree_relations_options,
optional(:broadcast) => boolean,
optional(:internal_transactions) => internal_transactions_options,
optional(:logs) => logs_options,
@ -82,7 +91,8 @@ defmodule Explorer.Chain.Import do
optional(:token_transfers) => token_transfers_options,
optional(:tokens) => tokens_options,
optional(:token_balances) => token_balances_options,
optional(:transactions) => transactions_options
optional(:transactions) => transactions_options,
optional(:transaction_forks) => transaction_forks_options
}
@type all_result ::
{:ok,
@ -92,6 +102,9 @@ defmodule Explorer.Chain.Import do
%{required(:address_hash) => Hash.Address.t(), required(:block_number) => Block.block_number()}
],
optional(:blocks) => [Block.t()],
optional(:block_second_degree_relations) => [
%{required(:nephew_hash) => Hash.Full.t(), required(:uncle_hash) => Hash.Full.t()}
],
optional(:internal_transactions) => [
%{required(:index) => non_neg_integer(), required(:transaction_hash) => Hash.Full.t()}
],
@ -100,7 +113,10 @@ defmodule Explorer.Chain.Import do
optional(:token_transfers) => [TokenTransfer.t()],
optional(:tokens) => [Token.t()],
optional(:token_balances) => [TokenBalance.t()],
optional(:transactions) => [Hash.Full.t()]
optional(:transactions) => [Hash.Full.t()],
optional(:transaction_forks) => [
%{required(:uncle_hash) => Hash.Full.t(), required(:hash) => Hash.Full.t()}
]
}}
| {:error, [Changeset.t()]}
| {:error, step :: Ecto.Multi.name(), failed_value :: any(),
@ -115,28 +131,32 @@ defmodule Explorer.Chain.Import do
@insert_addresses_timeout 60_000
@insert_balances_timeout 60_000
@insert_blocks_timeout 60_000
@insert_block_second_degree_relations_timeout 60_000
@insert_internal_transactions_timeout 60_000
@insert_logs_timeout 60_000
@insert_token_transfers_timeout 60_000
@insert_token_balances_timeout 60_000
@insert_tokens_timeout 60_000
@insert_transactions_timeout 60_000
@insert_transaction_forks_timeout 60_000
@doc """
Bulk insert all data stored in the `Explorer`.
The import returns the unique key(s) for each type of record inserted.
| Key | Value Type | Value Description |
|--------------------------|-------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------|
| `:addresses` | `[Explorer.Chain.Address.t()]` | List of `t:Explorer.Chain.Address.t/0`s |
| `:balances` | `[%{address_hash: Explorer.Chain.Hash.t(), block_number: Explorer.Chain.Block.block_number()}]` | List of `t:Explorer.Chain.Address.t/0`s |
| `:blocks` | `[Explorer.Chain.Block.t()]` | List of `t:Explorer.Chain.Block.t/0`s |
| `:internal_transactions` | `[%{index: non_neg_integer(), transaction_hash: Explorer.Chain.Hash.t()}]` | List of maps of the `t:Explorer.Chain.InternalTransaction.t/0` `index` and `transaction_hash` |
| `:logs` | `[Explorer.Chain.Log.t()]` | List of `t:Explorer.Chain.Log.t/0`s |
| `:token_transfers` | `[Explorer.Chain.TokenTransfer.t()]` | List of `t:Explor.Chain.TokenTransfer.t/0`s |
| `:tokens` | `[Explorer.Chain.Token.t()]` | List of `t:Explorer.Chain.token.t/0`s |
| `:transactions` | `[Explorer.Chain.Hash.t()]` | List of `t:Explorer.Chain.Transaction.t/0` `hash` |
| Key | Value Type | Value Description |
|----------------------------------|-------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------|
| `:addresses` | `[Explorer.Chain.Address.t()]` | List of `t:Explorer.Chain.Address.t/0`s |
| `:balances` | `[%{address_hash: Explorer.Chain.Hash.t(), block_number: Explorer.Chain.Block.block_number()}]` | List of `t:Explorer.Chain.Address.t/0`s |
| `:blocks` | `[Explorer.Chain.Block.t()]` | List of `t:Explorer.Chain.Block.t/0`s |
| `:internal_transactions` | `[%{index: non_neg_integer(), transaction_hash: Explorer.Chain.Hash.t()}]` | List of maps of the `t:Explorer.Chain.InternalTransaction.t/0` `index` and `transaction_hash` |
| `:logs` | `[Explorer.Chain.Log.t()]` | List of `t:Explorer.Chain.Log.t/0`s |
| `:token_transfers` | `[Explorer.Chain.TokenTransfer.t()]` | List of `t:Explor.Chain.TokenTransfer.t/0`s |
| `:tokens` | `[Explorer.Chain.Token.t()]` | List of `t:Explorer.Chain.token.t/0`s |
| `:transactions` | `[Explorer.Chain.Hash.t()]` | List of `t:Explorer.Chain.Transaction.t/0` `hash` |
| `:transaction_forks` | `[%{uncle_hash: Explorer.Chain.Hash.t(), hash: Explorer.Chain.Hash.t()}]` | List of maps of the `t:Explorer.Chain.Transaction.Fork.t/0` `uncle_hash` and `hash` |
| `:block_second_degree_relations` | `[%{uncle_hash: Explorer.Chain.Hash.t(), nephew_hash: Explorer.Chain.Hash.t()]` | List of maps of the `t:Explorer.Chain.Block.SecondDegreeRelation.t/0` `uncle_hash` and `nephew_hash` |
The params for each key are validated using the corresponding `Ecto.Schema` module's `changeset/2` function. If there
are errors, they are returned in `Ecto.Changeset.t`s, so that the original, invalid value can be reconstructed for any
@ -165,7 +185,10 @@ defmodule Explorer.Chain.Import do
* `:blocks`
* `:params` - `list` of params for `Explorer.Chain.Block.changeset/2`.
* `:timeout` - the timeout for inserting all blocks. Defaults to `#{@insert_blocks_timeout}` milliseconds.
* `:broacast` - Boolean flag indicating whether or not to broadcast the event.
* `:block_second_degree_relations`
* `:params` - `list` of params `for `Explorer.Chain.Block.SecondDegreeRelation.changeset/2`.
* `:timeout` - the timeout for inserting all uncles found in the params list.
* `:broadcast` - Boolean flag indicating whether or not to broadcast the event.
* `:internal_transactions`
* `:params` - `list` of params for `Explorer.Chain.InternalTransaction.changeset/2`.
* `:timeout` - the timeout for inserting all internal transactions. Defaults to
@ -198,6 +221,7 @@ defmodule Explorer.Chain.Import do
* `:token_balances`
* `:params` - `list` of params for `Explorer.Chain.TokenBalance.changeset/2`
* `:timeout` - the timeout for `Repo.transaction`. Defaults to `#{@transaction_timeout}` milliseconds.
"""
@spec all(all_options()) :: all_result()
def all(options) when is_map(options) do
@ -277,12 +301,14 @@ defmodule Explorer.Chain.Import do
addresses: Address,
balances: CoinBalance,
blocks: Block,
block_second_degree_relations: Block.SecondDegreeRelation,
internal_transactions: InternalTransaction,
logs: Log,
token_transfers: TokenTransfer,
token_balances: TokenBalance,
tokens: Token,
transactions: Transaction
transactions: Transaction,
transaction_forks: Transaction.Fork
}
defp ecto_schema_module_to_changes_list_map_to_multi(ecto_schema_module_to_changes_list_map, options)
@ -294,7 +320,9 @@ defmodule Explorer.Chain.Import do
|> run_addresses(ecto_schema_module_to_changes_list_map, full_options)
|> run_balances(ecto_schema_module_to_changes_list_map, full_options)
|> run_blocks(ecto_schema_module_to_changes_list_map, full_options)
|> run_block_second_degree_relations(ecto_schema_module_to_changes_list_map, full_options)
|> run_transactions(ecto_schema_module_to_changes_list_map, full_options)
|> run_transaction_forks(ecto_schema_module_to_changes_list_map, full_options)
|> run_internal_transactions(ecto_schema_module_to_changes_list_map, full_options)
|> run_logs(ecto_schema_module_to_changes_list_map, full_options)
|> run_tokens(ecto_schema_module_to_changes_list_map, full_options)
@ -350,7 +378,8 @@ defmodule Explorer.Chain.Import do
%{Block => blocks_changes} ->
timestamps = Map.fetch!(options, :timestamps)
Multi.run(multi, :blocks, fn _ ->
multi
|> Multi.run(:blocks, fn _ ->
insert_blocks(
blocks_changes,
%{
@ -359,6 +388,16 @@ defmodule Explorer.Chain.Import do
}
)
end)
|> Multi.run(:uncle_fetched_block_second_degree_relations, fn %{blocks: blocks} when is_list(blocks) ->
update_block_second_degree_relations(
blocks,
%{
timeout:
options[:block_second_degree_relations][:timeout] || @insert_block_second_degree_relations_timeout,
timestamps: timestamps
}
)
end)
_ ->
multi
@ -388,6 +427,27 @@ defmodule Explorer.Chain.Import do
end
end
defp run_transaction_forks(multi, ecto_schema_module_to_changes_list_map, options)
when is_map(ecto_schema_module_to_changes_list_map) and is_map(options) do
case ecto_schema_module_to_changes_list_map do
%{Transaction.Fork => transaction_fork_changes} ->
%{timestamps: timestamps} = options
Multi.run(multi, :transaction_forks, fn _ ->
insert_transaction_forks(
transaction_fork_changes,
%{
timeout: options[:transaction_forks][:timeout] || @insert_transaction_forks_timeout,
timestamps: timestamps
}
)
end)
_ ->
multi
end
end
defp run_internal_transactions(multi, ecto_schema_module_to_changes_list_map, options)
when is_map(ecto_schema_module_to_changes_list_map) and is_map(options) do
case ecto_schema_module_to_changes_list_map do
@ -507,6 +567,25 @@ defmodule Explorer.Chain.Import do
end
end
defp run_block_second_degree_relations(multi, ecto_schema_module_to_changes_list, options)
when is_map(ecto_schema_module_to_changes_list) and is_map(options) do
case ecto_schema_module_to_changes_list do
%{Block.SecondDegreeRelation => block_second_degree_relations_changes} ->
Multi.run(multi, :block_second_degree_relations, fn _ ->
insert_block_second_degree_relations(
block_second_degree_relations_changes,
%{
timeout:
options[:block_second_degree_relations][:timeout] || @insert_block_second_degree_relations_timeout
}
)
end)
_ ->
multi
end
end
@spec insert_addresses([%{hash: Hash.Address.t()}], %{
required(:timeout) => timeout,
required(:timestamps) => timestamps
@ -645,7 +724,7 @@ defmodule Explorer.Chain.Import do
{:ok, blocks} =
insert_changes_list(
ordered_changes_list,
conflict_target: :number,
conflict_target: :hash,
on_conflict: :replace_all,
for: Block,
returning: true,
@ -656,6 +735,32 @@ defmodule Explorer.Chain.Import do
{:ok, blocks}
end
@spec insert_block_second_degree_relations([map()], %{required(:timeout) => timeout}) ::
{:ok, %{nephew_hash: Hash.Full.t(), uncle_hash: Hash.Full.t()}} | {:error, [Changeset.t()]}
defp insert_block_second_degree_relations(changes_list, %{timeout: timeout}) when is_list(changes_list) do
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.nephew_hash, &1.uncle_hash})
insert_changes_list(ordered_changes_list,
conflict_target: [:nephew_hash, :uncle_hash],
on_conflict:
from(
block_second_degree_relation in Block.SecondDegreeRelation,
update: [
set: [
uncle_fetched_at:
fragment("LEAST(?, EXCLUDED.uncle_fetched_at)", block_second_degree_relation.uncle_fetched_at)
]
]
),
for: Block.SecondDegreeRelation,
returning: [:nephew_hash, :uncle_hash],
timeout: timeout,
# block_second_degree_relations doesn't have timestamps
timestamps: %{}
)
end
@spec insert_internal_transactions([map], %{required(:timeout) => timeout, required(:timestamps) => timestamps}) ::
{:ok, [%{index: non_neg_integer, transaction_hash: Hash.t()}]}
| {:error, [Changeset.t()]}
@ -828,6 +933,34 @@ defmodule Explorer.Chain.Import do
{:ok, for(transaction <- transactions, do: transaction.hash)}
end
@spec insert_transaction_forks([map()], %{
required(:timeout) => timeout,
required(:timestamps) => timestamps
}) :: {:ok, [%{uncle_hash: Hash.t(), hash: Hash.t()}]}
defp insert_transaction_forks(changes_list, %{timeout: timeout, timestamps: timestamps})
when is_list(changes_list) do
# order so that row ShareLocks are grabbed in a consistent order
ordered_changes_list = Enum.sort_by(changes_list, &{&1.uncle_hash, &1.hash})
insert_changes_list(
ordered_changes_list,
conflict_target: [:uncle_hash, :index],
on_conflict:
from(
transaction_fork in Transaction.Fork,
update: [
set: [
hash: fragment("EXCLUDED.hash")
]
]
),
for: Transaction.Fork,
returning: [:uncle_hash, :hash],
timeout: timeout,
timestamps: timestamps
)
end
defp insert_changes_list(changes_list, options) when is_list(changes_list) do
ecto_schema_module = Keyword.fetch!(options, :for)
@ -843,6 +976,34 @@ defmodule Explorer.Chain.Import do
{:ok, inserted}
end
defp update_block_second_degree_relations(blocks, %{timeout: timeout, timestamps: %{updated_at: updated_at}})
when is_list(blocks) do
ordered_uncle_hashes =
blocks
|> MapSet.new(& &1.hash)
|> Enum.sort()
query =
from(
bsdr in Block.SecondDegreeRelation,
where: bsdr.uncle_hash in ^ordered_uncle_hashes,
update: [
set: [
uncle_fetched_at: ^updated_at
]
]
)
try do
{_, result} = Repo.update_all(query, [], timeout: timeout)
{:ok, result}
rescue
postgrex_error in Postgrex.Error ->
{:error, %{exception: postgrex_error, uncle_hashes: ordered_uncle_hashes}}
end
end
defp update_transactions(internal_transactions, %{
timeout: timeout,
timestamps: timestamps

@ -20,7 +20,7 @@ defmodule Explorer.Chain.Transaction do
Wei
}
alias Explorer.Chain.Transaction.Status
alias Explorer.Chain.Transaction.{Fork, Status}
@optional_attrs ~w(block_hash block_number created_contract_address_hash cumulative_gas_used error gas_used index
internal_transactions_indexed_at status to_address_hash)a
@ -66,9 +66,12 @@ defmodule Explorer.Chain.Transaction do
@type wei_per_gas :: Wei.t()
@typedoc """
* `block` - the block in which this transaction was mined/validated. `nil` when transaction is pending.
* `block_hash` - `block` foreign key. `nil` when transaction is pending.
* `block_number` - Denormalized `block` `number`. `nil` when transaction is pending.
* `block` - the block in which this transaction was mined/validated. `nil` when transaction is pending or has only
been collated into one of the `uncles` in one of the `forks`.
* `block_hash` - `block` foreign key. `nil` when transaction is pending or has only been collated into one of the
`uncles` in one of the `forks`.
* `block_number` - Denormalized `block` `number`. `nil` when transaction is pending or has only been collated into
one of the `uncles` in one of the `forks`.
* `created_contract_address` - belongs_to association to `address` corresponding to `created_contract_address_hash`.
* `created_contract_address_hash` - Denormalized `internal_transaction` `created_contract_address_hash`
populated only when `to_address_hash` is nil.
@ -76,13 +79,16 @@ defmodule Explorer.Chain.Transaction do
`transaction`'s `index`. `nil` when transaction is pending.
* `error` - the `error` from the last `t:Explorer.Chain.InternalTransaction.t/0` in `internal_transactions` that
caused `status` to be `:error`. Only set after `internal_transactions_index_at` is set AND if there was an error.
* `forks` - copies of this transactions that were collated into `uncles` not on the primary consensus of the chain.
* `from_address` - the source of `value`
* `from_address_hash` - foreign key of `from_address`
* `gas` - Gas provided by the sender
* `gas_price` - How much the sender is willing to pay for `gas`
* `gas_used` - the gas used for just `transaction`. `nil` when transaction is pending.
* `gas_used` - the gas used for just `transaction`. `nil` when transaction is pending or has only been collated into
one of the `uncles` in one of the `forks`.
* `hash` - hash of contents of this transaction
* `index` - index of this transaction in `block`. `nil` when transaction is pending.
* `index` - index of this transaction in `block`. `nil` when transaction is pending or has only been collated into
one of the `uncles` in one of the `forks`.
* `input`- data sent along with the transaction
* `internal_transactions` - transactions (value transfers) created while executing contract used for this
transaction
@ -93,9 +99,11 @@ defmodule Explorer.Chain.Transaction do
the X coordinate of a point R, modulo the curve order n.
* `s` - The S field of the signature. The (r, s) is the normal output of an ECDSA signature, where r is computed as
the X coordinate of a point R, modulo the curve order n.
* `status` - whether the transaction was successfully mined or failed. `nil` when transaction is pending.
* `status` - whether the transaction was successfully mined or failed. `nil` when transaction is pending or has only
been collated into one of the `uncles` in one of the `forks.
* `to_address` - sink of `value`
* `to_address_hash` - `to_address` foreign key
* `uncles` - uncle blocks where `forks` were collated
* `v` - The V field of the signature.
* `value` - wei transferred from `from_address` to `to_address`
"""
@ -107,6 +115,7 @@ defmodule Explorer.Chain.Transaction do
created_contract_address_hash: Hash.Address.t() | nil,
cumulative_gas_used: Gas.t() | nil,
error: String.t() | nil,
forks: %Ecto.Association.NotLoaded{} | [Fork.t()],
from_address: %Ecto.Association.NotLoaded{} | Address.t(),
from_address_hash: Hash.Address.t(),
gas: Gas.t(),
@ -124,6 +133,7 @@ defmodule Explorer.Chain.Transaction do
status: Status.t() | nil,
to_address: %Ecto.Association.NotLoaded{} | Address.t() | nil,
to_address_hash: Hash.Address.t() | nil,
uncles: %Ecto.Association.NotLoaded{} | [Block.t()],
v: v(),
value: Wei.t()
}
@ -149,6 +159,7 @@ defmodule Explorer.Chain.Transaction do
timestamps()
belongs_to(:block, Block, foreign_key: :block_hash, references: :hash, type: Hash.Full)
has_many(:forks, Fork, foreign_key: :hash)
belongs_to(
:from_address,
@ -170,6 +181,8 @@ defmodule Explorer.Chain.Transaction do
type: Hash.Address
)
has_many(:uncles, through: [:forks, :uncle])
belongs_to(
:created_contract_address,
Address,

@ -0,0 +1,63 @@
defmodule Explorer.Chain.Transaction.Fork do
@moduledoc """
A transaction fork has the same `hash` as a `t:Explorer.Chain.Transaction.t/0`, but associates that `hash` with a
non-consensus uncle `t:Explorer.Chain.Block.t/0` instead of the consensus block linked in the
`t:Explorer.Chain.Transaction.t/0` `block_hash`.
"""
use Explorer.Schema
alias Explorer.Chain.{Block, Hash, Transaction}
@optional_attrs ~w()a
@required_attrs ~w(hash index uncle_hash)a
@allowed_attrs @optional_attrs ++ @required_attrs
@typedoc """
* `hash` - hash of contents of this transaction
* `index` - index of this transaction in `uncle`.
* `transaction` - the data shared between all forks and the consensus transaction.
* `uncle` - the block in which this transaction was mined/validated.
* `uncle_hash` - `uncle` foreign key.
"""
@type t :: %__MODULE__{
hash: Hash.t(),
index: Transaction.transaction_index(),
transaction: %Ecto.Association.NotLoaded{} | Transaction.t(),
uncle: %Ecto.Association.NotLoaded{} | Block.t(),
uncle_hash: Hash.t()
}
@primary_key false
schema "transaction_forks" do
field(:index, :integer)
timestamps()
belongs_to(:transaction, Transaction, foreign_key: :hash, references: :hash, type: Hash.Full)
belongs_to(:uncle, Block, foreign_key: :uncle_hash, references: :hash, type: Hash.Full)
end
@doc """
All fields are required for transaction fork
iex> changeset = Fork.changeset(
...> %Fork{},
...> %{
...> hash: "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
...> index: 1,
...> uncle_hash: "0xe52d77084cab13a4e724162bcd8c6028e5ecfaa04d091ee476e96b9958ed6b48"
...> }
...> )
iex> changeset.valid?
true
"""
def changeset(%__MODULE__{} = fork, attrs \\ %{}) do
fork
|> cast(attrs, @allowed_attrs)
|> validate_required(@required_attrs)
|> assoc_constraint(:transaction)
|> assoc_constraint(:uncle)
end
end

@ -114,7 +114,7 @@ defmodule Explorer.SmartContract.Reader do
|> decode_results(abi, functions)
rescue
error ->
format_error(functions, error.message)
format_error(functions, error)
end
defp decode_results({:ok, results}, abi, functions), do: Encoder.decode_abi_results(results, abi, functions)
@ -123,7 +123,7 @@ defmodule Explorer.SmartContract.Reader do
format_error(functions, "Bad Gateway")
end
defp format_error(functions, message) do
defp format_error(functions, message) when is_binary(message) do
functions
|> Enum.map(fn {function_name, _args} ->
%{function_name => {:error, message}}
@ -131,6 +131,14 @@ defmodule Explorer.SmartContract.Reader do
|> List.first()
end
defp format_error(functions, %{message: error_message}) do
format_error(functions, error_message)
end
defp format_error(functions, error) do
format_error(functions, Exception.message(error))
end
@doc """
Given the encoded data that references a function and its arguments in the blockchain, as well as the contract address, returns what EthereumJSONRPC.execute_contract_functions expects.
"""

@ -3,6 +3,7 @@ defmodule Explorer.Repo.Migrations.CreateBlocks do
def change do
create table(:blocks, primary_key: false) do
add(:consensus, :boolean, null: false)
add(:difficulty, :numeric, precision: 50)
add(:gas_limit, :numeric, precision: 100, null: false)
add(:gas_used, :numeric, precision: 100, null: false)
@ -22,7 +23,7 @@ defmodule Explorer.Repo.Migrations.CreateBlocks do
end
create(index(:blocks, [:timestamp]))
create(unique_index(:blocks, [:parent_hash]))
create(unique_index(:blocks, [:number]))
create(index(:blocks, [:parent_hash], unique: true, where: ~s(consensus), name: :one_consensus_child_per_parent))
create(index(:blocks, [:number], unique: true, where: ~s(consensus), name: :one_consensus_block_at_height))
end
end

@ -0,0 +1,22 @@
defmodule Explorer.Repo.Migrations.CreateBlockSecondDegreeRelations do
use Ecto.Migration
def change do
create table(:block_second_degree_relations, primary_key: false) do
add(:nephew_hash, references(:blocks, column: :hash, type: :bytea), null: false)
add(:uncle_hash, :bytea, null: false)
add(:uncle_fetched_at, :utc_datetime, default: fragment("NULL"), null: true)
end
create(unique_index(:block_second_degree_relations, [:nephew_hash, :uncle_hash], name: :nephew_hash_to_uncle_hash))
create(
unique_index(:block_second_degree_relations, [:nephew_hash, :uncle_hash],
name: :unfetched_uncles,
where: "uncle_fetched_at IS NULL"
)
)
create(unique_index(:block_second_degree_relations, [:uncle_hash, :nephew_hash], name: :uncle_hash_to_nephew_hash))
end
end

@ -0,0 +1,16 @@
defmodule Explorer.Repo.Migrations.CreateTransactionBlockUncles do
use Ecto.Migration
def change do
create table(:transaction_forks, primary_key: false) do
add(:hash, references(:transactions, column: :hash, on_delete: :delete_all, type: :bytea), null: false)
add(:index, :integer, null: false)
add(:uncle_hash, references(:blocks, column: :hash, on_delete: :delete_all, type: :bytea), null: false)
timestamps()
end
create(index(:transaction_forks, :uncle_hash))
create(unique_index(:transaction_forks, [:uncle_hash, :index]))
end
end

@ -0,0 +1,50 @@
defmodule Explorer.Chain.Block.SecondDegreeRelationTest do
use Explorer.DataCase, async: true
alias Ecto.Changeset
alias Explorer.Chain.Block
describe "changeset/2" do
test "requires hash and nephew_hash" do
assert %Changeset{valid?: false} =
changeset = Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{})
assert changeset_errors(changeset) == %{nephew_hash: ["can't be blank"], uncle_hash: ["can't be blank"]}
assert %Changeset{valid?: true} =
Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{
nephew_hash: block_hash(),
uncle_hash: block_hash()
})
end
test "allows uncle_fetched_at" do
assert %Changeset{changes: %{uncle_fetched_at: _}, valid?: true} =
Block.SecondDegreeRelation.changeset(%Block.SecondDegreeRelation{}, %{
nephew_hash: block_hash(),
uncle_hash: block_hash(),
uncle_fetched_at: DateTime.utc_now()
})
end
test "enforces foreign key constraint on nephew_hash" do
assert {:error, %Changeset{valid?: false} = changeset} =
%Block.SecondDegreeRelation{}
|> Block.SecondDegreeRelation.changeset(%{nephew_hash: block_hash(), uncle_hash: block_hash()})
|> Repo.insert()
assert changeset_errors(changeset) == %{nephew_hash: ["does not exist"]}
end
test "enforces unique constraints on {nephew_hash, uncle_hash}" do
%Block.SecondDegreeRelation{nephew_hash: nephew_hash, uncle_hash: hash} = insert(:block_second_degree_relation)
assert {:error, %Changeset{valid?: false} = changeset} =
%Block.SecondDegreeRelation{}
|> Block.SecondDegreeRelation.changeset(%{nephew_hash: nephew_hash, uncle_hash: hash})
|> Repo.insert()
assert changeset_errors(changeset) == %{uncle_hash: ["has already been taken"]}
end
end
end

@ -24,6 +24,7 @@ defmodule Explorer.Chain.ImportTest do
blocks: %{
params: [
%{
consensus: true,
difficulty: 340_282_366_920_938_463_463_374_607_431_768_211_454,
gas_limit: 6_946_336,
gas_used: 50450,
@ -516,6 +517,7 @@ defmodule Explorer.Chain.ImportTest do
blocks: %{
params: [
%{
consensus: true,
difficulty: 340_282_366_920_938_463_463_374_607_431_768_211_454,
gas_limit: 6_926_030,
gas_used: 269_607,
@ -605,6 +607,7 @@ defmodule Explorer.Chain.ImportTest do
blocks: %{
params: [
%{
consensus: true,
difficulty: 340_282_366_920_938_463_463_374_607_431_768_211_454,
gas_limit: 6_926_030,
gas_used: 269_607,
@ -698,6 +701,7 @@ defmodule Explorer.Chain.ImportTest do
blocks: %{
params: [
%{
consensus: true,
difficulty: 340_282_366_920_938_463_463_374_607_431_768_211_454,
gas_limit: 6_926_030,
gas_used: 269_607,
@ -788,6 +792,7 @@ defmodule Explorer.Chain.ImportTest do
blocks: %{
params: [
%{
consensus: true,
difficulty: 340_282_366_920_938_463_463_374_607_431_768_211_454,
gas_limit: 6_946_336,
gas_used: 50450,
@ -911,6 +916,7 @@ defmodule Explorer.Chain.ImportTest do
blocks: %{
params: [
%{
consensus: true,
difficulty: 242_354_495_292_210,
gas_limit: 4_703_218,
gas_used: 1_009_480,
@ -924,6 +930,7 @@ defmodule Explorer.Chain.ImportTest do
total_difficulty: 415_641_295_487_918_824_165
},
%{
consensus: true,
difficulty: 247_148_243_947_046,
gas_limit: 4_704_624,
gas_used: 363_000,
@ -1047,5 +1054,70 @@ defmodule Explorer.Chain.ImportTest do
assert %Transaction{status: :error, error: "Out of gas"} =
Repo.get(Transaction, "0xab349efbe1ddc6d85d84a993aa52bdaadce66e8ee166dd10013ce3f2a94ca724")
end
test "uncles record their transaction indexes in transactions_forks" do
miner_hash = address_hash()
from_address_hash = address_hash()
transaction_hash = transaction_hash()
uncle_hash = block_hash()
assert {:ok, _} =
Import.all(%{
addresses: %{
params: [
%{hash: miner_hash},
%{hash: from_address_hash}
]
},
blocks: %{
params: [
%{
consensus: false,
difficulty: 0,
gas_limit: 21_000,
gas_used: 21_000,
hash: uncle_hash,
miner_hash: miner_hash,
nonce: 0,
number: 0,
parent_hash: block_hash(),
size: 0,
timestamp: DateTime.utc_now(),
total_difficulty: 0
}
]
},
transactions: %{
params: [
%{
block_hash: nil,
block_number: nil,
from_address_hash: from_address_hash,
gas: 21_000,
gas_price: 1,
hash: transaction_hash,
input: "0x",
nonce: 0,
r: 0,
s: 0,
v: 0,
value: 0
}
],
on_conflict: :replace_all
},
transaction_forks: %{
params: [
%{
uncle_hash: uncle_hash,
index: 0,
hash: transaction_hash
}
]
}
})
assert Repo.aggregate(Transaction.Fork, :count, :hash) == 1
end
end
end

@ -0,0 +1,23 @@
defmodule Explorer.Chain.Transaction.ForkTest do
use Explorer.DataCase
alias Ecto.Changeset
alias Explorer.Chain.Transaction.Fork
doctest Fork
test "a transaction fork cannot be inserted if the corresponding transaction does not exist" do
assert %Changeset{valid?: true} = changeset = Fork.changeset(%Fork{}, params_for(:transaction_fork))
assert {:error, %Changeset{errors: [transaction: {"does not exist", []}]}} = Repo.insert(changeset)
end
test "a transaction fork cannot be inserted if the corresponding uncle does not exist" do
transaction = insert(:transaction)
assert %Changeset{valid?: true} =
changeset = Fork.changeset(%Fork{}, %{hash: transaction.hash, index: 0, uncle_hash: block_hash()})
assert {:error, %Changeset{errors: [uncle: {"does not exist", []}]}} = Repo.insert(changeset)
end
end

@ -1,6 +1,9 @@
defmodule Explorer.ChainTest do
use Explorer.DataCase
require Ecto.Query
import Ecto.Query
import Explorer.Factory
alias Explorer.{Chain, Factory, PagingOptions, Repo}
@ -670,6 +673,7 @@ defmodule Explorer.ChainTest do
blocks: %{
params: [
%{
consensus: true,
difficulty: 340_282_366_920_938_463_463_374_607_431_768_211_454,
gas_limit: 6_946_336,
gas_used: 50450,
@ -684,6 +688,14 @@ defmodule Explorer.ChainTest do
}
]
},
block_second_degree_relations: %{
params: [
%{
nephew_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471bd",
uncle_hash: "0xf6b4b8c88df3ebd252ec476328334dc026cf66606a84fb769b3d3cbccc8471be"
}
]
},
broadcast: true,
internal_transactions: %{
params: [
@ -817,6 +829,7 @@ defmodule Explorer.ChainTest do
],
blocks: [
%Block{
consensus: true,
difficulty: ^difficulty,
gas_limit: ^gas_limit,
gas_used: ^gas_used,
@ -2259,6 +2272,20 @@ defmodule Explorer.ChainTest do
end
end
describe "stream_unfetched_uncle_hashes/2" do
test "does not return uncle hashes where t:Explorer.Chain.Block.SecondDegreeRelation.t/0 unclue_fetched_at is not nil" do
%Block.SecondDegreeRelation{nephew: %Block{}, uncle_hash: uncle_hash} = insert(:block_second_degree_relation)
assert {:ok, [^uncle_hash]} = Explorer.Chain.stream_unfetched_uncle_hashes([], &[&1 | &2])
query = from(bsdr in Block.SecondDegreeRelation, where: bsdr.uncle_hash == ^uncle_hash)
assert {1, _} = Repo.update_all(query, set: [uncle_fetched_at: DateTime.utc_now()])
assert {:ok, []} = Explorer.Chain.stream_unfetched_uncle_hashes([], &[&1 | &2])
end
end
test "total_supply/0" do
height = 2_000_000
insert(:block, number: height)

@ -82,6 +82,24 @@ defmodule Explorer.SmartContract.ReaderTest do
assert %{"get" => {:error, "Bad Gateway"}} = response
end
test "handles other types of errors" do
smart_contract = build(:smart_contract)
contract_address_hash = Hash.to_string(smart_contract.address_hash)
abi = smart_contract.abi
expect(
EthereumJSONRPC.Mox,
:json_rpc,
fn [%{id: _, method: _, params: [%{data: _, to: _}]}], _options ->
raise FunctionClauseError
end
)
response = Reader.query_contract(contract_address_hash, abi, %{"get" => []})
assert %{"get" => {:error, "no function clause matches"}} = response
end
end
describe "query_verified_contract/2" do

@ -105,6 +105,7 @@ defmodule Explorer.Factory do
def block_factory do
%Block{
consensus: true,
number: block_number(),
hash: block_hash(),
parent_hash: block_hash(),
@ -132,6 +133,13 @@ defmodule Explorer.Factory do
sequence("block_number", & &1)
end
def block_second_degree_relation_factory do
%Block.SecondDegreeRelation{
uncle_hash: block_hash(),
nephew: build(:block)
}
end
def with_block(%Transaction{index: nil} = transaction) do
with_block(transaction, insert(:block))
end
@ -420,6 +428,14 @@ defmodule Explorer.Factory do
data(:transaction_input)
end
def transaction_fork_factory do
%Transaction.Fork{
hash: transaction_hash(),
index: 0,
uncle_hash: block_hash()
}
end
def smart_contract_factory() do
%SmartContract{
address_hash: insert(:address).hash,

@ -32,6 +32,10 @@ defmodule Indexer.Address.CoinBalances do
Enum.reduce(transactions_params, initial, &transactions_params_reducer/2)
end
defp reducer({:block_second_degree_relations_params, block_second_degree_relations_params}, initial)
when is_list(block_second_degree_relations_params),
do: initial
defp internal_transactions_params_reducer(%{block_number: block_number} = internal_transaction_params, acc)
when is_integer(block_number) do
case internal_transaction_params do

@ -105,7 +105,10 @@ defmodule Indexer.Block.Catchup.Fetcher do
{async_import_remaning_block_data_options, chain_import_options} =
Map.split(options, @async_import_remaning_block_data_options)
with {:ok, results} = ok <- Chain.import(chain_import_options) do
with {:ok, results} = ok <-
chain_import_options
|> put_in([:blocks, :params, Access.all(), :consensus], true)
|> Chain.import() do
async_import_remaining_block_data(
results,
async_import_remaning_block_data_options
@ -117,6 +120,7 @@ defmodule Indexer.Block.Catchup.Fetcher do
defp async_import_remaining_block_data(
%{
block_second_degree_relations: block_second_degree_relations,
transactions: transaction_hashes,
addresses: address_hashes,
tokens: tokens,
@ -146,6 +150,10 @@ defmodule Indexer.Block.Catchup.Fetcher do
|> Token.Fetcher.async_fetch()
TokenBalance.Fetcher.async_fetch(token_balances)
block_second_degree_relations
|> Enum.map(& &1.uncle_hash)
|> Block.Uncle.Fetcher.async_fetch_blocks()
end
defp stream_fetch_and_import(%__MODULE__{blocks_concurrency: blocks_concurrency} = state, sequence)

@ -26,6 +26,7 @@ defmodule Indexer.Block.Fetcher do
addresses: Import.addresses_options(),
balances: Import.balances_options(),
blocks: Import.blocks_options(),
block_second_degree_relations: Import.block_second_degree_relations_options(),
broadcast: boolean,
logs: Import.logs_options(),
receipts: Import.receipts_options(),
@ -91,7 +92,11 @@ defmodule Indexer.Block.Fetcher do
when broadcast in ~w(true false)a and callback_module != nil do
with {:blocks, {:ok, next, result}} <-
{:blocks, EthereumJSONRPC.fetch_blocks_by_range(range, json_rpc_named_arguments)},
%{blocks: blocks, transactions: transactions_without_receipts} = result,
%{
blocks: blocks,
transactions: transactions_without_receipts,
block_second_degree_relations: block_second_degree_relations
} = result,
{:receipts, {:ok, receipt_params}} <- {:receipts, Receipts.fetch(state, transactions_without_receipts)},
%{logs: logs, receipts: receipts} = receipt_params,
transactions_with_receipts = Receipts.put(transactions_without_receipts, receipts),
@ -111,14 +116,14 @@ defmodule Indexer.Block.Fetcher do
}),
token_balances = TokenBalances.params_set(%{token_transfers_params: token_transfers}),
{:ok, inserted} <-
import_range(
__MODULE__.import(
state,
%{
range: range,
addresses: %{params: addresses},
balances: %{params: coin_balances_params_set},
token_balances: %{params: token_balances},
blocks: %{params: blocks},
block_second_degree_relations: %{params: block_second_degree_relations},
logs: %{params: logs},
receipts: %{params: receipts},
token_transfers: %{params: token_transfers},
@ -134,11 +139,11 @@ defmodule Indexer.Block.Fetcher do
end
end
defp import_range(
%__MODULE__{broadcast: broadcast, callback_module: callback_module} = state,
options
)
when is_map(options) do
def import(
%__MODULE__{broadcast: broadcast, callback_module: callback_module} = state,
options
)
when is_map(options) do
{address_hash_to_fetched_balance_block_number, import_options} =
pop_address_hash_to_fetched_balance_block_number(options)

@ -104,6 +104,7 @@ defmodule Indexer.Block.Realtime.Fetcher do
options
|> Map.drop(@import_options)
|> put_in([:addresses, :params], balances_addresses_params)
|> put_in([:blocks, :params, Access.all(), :consensus], true)
|> put_in([Access.key(:balances, %{}), :params], balances_params)
|> put_in([Access.key(:internal_transactions, %{}), :params], internal_transactions_params)
|> put_in([Access.key(:token_balances), :params], token_balances),
@ -124,6 +125,9 @@ defmodule Indexer.Block.Realtime.Fetcher do
end
def fetch_and_import_block(block_number_to_fetch, block_fetcher) do
# Wait half a second to give Parity/Geth time to sync.
:timer.sleep(500)
case fetch_and_import_range(block_fetcher, block_number_to_fetch..block_number_to_fetch) do
{:ok, {_inserted, _next}} ->
Logger.debug(fn ->
@ -169,10 +173,14 @@ defmodule Indexer.Block.Realtime.Fetcher do
end
end
defp async_import_remaining_block_data(%{tokens: tokens}) do
defp async_import_remaining_block_data(%{block_second_degree_relations: block_second_degree_relations, tokens: tokens}) do
tokens
|> Enum.map(& &1.contract_address_hash)
|> Token.Fetcher.async_fetch()
block_second_degree_relations
|> Enum.map(& &1.uncle_hash)
|> Block.Uncle.Fetcher.async_fetch_blocks()
end
defp internal_transactions(

@ -4,7 +4,7 @@ defmodule Indexer.Block.Supervisor do
"""
alias Indexer.Block
alias Indexer.Block.{Catchup, Realtime}
alias Indexer.Block.{Catchup, Realtime, Uncle}
use Supervisor
@ -27,7 +27,8 @@ defmodule Indexer.Block.Supervisor do
[
%{block_fetcher: block_fetcher, subscribe_named_arguments: subscribe_named_arguments},
[name: Realtime.Supervisor]
]}
]},
{Uncle.Supervisor, [[block_fetcher: block_fetcher], [name: Uncle.Supervisor]]}
],
strategy: :one_for_one
)

@ -0,0 +1,154 @@
defmodule Indexer.Block.Uncle.Fetcher do
@moduledoc """
Fetches `t:Explorer.Chain.Block.t/0` by `hash` and updates `t:Explorer.Chain.Block.SecondDegreeRelation.t/0`
`uncle_fetched_at` where the `uncle_hash` matches `hash`.
"""
require Logger
alias Explorer.Chain
alias Explorer.Chain.Hash
alias Indexer.{AddressExtraction, Block, BufferedTask}
@behaviour Block.Fetcher
@behaviour BufferedTask
@defaults [
flush_interval: :timer.seconds(3),
max_batch_size: 10,
max_concurrency: 10,
init_chunk_size: 1000,
task_supervisor: Indexer.Block.Uncle.TaskSupervisor
]
@doc """
Asynchronously fetches `t:Explorer.Chain.Block.t/0` for the given `hashes` and updates
`t:Explorer.Chain.Block.SecondDegreeRelation.t/0` `block_fetched_at`.
"""
@spec async_fetch_blocks([Hash.Full.t()]) :: :ok
def async_fetch_blocks(block_hashes) when is_list(block_hashes) do
BufferedTask.buffer(
__MODULE__,
block_hashes
|> Enum.map(&to_string/1)
|> Enum.uniq()
)
end
@doc false
def child_spec([init_options, gen_server_options]) when is_list(init_options) do
{state, mergeable_init_options} = Keyword.pop(init_options, :block_fetcher)
unless state do
raise ArgumentError,
":json_rpc_named_arguments must be provided to `#{__MODULE__}.child_spec " <>
"to allow for json_rpc calls when running."
end
merged_init_options =
@defaults
|> Keyword.merge(mergeable_init_options)
|> Keyword.put(:state, %Block.Fetcher{state | broadcast: false, callback_module: __MODULE__})
Supervisor.child_spec({BufferedTask, [{__MODULE__, merged_init_options}, gen_server_options]}, id: __MODULE__)
end
@impl BufferedTask
def init(initial, reducer, _) do
{:ok, final} =
Chain.stream_unfetched_uncle_hashes(initial, fn uncle_hash, acc ->
uncle_hash
|> to_string()
|> reducer.(acc)
end)
final
end
@impl BufferedTask
def run(hashes, _retries, %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments} = block_fetcher) do
# the same block could be included as an uncle on multiple blocks, but we only want to fetch it once
unique_hashes = Enum.uniq(hashes)
Logger.debug(fn -> "fetching #{length(unique_hashes)} uncle blocks" end)
case EthereumJSONRPC.fetch_blocks_by_hash(unique_hashes, json_rpc_named_arguments) do
{:ok,
%{
blocks: blocks_params,
transactions: transactions_params,
block_second_degree_relations: block_second_degree_relations_params
}} ->
addresses_params =
AddressExtraction.extract_addresses(%{blocks: blocks_params, transactions: transactions_params})
{:ok, _} =
Block.Fetcher.import(block_fetcher, %{
addresses: %{params: addresses_params},
blocks: %{params: blocks_params},
block_second_degree_relations: %{params: block_second_degree_relations_params},
transactions: %{params: transactions_params, on_conflict: :nothing}
})
:ok
{:error, reason} ->
Logger.debug(fn -> "failed to fetch #{length(unique_hashes)} uncle blocks, #{inspect(reason)}" end)
{:retry, unique_hashes}
end
end
@impl Block.Fetcher
def import(_, options) when is_map(options) do
with {:ok, %{block_second_degree_relations: block_second_degree_relations}} = ok <-
options
|> uncle_blocks()
|> fork_transactions()
|> Chain.import() do
# * CoinBalance.Fetcher.async_fetch_balances is not called because uncles don't affect balances
# * InternalTransaction.Fetcher.async_fetch is not called because internal transactions are based on transaction
# hash, which is shared with transaction on consensus blocks.
# * Token.Fetcher.async_fetch is not called because the tokens only matter on consensus blocks
# * TokenBalance.Fetcher.async_fetch is not called because it uses block numbers from consensus, not uncles
block_second_degree_relations
|> Enum.map(& &1.uncle_hash)
|> Block.Uncle.Fetcher.async_fetch_blocks()
ok
end
end
defp uncle_blocks(chain_import_options) do
put_in(chain_import_options, [:blocks, :params, Access.all(), :consensus], false)
end
defp fork_transactions(chain_import_options) do
transactions_params = chain_import_options[:transactions][:params] || []
chain_import_options
|> put_in([:transactions, :params], forked_transactions_params(transactions_params))
|> put_in([Access.key(:transaction_forks, %{}), :params], transaction_forks_params(transactions_params))
end
defp forked_transactions_params(transactions_params) do
# With no block_hash, there will be a collision for the same hash when a transaction is used in more than 1 uncle,
# so use MapSet to prevent duplicate row errors.
MapSet.new(transactions_params, fn transaction_params ->
Map.merge(transaction_params, %{
block_hash: nil,
block_number: nil,
index: nil,
gas_used: nil,
cumulative_gas_used: nil,
status: nil
})
end)
end
defp transaction_forks_params(transactions_params) do
Enum.map(transactions_params, fn %{block_hash: uncle_hash, index: index, hash: hash} ->
%{uncle_hash: uncle_hash, index: index, hash: hash}
end)
end
end

@ -0,0 +1,38 @@
defmodule Indexer.Block.Uncle.Supervisor do
@moduledoc """
Supervises `Indexer.Block.Uncle.Fetcher`.
"""
use Supervisor
alias Indexer.Block.Uncle.Fetcher
def child_spec([init_arguments]) do
child_spec([init_arguments, []])
end
def child_spec([_init_arguments, _gen_server_options] = start_link_arguments) do
default = %{
id: __MODULE__,
start: {__MODULE__, :start_link, start_link_arguments},
type: :supervisor
}
Supervisor.child_spec(default, [])
end
def start_link(arguments, gen_server_options \\ []) do
Supervisor.start_link(__MODULE__, arguments, gen_server_options)
end
@impl Supervisor
def init(fetcher_arguments) do
Supervisor.init(
[
{Task.Supervisor, name: Indexer.Block.Uncle.TaskSupervisor},
{Fetcher, [fetcher_arguments, [name: Fetcher]]}
],
strategy: :rest_for_one
)
end
end

@ -110,6 +110,7 @@ defmodule Indexer.PendingTransaction.Fetcher do
{:ok, _} =
Chain.import(%{
addresses: %{params: addresses_params},
broadcast: true,
transactions: %{params: transactions_params, on_conflict: :nothing}
})

@ -11,12 +11,22 @@ defmodule Indexer.Address.CoinBalancesTest do
|> to_string()
block_number = 1
params_set = CoinBalances.params_set(%{blocks_params: [%{miner_hash: miner_hash, number: block_number}]})
assert MapSet.size(params_set) == 1
assert %{address_hash: miner_hash, block_number: block_number}
end
test "with block second degree relations extracts nothing" do
params_set =
CoinBalances.params_set(%{
block_second_degree_relations_params: [%{nephew_hash: Factory.block_hash(), uncle_hash: Factory.block_hash()}]
})
assert MapSet.size(params_set) == 0
end
test "with call internal transaction extracts nothing" do
internal_transaction_params =
:internal_transaction

@ -8,7 +8,7 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do
alias Explorer.Chain.Block
alias Indexer.{BoundInterval, CoinBalance, InternalTransaction, Token, TokenBalance}
alias Indexer.Block.Catchup
alias Indexer.Block.{Catchup, Uncle}
@moduletag capture_log: true
@ -202,6 +202,10 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do
Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
Uncle.Supervisor.Case.start_supervised!(
block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}
)
Catchup.Supervisor.Case.start_supervised!(%{
block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}
})
@ -301,7 +305,8 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do
"size" => "0x0",
"timestamp" => "0x0",
"totalDifficulty" => "0x0",
"transactions" => []
"transactions" => [],
"uncles" => []
}
}
]}
@ -323,6 +328,10 @@ defmodule Indexer.Block.Catchup.BoundIntervalSupervisorTest do
Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
Uncle.Supervisor.Case.start_supervised!(
block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}
)
# from `setup :state`
assert_received :catchup_index

@ -0,0 +1,115 @@
defmodule Indexer.Block.Catchup.FetcherTest do
use EthereumJSONRPC.Case, async: false
use Explorer.DataCase
import Mox
alias Indexer.{Block, CoinBalance, InternalTransaction, Token, TokenBalance}
alias Indexer.Block.Catchup.Fetcher
@moduletag capture_log: true
# MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to
# use expectations and stubs from test's pid.
setup :set_mox_global
setup :verify_on_exit!
setup do
# Uncle don't occur on POA chains, so there's no way to test this using the public addresses, so mox-only testing
%{
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
# Which one does not matter, so pick one
variant: EthereumJSONRPC.Parity
]
}
end
describe "import/1" do
test "fetches uncles asynchronously", %{json_rpc_named_arguments: json_rpc_named_arguments} do
CoinBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
InternalTransaction.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
parent = self()
pid =
spawn_link(fn ->
receive do
{:"$gen_call", from, {:buffer, uncles}} ->
GenServer.reply(from, :ok)
send(parent, {:uncles, uncles})
end
end)
Process.register(pid, Block.Uncle.Fetcher)
nephew_hash = block_hash() |> to_string()
uncle_hash = block_hash() |> to_string()
miner_hash = address_hash() |> to_string()
block_number = 0
assert {:ok, _} =
Fetcher.import(%Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}, %{
addresses: %{
params: [
%{hash: miner_hash}
]
},
address_hash_to_fetched_balance_block_number: %{miner_hash => block_number},
balances: %{
params: [
%{
address_hash: miner_hash,
block_number: block_number
}
]
},
blocks: %{
params: [
%{
difficulty: 0,
gas_limit: 21000,
gas_used: 21000,
miner_hash: miner_hash,
nonce: 0,
number: block_number,
parent_hash:
block_hash()
|> to_string(),
size: 0,
timestamp: DateTime.utc_now(),
total_difficulty: 0,
hash: nephew_hash
}
]
},
block_second_degree_relations: %{
params: [
%{
nephew_hash: nephew_hash,
uncle_hash: uncle_hash
}
]
},
tokens: %{
params: [],
on_conflict: :nothing
},
token_balances: %{
params: []
},
transactions: %{
params: [],
on_conflict: :nothing
},
transaction_hash_to_block_number: %{}
})
assert_receive {:uncles, [^uncle_hash]}
end
end
end

@ -10,7 +10,7 @@ defmodule Indexer.Block.FetcherTest do
alias Explorer.Chain
alias Explorer.Chain.{Address, Log, Transaction, Wei}
alias Indexer.{CoinBalance, BufferedTask, InternalTransaction, Token, TokenBalance}
alias Indexer.Block.Fetcher
alias Indexer.Block.{Fetcher, Uncle}
@moduletag capture_log: true
@ -45,6 +45,10 @@ defmodule Indexer.Block.FetcherTest do
Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
TokenBalance.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
Uncle.Supervisor.Case.start_supervised!(
block_fetcher: %Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}
)
%{
block_fetcher: %Fetcher{
broadcast: false,

@ -1,4 +1,4 @@
defmodule Indexer.BlockFetcher.RealtimeTest do
defmodule Indexer.Block.Realtime.FetcherTest do
use EthereumJSONRPC.Case, async: false
use Explorer.DataCase
@ -7,7 +7,7 @@ defmodule Indexer.BlockFetcher.RealtimeTest do
alias Explorer.Chain
alias Explorer.Chain.Address
alias Indexer.{Sequence, Token}
alias Indexer.Block.Realtime
alias Indexer.Block.{Realtime, Uncle}
@moduletag capture_log: true
@ -36,7 +36,7 @@ defmodule Indexer.BlockFetcher.RealtimeTest do
%{block_fetcher: block_fetcher, json_rpc_named_arguments: core_json_rpc_named_arguments}
end
describe "Indexer.BlockFetcher.stream_import/1" do
describe "Indexer.Block.Fetcher.fetch_and_import_range/1" do
@tag :no_geth
test "in range with internal transactions", %{
block_fetcher: %Indexer.Block.Fetcher{} = block_fetcher,
@ -47,6 +47,10 @@ defmodule Indexer.BlockFetcher.RealtimeTest do
Token.Supervisor.Case.start_supervised!(json_rpc_named_arguments: json_rpc_named_arguments)
Uncle.Supervisor.Case.start_supervised!(
block_fetcher: %Indexer.Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}
)
if json_rpc_named_arguments[:transport] == EthereumJSONRPC.Mox do
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [
@ -122,7 +126,7 @@ defmodule Indexer.BlockFetcher.RealtimeTest do
}
],
"transactionsRoot" => "0xd7c39a93eafe0bdcbd1324c13dcd674bed8c9fa8adbf8f95bf6a59788985da6f",
"uncles" => []
"uncles" => ["0xa4ec735cabe1510b5ae081b30f17222580b4588dbec52830529753a688b046cd"]
}
},
%{

@ -0,0 +1,126 @@
defmodule Indexer.Block.Uncle.FetcherTest do
# MUST be `async: false` so that {:shared, pid} is set for connection to allow CoinBalanceFetcher's self-send to have
# connection allowed immediately.
use EthereumJSONRPC.Case, async: false
use Explorer.DataCase
alias Explorer.Chain
alias Indexer.Block
import Mox
@moduletag :capture_log
# MUST use global mode because we aren't guaranteed to get `start_supervised`'s pid back fast enough to `allow` it to
# use expectations and stubs from test's pid.
setup :set_mox_global
setup :verify_on_exit!
setup do
# Uncle don't occur on POA chains, so there's no way to test this using the public addresses, so mox-only testing
%{
json_rpc_named_arguments: [
transport: EthereumJSONRPC.Mox,
transport_options: [],
# Which one does not matter, so pick one
variant: EthereumJSONRPC.Parity
]
}
end
describe "child_spec/1" do
test "raises ArgumentError is `json_rpc_named_arguments is not provided" do
assert_raise ArgumentError,
":json_rpc_named_arguments must be provided to `Elixir.Indexer.Block.Uncle.Fetcher.child_spec " <>
"to allow for json_rpc calls when running.",
fn ->
start_supervised({Block.Uncle.Fetcher, [[], []]})
end
end
end
describe "init/1" do
test "fetched unfetched uncle hashes", %{json_rpc_named_arguments: json_rpc_named_arguments} do
assert %Chain.Block.SecondDegreeRelation{nephew_hash: nephew_hash, uncle_hash: uncle_hash, uncle: nil} =
:block_second_degree_relation
|> insert()
|> Repo.preload([:nephew, :uncle])
uncle_hash_data = to_string(uncle_hash)
uncle_uncle_hash_data = to_string(block_hash())
EthereumJSONRPC.Mox
|> expect(:json_rpc, fn [%{method: "eth_getBlockByHash", params: [^uncle_hash_data, true]}], _ ->
number_quantity = "0x0"
{:ok,
[
%{
result: %{
"author" => "0xe2ac1c6843a33f81ae4935e5ef1277a392990381",
"difficulty" => "0xfffffffffffffffffffffffffffffffe",
"extraData" => "0xd583010a068650617269747986312e32362e32826c69",
"gasLimit" => "0x7a1200",
"gasUsed" => "0x0",
"hash" => uncle_hash_data,
"logsBloom" => "0x",
"miner" => "0xe2ac1c6843a33f81ae4935e5ef1277a392990381",
"number" => number_quantity,
"parentHash" => "0x006edcaa1e6fde822908783bc4ef1ad3675532d542fce53537557391cfe34c3c",
"size" => "0x243",
"timestamp" => "0x5b437f41",
"totalDifficulty" => "0x342337ffffffffffffffffffffffffed8d29bb",
"transactions" => [
%{
"blockHash" => uncle_hash_data,
"blockNumber" => number_quantity,
"chainId" => "0x4d",
"condition" => nil,
"creates" => "0xffc87239eb0267bc3ca2cd51d12fbf278e02ccb4",
"from" => "0xe8ddc5c7a2d2f0d7a9798459c0104fdf5e987aca",
"gas" => "0x47b760",
"gasPrice" => "0x174876e800",
"hash" => "0x3a3eb134e6792ce9403ea4188e5e79693de9e4c94e499db132be086400da79e6",
"input" => "0x",
"nonce" => "0x0",
"r" => "0xad3733df250c87556335ffe46c23e34dbaffde93097ef92f52c88632a40f0c75",
"s" => "0x72caddc0371451a58de2ca6ab64e0f586ccdb9465ff54e1c82564940e89291e3",
"standardV" => "0x0",
"to" => nil,
"transactionIndex" => "0x0",
"v" => "0xbd",
"value" => "0x0"
}
],
"uncles" => [uncle_uncle_hash_data]
}
}
]}
end)
Block.Uncle.Supervisor.Case.start_supervised!(
block_fetcher: %Block.Fetcher{json_rpc_named_arguments: json_rpc_named_arguments}
)
wait(fn ->
Repo.one!(
from(bsdr in Chain.Block.SecondDegreeRelation,
where: bsdr.nephew_hash == ^nephew_hash and not is_nil(bsdr.uncle_fetched_at)
)
)
end)
refute is_nil(Repo.get(Chain.Block, uncle_hash))
assert Repo.aggregate(Chain.Transaction.Fork, :count, :hash) == 1
end
end
defp wait(producer) do
producer.()
rescue
Ecto.NoResultsError ->
Process.sleep(100)
wait(producer)
end
end

@ -0,0 +1,18 @@
defmodule Indexer.Block.Uncle.Supervisor.Case do
alias Indexer.Block
def start_supervised!(fetcher_arguments \\ []) when is_list(fetcher_arguments) do
merged_fetcher_arguments =
Keyword.merge(
fetcher_arguments,
flush_interval: 50,
init_chunk_size: 1,
max_batch_size: 1,
max_concurrency: 1
)
[merged_fetcher_arguments]
|> Block.Uncle.Supervisor.child_spec()
|> ExUnit.Callbacks.start_supervised!()
end
end
Loading…
Cancel
Save