diff --git a/.tool-versions b/.tool-versions index 2f81e457af..bf2689b13f 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ elixir 1.7.1 erlang 21.0.4 -nodejs 10.5.0 +nodejs 10.11.0 diff --git a/apps/block_scout_web/assets/__tests__/pages/transaction.js b/apps/block_scout_web/assets/__tests__/pages/transaction.js index 20da637dae..b4ba0f62b7 100644 --- a/apps/block_scout_web/assets/__tests__/pages/transaction.js +++ b/apps/block_scout_web/assets/__tests__/pages/transaction.js @@ -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) }) }) diff --git a/apps/block_scout_web/assets/css/components/_animations.scss b/apps/block_scout_web/assets/css/components/_animations.scss index 0db1f9c125..622f8de130 100644 --- a/apps/block_scout_web/assets/css/components/_animations.scss +++ b/apps/block_scout_web/assets/css/components/_animations.scss @@ -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; +} diff --git a/apps/block_scout_web/assets/css/components/_dropdown.scss b/apps/block_scout_web/assets/css/components/_dropdown.scss index 84bc109cb8..d62e7820f2 100644 --- a/apps/block_scout_web/assets/css/components/_dropdown.scss +++ b/apps/block_scout_web/assets/css/components/_dropdown.scss @@ -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; +} diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index caa01d32d4..b26832696f 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -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' diff --git a/apps/block_scout_web/assets/js/lib/token_balance_dropdown_search.js b/apps/block_scout_web/assets/js/lib/token_balance_dropdown_search.js new file mode 100644 index 0000000000..ce31e0cb90 --- /dev/null +++ b/apps/block_scout_web/assets/js/lib/token_balance_dropdown_search.js @@ -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) +}) diff --git a/apps/block_scout_web/assets/js/pages/chain.js b/apps/block_scout_web/assets/js/pages/chain.js index c6fc48cbc9..08ed8fe023 100644 --- a/apps/block_scout_web/assets/js/pages/chain.js +++ b/apps/block_scout_web/assets/js/pages/chain.js @@ -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() } diff --git a/apps/block_scout_web/assets/js/pages/transaction.js b/apps/block_scout_web/assets/js/pages/transaction.js index 6598e33659..5468cef4a8 100644 --- a/apps/block_scout_web/assets/js/pages/transaction.js +++ b/apps/block_scout_web/assets/js/pages/transaction.js @@ -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"]') diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index bbb21ba718..b0a2f89c3c 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -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) + }) } diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index 6f0ffa5568..83b0b292fd 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -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", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index f363ff1246..a3c05cba5a 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -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", diff --git a/apps/block_scout_web/assets/static/android-chrome-192x192.png b/apps/block_scout_web/assets/static/android-chrome-192x192.png new file mode 100644 index 0000000000..5a5c281449 Binary files /dev/null and b/apps/block_scout_web/assets/static/android-chrome-192x192.png differ diff --git a/apps/block_scout_web/assets/static/android-chrome-512x512.png b/apps/block_scout_web/assets/static/android-chrome-512x512.png new file mode 100644 index 0000000000..0b4840d41a Binary files /dev/null and b/apps/block_scout_web/assets/static/android-chrome-512x512.png differ diff --git a/apps/block_scout_web/assets/static/apple-touch-icon.png b/apps/block_scout_web/assets/static/apple-touch-icon.png new file mode 100644 index 0000000000..70a5198240 Binary files /dev/null and b/apps/block_scout_web/assets/static/apple-touch-icon.png differ diff --git a/apps/block_scout_web/assets/static/browserconfig.xml b/apps/block_scout_web/assets/static/browserconfig.xml new file mode 100644 index 0000000000..b3930d0f04 --- /dev/null +++ b/apps/block_scout_web/assets/static/browserconfig.xml @@ -0,0 +1,9 @@ + + + + + + #da532c + + + diff --git a/apps/block_scout_web/assets/static/favicon-16x16.png b/apps/block_scout_web/assets/static/favicon-16x16.png new file mode 100644 index 0000000000..fd59e524be Binary files /dev/null and b/apps/block_scout_web/assets/static/favicon-16x16.png differ diff --git a/apps/block_scout_web/assets/static/favicon-32x32.png b/apps/block_scout_web/assets/static/favicon-32x32.png new file mode 100644 index 0000000000..3b9e22b4e3 Binary files /dev/null and b/apps/block_scout_web/assets/static/favicon-32x32.png differ diff --git a/apps/block_scout_web/assets/static/favicon.ico b/apps/block_scout_web/assets/static/favicon.ico index 73de524aaa..293b5abbc7 100644 Binary files a/apps/block_scout_web/assets/static/favicon.ico and b/apps/block_scout_web/assets/static/favicon.ico differ diff --git a/apps/block_scout_web/assets/static/mstile-150x150.png b/apps/block_scout_web/assets/static/mstile-150x150.png new file mode 100644 index 0000000000..f9a6b5a8dd Binary files /dev/null and b/apps/block_scout_web/assets/static/mstile-150x150.png differ diff --git a/apps/block_scout_web/assets/static/safari-pinned-tab.svg b/apps/block_scout_web/assets/static/safari-pinned-tab.svg new file mode 100644 index 0000000000..0c9f9bdc17 --- /dev/null +++ b/apps/block_scout_web/assets/static/safari-pinned-tab.svg @@ -0,0 +1,39 @@ + + + + +Created by potrace 1.11, written by Peter Selinger 2001-2013 + + + + + diff --git a/apps/block_scout_web/assets/static/site.webmanifest b/apps/block_scout_web/assets/static/site.webmanifest new file mode 100644 index 0000000000..b20abb7cbb --- /dev/null +++ b/apps/block_scout_web/assets/static/site.webmanifest @@ -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" +} diff --git a/apps/block_scout_web/lib/block_scout_web/chain.ex b/apps/block_scout_web/lib/block_scout_web/chain.ex index 7a3be7a6ee..c41229acec 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex index 1887c90ffd..1080bde074 100644 --- a/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex +++ b/apps/block_scout_web/lib/block_scout_web/channels/transaction_channel.ex @@ -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 }) diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex index 48d01318d1..80a41bf4a3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex index 5924ad658d..1220231a30 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/block_transaction_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex index b9398ca056..70ec6219f3 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/smart_contract_controller.ex @@ -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, diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex index 421c0c38de..be3567f47c 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/transaction_log_controller.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/notifier.ex b/apps/block_scout_web/lib/block_scout_web/notifier.ex index 023adfb87a..a67012b75e 100644 --- a/apps/block_scout_web/lib/block_scout_web/notifier.ex +++ b/apps/block_scout_web/lib/block_scout_web/notifier.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/router.ex b/apps/block_scout_web/lib/block_scout_web/router.ex index 0eef001ade..93e6532cd3 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex index 4d1ed942ae..fb4a6fd400 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_balance_card.html.eex @@ -1,4 +1,4 @@ -
+

<%= gettext "Balance" %>

diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex index aef2b60213..ef8b5cd66c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/overview.html.eex @@ -32,24 +32,24 @@ <%= link(token_title(@address.token), to: token_path(@conn, :show, @address.hash), "data-test": "token_hash_link" ) %> <% end %> - <%= if contract?(@address) do %> - - <%= 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) - ) %> +
+ <%= if contract?(@address) do %> + + <%= 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" - ) %> - - <% end %> -
+ <%= 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" + ) %> + + <% end %>
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex index 1a62b895f5..f8378f477d 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_contract/index.html.eex @@ -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 %> <% end %> <% end %> - <%= if smart_contract_with_read_only_functions?(@address) do %> + <%= if BlockScoutWeb.AddressView.smart_contract_with_read_only_functions?(@address) do %> @@ -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"]) ) %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex index a2cea8f5da..3a1f9d0376 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/layout/app.html.eex @@ -6,6 +6,16 @@ <%= gettext "BlockScout" %> "> + + "> + "> + "> + "> + " color="#5bbad5"> + "> + + "> + diff --git a/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex index 7a37f3d93b..4990a69919 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/pending_transaction/index.html.eex @@ -1,4 +1,4 @@ -
+
@@ -43,36 +43,28 @@
+
+ +
+
+ +

<%= gettext "Transactions" %>

-

<%= gettext("Showing %{count} Pending Transactions", count: @pending_transaction_count) %>

- - <%= for transaction <- @transactions do %> -
-
-
- <%= BlockScoutWeb.TransactionView.transaction_display_type(transaction) %> -
<%= BlockScoutWeb.TransactionView.formatted_status(transaction) %>
-
-
- <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: transaction.hash %> - - <%= render BlockScoutWeb.AddressView, "_link.html", address: transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(transaction.from_address) %> - → - <%= 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 %> - - - <%= BlockScoutWeb.TransactionView.value(transaction, include_label: false) %> <%= gettext "Ether" %> - <%= BlockScoutWeb.TransactionView.formatted_fee(transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %> - +

+ <%= gettext("Showing") %> + <%= Cldr.Number.to_string!(@pending_transaction_count, format: "#,###") %> + <%= gettext("Pending Transactions") %> +

+ + <%= for transaction <- @transactions do %> + <%= render BlockScoutWeb.TransactionView, "_pending_tile.html", transaction: transaction %> + <% end %> + -
-
-
- <% end %> <%= if @next_page_params do %> <%= link( gettext("Older"), diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex new file mode 100644 index 0000000000..d92872e2a5 --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_pending_tile.html.eex @@ -0,0 +1,24 @@ +
+
+
+ <%= BlockScoutWeb.TransactionView.transaction_display_type(@transaction) %> +
<%= BlockScoutWeb.TransactionView.formatted_status(@transaction) %>
+
+
+ <%= render BlockScoutWeb.TransactionView, "_link.html", transaction_hash: @transaction.hash %> + + <%= render BlockScoutWeb.AddressView, "_link.html", address: @transaction.from_address, contract: BlockScoutWeb.AddressView.contract?(@transaction.from_address) %> + → + <%= 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 %> + + + <%= BlockScoutWeb.TransactionView.value(@transaction, include_label: false) %> <%= gettext "Ether" %> + <%= BlockScoutWeb.TransactionView.formatted_fee(@transaction, denomination: :ether, include_label: false) %> <%= gettext "TX Fee" %> + +
+
+
diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex index 778824848f..fb5b733494 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/_tile.html.eex @@ -51,7 +51,7 @@
<% [first_token_transfer | remaining_token_transfers]= @transaction.token_transfers %> <%= render "_token_transfer.html", address: assigns[:current_address], token_transfer: first_token_transfer %> -
+
<%= 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 %>
- <%= 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" %>
<% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex index 0d82093f03..6ed329392e 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/transaction/index.html.eex @@ -54,7 +54,11 @@

<%= gettext "Transactions" %>

-

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

+

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

<%= for transaction <- @transactions do %> <%= render BlockScoutWeb.TransactionView, "_tile.html", transaction: transaction %> diff --git a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex index 4e95641454..3f453e0478 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/address_contract_view.ex @@ -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 diff --git a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex index 7a7637fa57..1da0d7e2eb 100644 --- a/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex +++ b/apps/block_scout_web/lib/block_scout_web/views/transaction_view.ex @@ -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 diff --git a/apps/block_scout_web/lib/phoenix/param.ex b/apps/block_scout_web/lib/phoenix/param.ex index 7abe4d1d42..f1968f40c9 100644 --- a/apps/block_scout_web/lib/phoenix/param.ex +++ b/apps/block_scout_web/lib/phoenix/param.ex @@ -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 diff --git a/apps/block_scout_web/priv/gettext/default.pot b/apps/block_scout_web/priv/gettext/default.pot index 1db4c0cfb7..ddac06d944 100644 --- a/apps/block_scout_web/priv/gettext/default.pot +++ b/apps/block_scout_web/priv/gettext/default.pot @@ -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 "" diff --git a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po index cacf084173..74e593ef62 100644 --- a/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po +++ b/apps/block_scout_web/priv/gettext/en/LC_MESSAGES/default.po @@ -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 "" diff --git a/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs b/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs index 69ba3a6493..3335b859d6 100644 --- a/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs +++ b/apps/block_scout_web/test/block_scout_web/channels/transaction_channel_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs index 0cdb3ae8ad..2bd2171b02 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/address_contract_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs index 10e3cbfb20..0c1c41c70c 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/block_transaction_controller_test.exs @@ -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) diff --git a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs index a8011dce5e..678edbb0af 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/chain_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs index e69e5e7316..892b05692e 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/smart_contract_controller_test.exs @@ -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() diff --git a/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs b/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs index eb1550fd5a..c84525219f 100644 --- a/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs +++ b/apps/block_scout_web/test/block_scout_web/controllers/transaction_log_controller_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex b/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex index 9c2f3ba3e5..d4ad3ae449 100644 --- a/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex +++ b/apps/block_scout_web/test/block_scout_web/features/pages/address_page.ex @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs index aea6abe896..c89847d0c6 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_addresses_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs b/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs index f2b522808c..fc0fe78dee 100644 --- a/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs +++ b/apps/block_scout_web/test/block_scout_web/features/viewing_transactions_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/views/error_helpers_test.exs b/apps/block_scout_web/test/block_scout_web/views/error_helpers_test.exs index f61256f383..ffa3899fba 100644 --- a/apps/block_scout_web/test/block_scout_web/views/error_helpers_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/error_helpers_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs index 932d60ef1b..33d41d9570 100644 --- a/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/layout_view_test.exs @@ -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 diff --git a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs index 60ce701647..82df6450a6 100644 --- a/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs +++ b/apps/block_scout_web/test/block_scout_web/views/transaction_view_test.exs @@ -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 diff --git a/apps/block_scout_web/test/phoenix/param/explorer/chain/block_test.exs b/apps/block_scout_web/test/phoenix/param/explorer/chain/block_test.exs new file mode 100644 index 0000000000..38ff7d5e54 --- /dev/null +++ b/apps/block_scout_web/test/phoenix/param/explorer/chain/block_test.exs @@ -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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex index 35d72e8aef..354088f078 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc.ex @@ -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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex index 9f90c66c66..ebade1d4da 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/block.ex @@ -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` diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex index 74f9bc5bd0..a6964e17db 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/blocks.ex @@ -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` diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex index d3cae4a83b..c0642d1f61 100644 --- a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/transactions.ex @@ -8,6 +8,7 @@ defmodule EthereumJSONRPC.Transactions do alias EthereumJSONRPC.Transaction @type elixir :: [Transaction.elixir()] + @type params :: [Transaction.params()] @type t :: [Transaction.t()] @doc """ diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncle.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncle.ex new file mode 100644 index 0000000000..871f0daa7d --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncle.ex @@ -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 diff --git a/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncles.ex b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncles.ex new file mode 100644 index 0000000000..6755a439be --- /dev/null +++ b/apps/ethereum_jsonrpc/lib/ethereum_jsonrpc/uncles.ex @@ -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 diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncle_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncle_test.exs new file mode 100644 index 0000000000..7cc8f18acb --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncle_test.exs @@ -0,0 +1,5 @@ +defmodule EthereumJSONRPC.UncleTest do + use ExUnit.Case, async: true + + doctest EthereumJSONRPC.Uncle +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncles_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncles_test.exs new file mode 100644 index 0000000000..b38e3ad1cd --- /dev/null +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc/uncles_test.exs @@ -0,0 +1,5 @@ +defmodule EthereumJSONRPC.UnclesTest do + use ExUnit.Case, async: true + + doctest EthereumJSONRPC.Uncles +end diff --git a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs index 4bf522d178..b21a08d801 100644 --- a/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs +++ b/apps/ethereum_jsonrpc/test/ethereum_jsonrpc_test.exs @@ -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" }, diff --git a/apps/explorer/lib/explorer/chain.ex b/apps/explorer/lib/explorer/chain.ex index c443387679..88b4db01ed 100644 --- a/apps/explorer/lib/explorer/chain.ex +++ b/apps/explorer/lib/explorer/chain.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/block.ex b/apps/explorer/lib/explorer/chain/block.ex index 34b6e47ac1..7194d14344 100644 --- a/apps/explorer/lib/explorer/chain/block.ex +++ b/apps/explorer/lib/explorer/chain/block.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/block/second_degree_relation.ex b/apps/explorer/lib/explorer/chain/block/second_degree_relation.ex new file mode 100644 index 0000000000..986d4d5e03 --- /dev/null +++ b/apps/explorer/lib/explorer/chain/block/second_degree_relation.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/import.ex b/apps/explorer/lib/explorer/chain/import.ex index c9cb68a0b0..2704fa4157 100644 --- a/apps/explorer/lib/explorer/chain/import.ex +++ b/apps/explorer/lib/explorer/chain/import.ex @@ -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 diff --git a/apps/explorer/lib/explorer/chain/transaction.ex b/apps/explorer/lib/explorer/chain/transaction.ex index 39881284af..16873920a9 100644 --- a/apps/explorer/lib/explorer/chain/transaction.ex +++ b/apps/explorer/lib/explorer/chain/transaction.ex @@ -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, diff --git a/apps/explorer/lib/explorer/chain/transaction/fork.ex b/apps/explorer/lib/explorer/chain/transaction/fork.ex new file mode 100644 index 0000000000..74e2fc921d --- /dev/null +++ b/apps/explorer/lib/explorer/chain/transaction/fork.ex @@ -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 diff --git a/apps/explorer/lib/explorer/smart_contract/reader.ex b/apps/explorer/lib/explorer/smart_contract/reader.ex index 523b2bca8b..efdf7360cd 100644 --- a/apps/explorer/lib/explorer/smart_contract/reader.ex +++ b/apps/explorer/lib/explorer/smart_contract/reader.ex @@ -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. """ diff --git a/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs b/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs index 468dfd1bd5..faacee0d13 100644 --- a/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs +++ b/apps/explorer/priv/repo/migrations/20180117221922_create_blocks.exs @@ -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 diff --git a/apps/explorer/priv/repo/migrations/20180917182319_create_block_second_degree_relations.exs b/apps/explorer/priv/repo/migrations/20180917182319_create_block_second_degree_relations.exs new file mode 100644 index 0000000000..b6477a3aae --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20180917182319_create_block_second_degree_relations.exs @@ -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 diff --git a/apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs b/apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs new file mode 100644 index 0000000000..06e77dcd41 --- /dev/null +++ b/apps/explorer/priv/repo/migrations/20180918200001_create_transaction_fork.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain/block/second_degree_relation_test.exs b/apps/explorer/test/explorer/chain/block/second_degree_relation_test.exs new file mode 100644 index 0000000000..66f1097f16 --- /dev/null +++ b/apps/explorer/test/explorer/chain/block/second_degree_relation_test.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain/import_test.exs b/apps/explorer/test/explorer/chain/import_test.exs index dd73ea74b7..1e39d49983 100644 --- a/apps/explorer/test/explorer/chain/import_test.exs +++ b/apps/explorer/test/explorer/chain/import_test.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain/transaction/fork_test.exs b/apps/explorer/test/explorer/chain/transaction/fork_test.exs new file mode 100644 index 0000000000..d5028af6ab --- /dev/null +++ b/apps/explorer/test/explorer/chain/transaction/fork_test.exs @@ -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 diff --git a/apps/explorer/test/explorer/chain_test.exs b/apps/explorer/test/explorer/chain_test.exs index 0c0ed7ec18..cc196e8af2 100644 --- a/apps/explorer/test/explorer/chain_test.exs +++ b/apps/explorer/test/explorer/chain_test.exs @@ -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) diff --git a/apps/explorer/test/explorer/smart_contract/reader_test.exs b/apps/explorer/test/explorer/smart_contract/reader_test.exs index 77f0874c08..5452c10e2d 100644 --- a/apps/explorer/test/explorer/smart_contract/reader_test.exs +++ b/apps/explorer/test/explorer/smart_contract/reader_test.exs @@ -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 diff --git a/apps/explorer/test/support/factory.ex b/apps/explorer/test/support/factory.ex index 4e4972e45d..e4c05a4abd 100644 --- a/apps/explorer/test/support/factory.ex +++ b/apps/explorer/test/support/factory.ex @@ -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, diff --git a/apps/indexer/lib/indexer/address/coin_balances.ex b/apps/indexer/lib/indexer/address/coin_balances.ex index 895810aef7..6955b085b4 100644 --- a/apps/indexer/lib/indexer/address/coin_balances.ex +++ b/apps/indexer/lib/indexer/address/coin_balances.ex @@ -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 diff --git a/apps/indexer/lib/indexer/block/catchup/fetcher.ex b/apps/indexer/lib/indexer/block/catchup/fetcher.ex index 4dd7d97f44..b387468f63 100644 --- a/apps/indexer/lib/indexer/block/catchup/fetcher.ex +++ b/apps/indexer/lib/indexer/block/catchup/fetcher.ex @@ -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) diff --git a/apps/indexer/lib/indexer/block/fetcher.ex b/apps/indexer/lib/indexer/block/fetcher.ex index ee2485c8ab..312b76cc47 100644 --- a/apps/indexer/lib/indexer/block/fetcher.ex +++ b/apps/indexer/lib/indexer/block/fetcher.ex @@ -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) diff --git a/apps/indexer/lib/indexer/block/realtime/fetcher.ex b/apps/indexer/lib/indexer/block/realtime/fetcher.ex index f55a298a69..aac96a77e8 100644 --- a/apps/indexer/lib/indexer/block/realtime/fetcher.ex +++ b/apps/indexer/lib/indexer/block/realtime/fetcher.ex @@ -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( diff --git a/apps/indexer/lib/indexer/block/supervisor.ex b/apps/indexer/lib/indexer/block/supervisor.ex index fcb8245ecd..a9c2ab8728 100644 --- a/apps/indexer/lib/indexer/block/supervisor.ex +++ b/apps/indexer/lib/indexer/block/supervisor.ex @@ -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 ) diff --git a/apps/indexer/lib/indexer/block/uncle/fetcher.ex b/apps/indexer/lib/indexer/block/uncle/fetcher.ex new file mode 100644 index 0000000000..032679b12c --- /dev/null +++ b/apps/indexer/lib/indexer/block/uncle/fetcher.ex @@ -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 diff --git a/apps/indexer/lib/indexer/block/uncle/supervisor.ex b/apps/indexer/lib/indexer/block/uncle/supervisor.ex new file mode 100644 index 0000000000..87daf8bdc6 --- /dev/null +++ b/apps/indexer/lib/indexer/block/uncle/supervisor.ex @@ -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 diff --git a/apps/indexer/lib/indexer/pending_transaction/fetcher.ex b/apps/indexer/lib/indexer/pending_transaction/fetcher.ex index 7d27c1606a..bfb9f46d3c 100644 --- a/apps/indexer/lib/indexer/pending_transaction/fetcher.ex +++ b/apps/indexer/lib/indexer/pending_transaction/fetcher.ex @@ -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} }) diff --git a/apps/indexer/test/indexer/address/coin_balances_test.exs b/apps/indexer/test/indexer/address/coin_balances_test.exs index 15047621ed..ef49d4033b 100644 --- a/apps/indexer/test/indexer/address/coin_balances_test.exs +++ b/apps/indexer/test/indexer/address/coin_balances_test.exs @@ -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 diff --git a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs index d79172a764..02190c8138 100644 --- a/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs +++ b/apps/indexer/test/indexer/block/catchup/bound_interval_supervisor_test.exs @@ -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 diff --git a/apps/indexer/test/indexer/block/catchup/fetcher_test.exs b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs new file mode 100644 index 0000000000..bdcec64fa8 --- /dev/null +++ b/apps/indexer/test/indexer/block/catchup/fetcher_test.exs @@ -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 diff --git a/apps/indexer/test/indexer/block/fetcher_test.exs b/apps/indexer/test/indexer/block/fetcher_test.exs index 196cd6137a..1c5475c1ca 100644 --- a/apps/indexer/test/indexer/block/fetcher_test.exs +++ b/apps/indexer/test/indexer/block/fetcher_test.exs @@ -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, diff --git a/apps/indexer/test/indexer/block/realtime_test.exs b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs similarity index 98% rename from apps/indexer/test/indexer/block/realtime_test.exs rename to apps/indexer/test/indexer/block/realtime/fetcher_test.exs index 9b42fc4533..68dbdfb741 100644 --- a/apps/indexer/test/indexer/block/realtime_test.exs +++ b/apps/indexer/test/indexer/block/realtime/fetcher_test.exs @@ -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"] } }, %{ diff --git a/apps/indexer/test/indexer/block/uncle/fetcher_test.exs b/apps/indexer/test/indexer/block/uncle/fetcher_test.exs new file mode 100644 index 0000000000..970f1700a0 --- /dev/null +++ b/apps/indexer/test/indexer/block/uncle/fetcher_test.exs @@ -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 diff --git a/apps/indexer/test/support/indexer/block/uncle/supervisor/case.ex b/apps/indexer/test/support/indexer/block/uncle/supervisor/case.ex new file mode 100644 index 0000000000..331ddfebd0 --- /dev/null +++ b/apps/indexer/test/support/indexer/block/uncle/supervisor/case.ex @@ -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