diff --git a/.circleci/config.yml b/.circleci/config.yml index f9db91d227..92a2b1d9e5 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -14,6 +14,8 @@ jobs: working_directory: ~/app steps: + - run: sudo apt-get update; sudo apt-get -y install autoconf build-essential libgmp3-dev libtool + - checkout - run: mix local.hex --force @@ -70,6 +72,11 @@ jobs: - run: mix compile + # Ensure NIF is compiled for libsecp256k1 + - run: + command: make + working_directory: "deps/libsecp256k1" + # `deps` needs to be cached with `_build` because `_build` will symlink into `deps` - save_cache: diff --git a/apps/block_scout_web/assets/__tests__/pages/address.js b/apps/block_scout_web/assets/__tests__/pages/address.js index 7691660a40..55eedad035 100644 --- a/apps/block_scout_web/assets/__tests__/pages/address.js +++ b/apps/block_scout_web/assets/__tests__/pages/address.js @@ -1,300 +1,327 @@ import { reducer, initialState } from '../../js/pages/address' -describe('PAGE_LOAD', () => { - test('page 1 without filter', () => { - const state = initialState +describe('RECEIVED_NEW_BLOCK', () => { + test('with new block', () => { + const state = Object.assign({}, initialState, { + validationCount: 30, + validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: false, - pendingTransactionHashes: ['1'] + type: 'RECEIVED_NEW_BLOCK', + msg: { blockNumber: 2, blockHtml: 'test 2' } } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(false) - expect(output.filter).toBe(undefined) - expect(output.pendingTransactionHashes).toEqual(['1']) + expect(output.validationCount).toEqual(31) + expect(output.validatedBlocks).toEqual([ + { blockNumber: 2, blockHtml: 'test 2' }, + { blockNumber: 1, blockHtml: 'test 1' } + ]) }) - test('page 2 without filter', () => { - const state = initialState + test('when channel has been disconnected', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true, + validationCount: 30, + validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: true, - pendingTransactionHashes: ['1'] + type: 'RECEIVED_NEW_BLOCK', + msg: { blockNumber: 2, blockHtml: 'test 2' } } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(true) - expect(output.filter).toBe(undefined) - expect(output.pendingTransactionHashes).toEqual(['1']) + expect(output.validationCount).toEqual(30) + expect(output.validatedBlocks).toEqual([ + { blockNumber: 1, blockHtml: 'test 1' } + ]) }) - test('page 1 with "to" filter', () => { - const state = initialState + test('beyond page one', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + validationCount: 30, + validatedBlocks: [{ blockNumber: 1, blockHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: false, - filter: 'to' + type: 'RECEIVED_NEW_BLOCK', + msg: { blockNumber: 2, blockHtml: 'test 2' } } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(false) - expect(output.filter).toBe('to') + expect(output.validationCount).toEqual(31) + expect(output.validatedBlocks).toEqual([ + { blockNumber: 1, blockHtml: 'test 1' } + ]) }) - test('page 2 with "to" filter', () => { - const state = initialState +}) + +describe('RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', () => { + test('with new internal transaction', () => { + const state = Object.assign({}, initialState, { + internalTransactions: [{ internalTransactionHtml: 'test 1' }] + }) const action = { - type: 'PAGE_LOAD', - addressHash: '1234', - beyondPageOne: true, - filter: 'to' + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.addressHash).toBe('1234') - expect(output.beyondPageOne).toBe(true) - expect(output.filter).toBe('to') + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 2' }, + { internalTransactionHtml: 'test 1' } + ]) }) -}) - -test('CHANNEL_DISCONNECTED', () => { - const state = initialState - const action = { - type: 'CHANNEL_DISCONNECTED' - } - const output = reducer(state, action) - - expect(output.channelDisconnected).toBe(true) -}) - -test('RECEIVED_UPDATED_BALANCE', () => { - const state = initialState - const action = { - type: 'RECEIVED_UPDATED_BALANCE', - msg: { - balance: 'hello world' + test('with batch of new internal transactions', () => { + const state = Object.assign({}, initialState, { + internalTransactions: [{ internalTransactionHtml: 'test 1' }] + }) + const action = { + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [ + { internalTransactionHtml: 'test 2' }, + { internalTransactionHtml: 'test 3' }, + { internalTransactionHtml: 'test 4' }, + { internalTransactionHtml: 'test 5' }, + { internalTransactionHtml: 'test 6' }, + { internalTransactionHtml: 'test 7' }, + { internalTransactionHtml: 'test 8' }, + { internalTransactionHtml: 'test 9' }, + { internalTransactionHtml: 'test 10' }, + { internalTransactionHtml: 'test 11' }, + { internalTransactionHtml: 'test 12' }, + { internalTransactionHtml: 'test 13' } + ] } - } - const output = reducer(state, action) - - expect(output.balance).toBe('hello world') -}) + const output = reducer(state, action) -describe('RECEIVED_NEW_PENDING_TRANSACTION', () => { - test('single transaction', () => { - const state = initialState + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 1' } + ]) + expect(output.internalTransactionsBatch).toEqual([ + { internalTransactionHtml: 'test 13' }, + { internalTransactionHtml: 'test 12' }, + { internalTransactionHtml: 'test 11' }, + { internalTransactionHtml: 'test 10' }, + { internalTransactionHtml: 'test 9' }, + { internalTransactionHtml: 'test 8' }, + { internalTransactionHtml: 'test 7' }, + { internalTransactionHtml: 'test 6' }, + { internalTransactionHtml: 'test 5' }, + { internalTransactionHtml: 'test 4' }, + { internalTransactionHtml: 'test 3' }, + { internalTransactionHtml: 'test 2' }, + ]) + }) + test('after batch of new internal transactions', () => { + const state = Object.assign({}, initialState, { + internalTransactionsBatch: [{ internalTransactionHtml: 'test 1' }] + }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x00', - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [ + { internalTransactionHtml: 'test 2' } + ] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual(['test']) - expect(output.transactionCount).toEqual(null) + expect(output.internalTransactionsBatch).toEqual([ + { internalTransactionHtml: 'test 2' }, + { internalTransactionHtml: 'test 1' } + ]) }) - test('single transaction after single transaction', () => { + test('when channel has been disconnected', () => { const state = Object.assign({}, initialState, { - newPendingTransactions: ['test 1'] + channelDisconnected: true, + internalTransactions: [{ internalTransactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x02', - transactionHtml: 'test 2' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual(['test 1', 'test 2']) - expect(output.pendingTransactionHashes.length).toEqual(1) + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 1' } + ]) }) - test('after disconnection', () => { + test('beyond page one', () => { const state = Object.assign({}, initialState, { - channelDisconnected: true + beyondPageOne: true, + internalTransactions: [{ internalTransactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x00', - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual([]) - expect(output.pendingTransactionHashes).toEqual([]) + expect(output.internalTransactions).toEqual([ + { internalTransactionHtml: 'test 1' } + ]) }) - test('on page 2', () => { + test('with filtered out internal transaction', () => { const state = Object.assign({}, initialState, { - beyondPageOne: true + filter: 'to' }) const action = { - type: 'RECEIVED_NEW_PENDING_TRANSACTION', - msg: { - transactionHash: '0x00', - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: [{ internalTransactionHtml: 'test 2' }] } const output = reducer(state, action) - expect(output.newPendingTransactions).toEqual([]) - expect(output.pendingTransactionHashes).toEqual([]) + expect(output.internalTransactions).toEqual([]) }) }) -describe('RECEIVED_NEW_TRANSACTION', () => { - test('single transaction', () => { +describe('RECEIVED_NEW_PENDING_TRANSACTION', () => { + test('with new pending transaction', () => { const state = Object.assign({}, initialState, { - addressHash: '0x111' + pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([{ transactionHtml: 'test' }]) - expect(output.transactionCount).toEqual(null) + expect(output.pendingTransactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test 2' }, + { transactionHash: 1, transactionHtml: 'test 1' } + ]) }) - test('single transaction after single transaction', () => { + test('when channel has been disconnected', () => { const state = Object.assign({}, initialState, { - newTransactions: [{ transactionHtml: 'test 1' }] + channelDisconnected: true, + pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test 2' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { transactionHtml: 'test 1' }, - { transactionHtml: 'test 2' } + expect(output.pendingTransactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } ]) }) - test('after disconnection', () => { + test('beyond page one', () => { const state = Object.assign({}, initialState, { - channelDisconnected: true + beyondPageOne: true, + pendingTransactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) + expect(output.pendingTransactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } + ]) }) - test('on page 2', () => { + test('with filtered out pending transaction', () => { const state = Object.assign({}, initialState, { - beyondPageOne: true, - transactionCount: 1, - addressHash: '0x111' + filter: 'to' }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', - msg: { - transactionHtml: 'test' - } + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) - expect(output.transactionCount).toEqual(1) + expect(output.pendingTransactions).toEqual([]) }) - test('transaction from current address with "from" filter', () => { +}) + +describe('RECEIVED_NEW_TRANSACTION', () => { + test('with new transaction', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'from' + pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }], + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - fromAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { fromAddressHash: '1234', transactionHtml: 'test' } + expect(output.pendingTransactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test 2', validated: true } + ]) + expect(output.transactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test 2' }, + { transactionHash: 1, transactionHtml: 'test 1' } ]) }) - test('transaction from current address with "to" filter', () => { + test('when channel has been disconnected', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'to' + channelDisconnected: true, + pendingTransactions: [{ transactionHash: 2, transactionHtml: 'test' }], + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - fromAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) + expect(output.pendingTransactions).toEqual([ + { transactionHash: 2, transactionHtml: 'test' } + ]) + expect(output.transactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } + ]) }) - test('transaction to current address with "to" filter', () => { + test('beyond page one', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'to' + beyondPageOne: true, + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - toAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { toAddressHash: '1234', transactionHtml: 'test' } + expect(output.pendingTransactions).toEqual([]) + expect(output.transactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' } ]) }) - test('transaction to current address with "from" filter', () => { + test('with filtered out transaction', () => { const state = Object.assign({}, initialState, { - addressHash: '1234', - filter: 'from' + filter: 'to' }) const action = { type: 'RECEIVED_NEW_TRANSACTION', - msg: { - toAddressHash: '1234', - transactionHtml: 'test' - } + msg: { transactionHash: 2, transactionHtml: 'test 2' } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([]) + expect(output.transactions).toEqual([]) }) - test('single transaction collated from pending', () => { - const state = initialState +}) + +describe('RECEIVED_NEXT_TRANSACTIONS_PAGE', () => { + test('with new transaction page', () => { + const state = Object.assign({}, initialState, { + loadingNextPage: true, + nextPageUrl: '1', + transactions: [{ transactionHash: 1, transactionHtml: 'test 1' }] + }) const action = { - type: 'RECEIVED_NEW_TRANSACTION', + type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE', msg: { - transactionHash: '0x00', - transactionHtml: 'test' + nextPageUrl: '2', + transactions: [{ transactionHash: 2, transactionHtml: 'test 2' }] } } const output = reducer(state, action) - expect(output.newTransactions).toEqual([ - { transactionHash: '0x00', transactionHtml: 'test' } + expect(output.loadingNextPage).toEqual(false) + expect(output.nextPageUrl).toEqual('2') + expect(output.transactions).toEqual([ + { transactionHash: 1, transactionHtml: 'test 1' }, + { transactionHash: 2, transactionHtml: 'test 2' } ]) - expect(output.transactionCount).toEqual(null) }) }) diff --git a/apps/block_scout_web/assets/__tests__/pages/block.js b/apps/block_scout_web/assets/__tests__/pages/blocks.js similarity index 98% rename from apps/block_scout_web/assets/__tests__/pages/block.js rename to apps/block_scout_web/assets/__tests__/pages/blocks.js index d854fb9746..2ddad6a5a3 100644 --- a/apps/block_scout_web/assets/__tests__/pages/block.js +++ b/apps/block_scout_web/assets/__tests__/pages/blocks.js @@ -1,4 +1,4 @@ -import { reducer, initialState } from '../../js/pages/block' +import { reducer, initialState } from '../../js/pages/blocks' test('CHANNEL_DISCONNECTED', () => { const state = initialState diff --git a/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js new file mode 100644 index 0000000000..25d03aed61 --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/pending_transactions.js @@ -0,0 +1,232 @@ +import { reducer, initialState } from '../../js/pages/pending_transactions' + +test('CHANNEL_DISCONNECTED', () => { + const state = initialState + const action = { + type: 'CHANNEL_DISCONNECTED' + } + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) +}) + +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([]) + }) + 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.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) + }) +}) diff --git a/apps/block_scout_web/assets/__tests__/pages/transaction.js b/apps/block_scout_web/assets/__tests__/pages/transaction.js index ea4eb3595f..55aa68f4b4 100644 --- a/apps/block_scout_web/assets/__tests__/pages/transaction.js +++ b/apps/block_scout_web/assets/__tests__/pages/transaction.js @@ -1,16 +1,5 @@ import { reducer, initialState } from '../../js/pages/transaction' -test('CHANNEL_DISCONNECTED', () => { - const state = initialState - const action = { - type: 'CHANNEL_DISCONNECTED' - } - const output = reducer(state, action) - - expect(output.channelDisconnected).toBe(true) - expect(output.batchCountAccumulator).toBe(0) -}) - test('RECEIVED_NEW_BLOCK', () => { const state = { ...initialState, blockNumber: 1 } const action = { @@ -23,374 +12,3 @@ 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 - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual(['test']) - expect(output.batchCountAccumulator).toEqual(0) - expect(output.transactionCount).toEqual(1) - }) - test('large batch of transactions', () => { - const state = initialState - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 1' - },{ - transactionHtml: 'test 2' - },{ - transactionHtml: 'test 3' - },{ - transactionHtml: 'test 4' - },{ - transactionHtml: 'test 5' - },{ - transactionHtml: 'test 6' - },{ - transactionHtml: 'test 7' - },{ - transactionHtml: 'test 8' - },{ - transactionHtml: 'test 9' - },{ - transactionHtml: 'test 10' - },{ - transactionHtml: 'test 11' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(11) - expect(output.transactionCount).toEqual(11) - }) - test('single transaction after single transaction', () => { - const state = Object.assign({}, initialState, { - newTransactions: ['test 1'] - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 2' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual(['test 1', 'test 2']) - expect(output.batchCountAccumulator).toEqual(0) - }) - test('single transaction after large batch of transactions', () => { - const state = Object.assign({}, initialState, { - batchCountAccumulator: 11 - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 12' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(12) - }) - test('large batch of transactions after large batch of transactions', () => { - const state = Object.assign({}, initialState, { - batchCountAccumulator: 11 - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test 12' - },{ - transactionHtml: 'test 13' - },{ - transactionHtml: 'test 14' - },{ - transactionHtml: 'test 15' - },{ - transactionHtml: 'test 16' - },{ - transactionHtml: 'test 17' - },{ - transactionHtml: 'test 18' - },{ - transactionHtml: 'test 19' - },{ - transactionHtml: 'test 20' - },{ - transactionHtml: 'test 21' - },{ - transactionHtml: 'test 22' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(22) - }) - test('after disconnection', () => { - const state = Object.assign({}, initialState, { - channelDisconnected: true - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(0) - }) - test('on page 2+', () => { - const state = Object.assign({}, initialState, { - beyondPageOne: true, - transactionCount: 1 - }) - const action = { - type: 'RECEIVED_NEW_TRANSACTION_BATCH', - msgs: [{ - transactionHtml: 'test' - }] - } - const output = reducer(state, action) - - expect(output.newTransactions).toEqual([]) - expect(output.batchCountAccumulator).toEqual(0) - expect(output.transactionCount).toEqual(2) - }) -}) diff --git a/apps/block_scout_web/assets/__tests__/pages/transactions.js b/apps/block_scout_web/assets/__tests__/pages/transactions.js new file mode 100644 index 0000000000..f45a4960d4 --- /dev/null +++ b/apps/block_scout_web/assets/__tests__/pages/transactions.js @@ -0,0 +1,160 @@ +import { reducer, initialState } from '../../js/pages/transactions' + +test('CHANNEL_DISCONNECTED', () => { + const state = initialState + const action = { + type: 'CHANNEL_DISCONNECTED' + } + const output = reducer(state, action) + + expect(output.channelDisconnected).toBe(true) + expect(output.batchCountAccumulator).toBe(0) +}) + +describe('RECEIVED_NEW_TRANSACTION_BATCH', () => { + test('single transaction', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual(['test']) + expect(output.batchCountAccumulator).toEqual(0) + expect(output.transactionCount).toEqual(1) + }) + test('large batch of transactions', () => { + const state = initialState + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 1' + },{ + transactionHtml: 'test 2' + },{ + transactionHtml: 'test 3' + },{ + transactionHtml: 'test 4' + },{ + transactionHtml: 'test 5' + },{ + transactionHtml: 'test 6' + },{ + transactionHtml: 'test 7' + },{ + transactionHtml: 'test 8' + },{ + transactionHtml: 'test 9' + },{ + transactionHtml: 'test 10' + },{ + transactionHtml: 'test 11' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(11) + expect(output.transactionCount).toEqual(11) + }) + test('single transaction after single transaction', () => { + const state = Object.assign({}, initialState, { + newTransactions: ['test 1'] + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 2' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual(['test 1', 'test 2']) + expect(output.batchCountAccumulator).toEqual(0) + }) + test('single transaction after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + batchCountAccumulator: 11 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 12' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(12) + }) + test('large batch of transactions after large batch of transactions', () => { + const state = Object.assign({}, initialState, { + batchCountAccumulator: 11 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test 12' + },{ + transactionHtml: 'test 13' + },{ + transactionHtml: 'test 14' + },{ + transactionHtml: 'test 15' + },{ + transactionHtml: 'test 16' + },{ + transactionHtml: 'test 17' + },{ + transactionHtml: 'test 18' + },{ + transactionHtml: 'test 19' + },{ + transactionHtml: 'test 20' + },{ + transactionHtml: 'test 21' + },{ + transactionHtml: 'test 22' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(22) + }) + test('after disconnection', () => { + const state = Object.assign({}, initialState, { + channelDisconnected: true + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(0) + }) + test('on page 2+', () => { + const state = Object.assign({}, initialState, { + beyondPageOne: true, + transactionCount: 1 + }) + const action = { + type: 'RECEIVED_NEW_TRANSACTION_BATCH', + msgs: [{ + transactionHtml: 'test' + }] + } + const output = reducer(state, action) + + expect(output.newTransactions).toEqual([]) + expect(output.batchCountAccumulator).toEqual(0) + expect(output.transactionCount).toEqual(2) + }) +}) diff --git a/apps/block_scout_web/assets/js/app.js b/apps/block_scout_web/assets/js/app.js index 935a325d04..83e3080c5c 100644 --- a/apps/block_scout_web/assets/js/app.js +++ b/apps/block_scout_web/assets/js/app.js @@ -20,6 +20,13 @@ import 'bootstrap' import './locale' +import './pages/address' +import './pages/blocks' +import './pages/chain' +import './pages/pending_transactions' +import './pages/transaction' +import './pages/transactions' + import './lib/clipboard_buttons' import './lib/currency' import './lib/from_now' @@ -37,8 +44,3 @@ import './lib/token_balance_dropdown_search' import './lib/token_transfers_toggle' import './lib/tooltip' import './lib/try_api' - -import './pages/address' -import './pages/block' -import './pages/chain' -import './pages/transaction' diff --git a/apps/block_scout_web/assets/js/lib/from_now.js b/apps/block_scout_web/assets/js/lib/from_now.js index ce00071d21..d0f1d57f63 100644 --- a/apps/block_scout_web/assets/js/lib/from_now.js +++ b/apps/block_scout_web/assets/js/lib/from_now.js @@ -8,8 +8,9 @@ moment.relativeTimeThreshold('m', 60) moment.relativeTimeThreshold('s', 60) moment.relativeTimeThreshold('ss', 1) -export function updateAllAges () { - $('[data-from-now]').each((i, el) => tryUpdateAge(el)) +export function updateAllAges ($container = $(document)) { + $container.find('[data-from-now]').each((i, el) => tryUpdateAge(el)) + return $container } function tryUpdateAge (el) { if (!el.dataset.fromNow) return diff --git a/apps/block_scout_web/assets/js/pages/address.js b/apps/block_scout_web/assets/js/pages/address.js index c3ed5a9ba7..fb969ac73f 100644 --- a/apps/block_scout_web/assets/js/pages/address.js +++ b/apps/block_scout_web/assets/js/pages/address.js @@ -4,47 +4,48 @@ import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' -import { updateAllAges } from '../lib/from_now' +import { createStore, connectElements, batchChannel, listMorph, onScrollBottom } from '../utils' import { updateAllCalculatedUsdValues } from '../lib/currency.js' import { loadTokenBalanceDropdown } from '../lib/token_balance_dropdown' const BATCH_THRESHOLD = 10 +const TRANSACTION_VALIDATED_MOVE_DELAY = 1000 export const initialState = { - addressHash: null, - balance: null, - batchCountAccumulator: 0, - beyondPageOne: null, channelDisconnected: false, + + addressHash: null, filter: null, - newBlock: null, - newInternalTransactions: [], - newPendingTransactions: [], - newTransactions: [], - pendingTransactionHashes: [], + + balance: null, transactionCount: null, - validationCount: null + validationCount: null, + + pendingTransactions: [], + transactions: [], + internalTransactions: [], + internalTransactionsBatch: [], + validatedBlocks: [], + + loadingNextPage: false, + pagingError: false, + nextPageUrl: null, + + beyondPageOne: null } export function reducer (state = initialState, action) { switch (action.type) { - case 'PAGE_LOAD': { - return Object.assign({}, state, { - addressHash: action.addressHash, - beyondPageOne: action.beyondPageOne, - filter: action.filter, - pendingTransactionHashes: action.pendingTransactionHashes, - transactionCount: numeral(action.transactionCount).value(), - validationCount: action.validationCount ? numeral(action.validationCount).value() : null - }) + case 'PAGE_LOAD': + case 'ELEMENTS_LOAD': { + return Object.assign({}, state, _.omit(action, 'type')) } case 'CHANNEL_DISCONNECTED': { if (state.beyondPageOne) return state return Object.assign({}, state, { channelDisconnected: true, - batchCountAccumulator: 0 + internalTransactionsBatch: [] }) } case 'RECEIVED_NEW_BLOCK': { @@ -54,7 +55,10 @@ export function reducer (state = initialState, action) { if (state.beyondPageOne) return Object.assign({}, state, { validationCount }) return Object.assign({}, state, { - newBlock: action.msg.blockHtml, + validatedBlocks: [ + action.msg, + ...state.validatedBlocks + ], validationCount }) } @@ -68,16 +72,19 @@ export function reducer (state = initialState, action) { (state.filter === 'from' && fromAddressHash === state.addressHash) )) - if (!state.batchCountAccumulator && incomingInternalTransactions.length < BATCH_THRESHOLD) { + if (!state.internalTransactionsBatch.length && incomingInternalTransactions.length < BATCH_THRESHOLD) { return Object.assign({}, state, { - newInternalTransactions: [ - ...state.newInternalTransactions, - ..._.map(incomingInternalTransactions, 'internalTransactionHtml') + internalTransactions: [ + ...incomingInternalTransactions.reverse(), + ...state.internalTransactions ] }) } else { return Object.assign({}, state, { - batchCountAccumulator: state.batchCountAccumulator + incomingInternalTransactions.length + internalTransactionsBatch: [ + ...incomingInternalTransactions.reverse(), + ...state.internalTransactionsBatch + ] }) } } @@ -90,16 +97,17 @@ export function reducer (state = initialState, action) { } return Object.assign({}, state, { - newPendingTransactions: [ - ...state.newPendingTransactions, - action.msg.transactionHtml - ], - pendingTransactionHashes: [ - ...state.pendingTransactionHashes, - action.msg.transactionHash + pendingTransactions: [ + action.msg, + ...state.pendingTransactions ] }) } + case 'REMOVE_PENDING_TRANSACTION': { + return Object.assign({}, state, { + pendingTransactions: state.pendingTransactions.filter((transaction) => action.msg.transactionHash !== transaction.transactionHash) + }) + } case 'RECEIVED_NEW_TRANSACTION': { if (state.channelDisconnected) return state @@ -111,15 +119,12 @@ export function reducer (state = initialState, action) { return Object.assign({}, state, { transactionCount }) } - const updatedPendingTransactionHashes = - _.without(state.pendingTransactionHashes, action.msg.transactionHash) - return Object.assign({}, state, { - newTransactions: [ - ...state.newTransactions, - action.msg + pendingTransactions: state.pendingTransactions.map((transaction) => action.msg.transactionHash === transaction.transactionHash ? Object.assign({}, action.msg, { validated: true }) : transaction), + transactions: [ + action.msg, + ...state.transactions ], - pendingTransactionHashes: updatedPendingTransactionHashes, transactionCount: transactionCount }) } @@ -128,109 +133,245 @@ export function reducer (state = initialState, action) { balance: action.msg.balance }) } + case 'LOADING_NEXT_PAGE': { + return Object.assign({}, state, { + loadingNextPage: true + }) + } + case 'PAGING_ERROR': { + return Object.assign({}, state, { + loadingNextPage: false, + pagingError: true + }) + } + case 'RECEIVED_NEXT_TRANSACTIONS_PAGE': { + return Object.assign({}, state, { + loadingNextPage: false, + nextPageUrl: action.msg.nextPageUrl, + transactions: [ + ...state.transactions, + ...action.msg.transactions + ] + }) + } default: return state } } -const $addressDetailsPage = $('[data-page="address-details"]') -if ($addressDetailsPage.length) { - initRedux(reducer, { - main (store) { - const addressHash = $addressDetailsPage[0].dataset.pageAddressHash - const addressChannel = socket.channel(`addresses:${addressHash}`, {}) - const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true)) - store.dispatch({ - type: 'PAGE_LOAD', - addressHash, - beyondPageOne: !!blockNumber, - filter, - pendingTransactionHashes: $('[data-selector="pending-transactions-list"]').children() - .map((index, el) => el.dataset.transactionHash).toArray(), - transactionCount: $('[data-selector="transaction-count"]').text(), - validationCount: $('[data-selector="validation-count"]') ? $('[data-selector="validation-count"]').text() : null - }) - addressChannel.join() - addressChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - addressChannel.on('balance', (msg) => { - store.dispatch({ type: 'RECEIVED_UPDATED_BALANCE', msg: humps.camelizeKeys(msg) }) - }) - addressChannel.on('internal_transaction', batchChannel((msgs) => - store.dispatch({ type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', msgs: humps.camelizeKeys(msgs) }) - )) - addressChannel.on('pending_transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_PENDING_TRANSACTION', msg: humps.camelizeKeys(msg) })) - addressChannel.on('transaction', (msg) => store.dispatch({ type: 'RECEIVED_NEW_TRANSACTION', msg: humps.camelizeKeys(msg) })) - const blocksChannel = socket.channel(`blocks:${addressHash}`, {}) - blocksChannel.join() - blocksChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - blocksChannel.on('new_block', (msg) => store.dispatch({ type: 'RECEIVED_NEW_BLOCK', msg: humps.camelizeKeys(msg) })) +const elements = { + '[data-selector="channel-disconnected-message"]': { + render ($el, state) { + if (state.channelDisconnected) $el.show() + } + }, + '[data-selector="balance-card"]': { + load ($el) { + return { balance: $el.html() } }, - render (state, oldState) { - const $balance = $('[data-selector="balance-card"]') - const $channelBatching = $('[data-selector="channel-batching-message"]') - const $channelBatchingCount = $('[data-selector="channel-batching-count"]') - const $channelDisconnected = $('[data-selector="channel-disconnected-message"]') - const $emptyInternalTransactionsList = $('[data-selector="empty-internal-transactions-list"]') - const $emptyTransactionsList = $('[data-selector="empty-transactions-list"]') - const $internalTransactionsList = $('[data-selector="internal-transactions-list"]') - const $pendingTransactionsCount = $('[data-selector="pending-transactions-count"]') - const $pendingTransactionsList = $('[data-selector="pending-transactions-list"]') - const $transactionCount = $('[data-selector="transaction-count"]') - const $transactionsList = $('[data-selector="transactions-list"]') - const $validationCount = $('[data-selector="validation-count"]') - const $validationsList = $('[data-selector="validations-list"]') - - if ($emptyInternalTransactionsList.length && state.newInternalTransactions.length) window.location.reload() - if ($emptyTransactionsList.length && state.newTransactions.length) window.location.reload() - if (state.channelDisconnected) $channelDisconnected.show() - if (oldState.balance !== state.balance) { - $balance.empty().append(state.balance) - loadTokenBalanceDropdown() - updateAllCalculatedUsdValues() - } - if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) - if (oldState.validationCount !== state.validationCount) $validationCount.empty().append(numeral(state.validationCount).format()) - if (state.batchCountAccumulator) { - $channelBatching.show() - $channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() + render ($el, state, oldState) { + if (oldState.balance === state.balance) return + $el.empty().append(state.balance) + loadTokenBalanceDropdown() + updateAllCalculatedUsdValues() + } + }, + '[data-selector="transaction-count"]': { + load ($el) { + return { transactionCount: numeral($el.text()).value() } + }, + render ($el, state, oldState) { + if (oldState.transactionCount === state.transactionCount) return + $el.empty().append(numeral(state.transactionCount).format()) + } + }, + '[data-selector="validation-count"]': { + load ($el) { + return { validationCount: numeral($el.text()).value } + }, + render ($el, state, oldState) { + if (oldState.validationCount === state.validationCount) return + $el.empty().append(numeral(state.validationCount).format()) + } + }, + '[data-selector="loading-next-page"]': { + render ($el, state) { + if (state.loadingNextPage) { + $el.show() } else { - $channelBatching.hide() + $el.hide() } - if (oldState.newInternalTransactions !== state.newInternalTransactions && $internalTransactionsList.length) { - slideDownPrepend($internalTransactionsList, state.newInternalTransactions.slice(oldState.newInternalTransactions.length).reverse().join('')) - updateAllAges() + } + }, + '[data-selector="paging-error-message"]': { + render ($el, state) { + if (state.pagingError) { + $el.show() } - if (oldState.pendingTransactionHashes.length !== state.pendingTransactionHashes.length && $pendingTransactionsCount.length) { - $pendingTransactionsCount[0].innerHTML = numeral(state.pendingTransactionHashes.length).format() + } + }, + '[data-selector="pending-transactions-list"]': { + load ($el) { + return { + pendingTransactions: $el.children().map((index, el) => ({ + transactionHash: el.dataset.transactionHash, + transactionHtml: el.outerHTML + })).toArray() } - if (oldState.newPendingTransactions !== state.newPendingTransactions && $pendingTransactionsList.length) { - slideDownPrepend($pendingTransactionsList, state.newPendingTransactions.slice(oldState.newPendingTransactions.length).reverse().join('')) - updateAllAges() + }, + render ($el, state, oldState) { + if (oldState.pendingTransactions === state.pendingTransactions) return + const container = $el[0] + const newElements = _.map(state.pendingTransactions, ({ transactionHtml }) => $(transactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.transactionHash' }) + } + }, + '[data-selector="pending-transactions-count"]': { + render ($el, state, oldState) { + if (oldState.pendingTransactions === state.pendingTransactions) return + $el[0].innerHTML = numeral(state.pendingTransactions.filter(({ validated }) => !validated).length).format() + } + }, + '[data-selector="transactions-list"]': { + load ($el, store) { + return { + transactions: $el.children().map((index, el) => ({ + transactionHash: el.dataset.transactionHash, + transactionHtml: el.outerHTML + })).toArray() } - if (oldState.newTransactions !== state.newTransactions && $transactionsList.length) { - const newlyValidatedTransactions = state.newTransactions.slice(oldState.newTransactions.length).reverse() - newlyValidatedTransactions.forEach(({ transactionHash, transactionHtml }) => { - let $transaction = $(`[data-selector="pending-transactions-list"] [data-transaction-hash="${transactionHash}"]`) - $transaction.html($(transactionHtml).html()) - if ($transaction.is(':visible')) { - setTimeout(() => { - $transaction.addClass('shrink-out') - setTimeout(() => { - slideUpRemove($transaction) - slideDownPrepend($transactionsList, transactionHtml) - }, 400) - }, 1000) - } else { - $transaction.remove() - slideDownPrepend($transactionsList, transactionHtml) - } - }) - updateAllAges() + }, + render ($el, state, oldState) { + if (oldState.transactions === state.transactions) return + function updateTransactions () { + const container = $el[0] + const newElements = _.map(state.transactions, ({ transactionHtml }) => $(transactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.transactionHash' }) + } + if ($('[data-selector="pending-transactions-list"]').is(':visible')) { + setTimeout(updateTransactions, TRANSACTION_VALIDATED_MOVE_DELAY + 400) + } else { + updateTransactions() + } + } + }, + '[data-selector="internal-transactions-list"]': { + load ($el) { + return { + internalTransactions: $el.children().map((index, el) => ({ + internalTransactionHtml: el.outerHTML + })).toArray() } - if (oldState.newBlock !== state.newBlock) { - slideDownPrepend($validationsList, state.newBlock) - updateAllAges() + }, + render ($el, state, oldState) { + if (oldState.internalTransactions === state.internalTransactions) return + const container = $el[0] + const newElements = _.map(state.internalTransactions, ({ internalTransactionHtml }) => $(internalTransactionHtml)[0]) + listMorph(container, newElements, { key: 'dataset.key' }) + } + }, + '[data-selector="channel-batching-count"]': { + render ($el, state, oldState) { + const $channelBatching = $('[data-selector="channel-batching-message"]') + if (!state.internalTransactionsBatch.length) return $channelBatching.hide() + $channelBatching.show() + $el[0].innerHTML = numeral(state.internalTransactionsBatch.length).format() + } + }, + '[data-selector="validations-list"]': { + load ($el) { + return { + validatedBlocks: $el.children().map((index, el) => ({ + blockNumber: parseInt(el.dataset.blockNumber), + blockHtml: el.outerHTML + })).toArray() } + }, + render ($el, state, oldState) { + if (oldState.validatedBlocks === state.validatedBlocks) return + const container = $el[0] + const newElements = _.map(state.validatedBlocks, ({ blockHtml }) => $(blockHtml)[0]) + listMorph(container, newElements, { key: 'dataset.blockNumber' }) + } + }, + '[data-selector="next-page-button"]': { + load ($el) { + return { + nextPageUrl: `${$el.hide().attr('href')}&type=JSON` + } + } + } +} + +const $addressDetailsPage = $('[data-page="address-details"]') +if ($addressDetailsPage.length) { + const store = createStore(reducer) + const addressHash = $addressDetailsPage[0].dataset.pageAddressHash + const { filter, blockNumber } = humps.camelizeKeys(URI(window.location).query(true)) + store.dispatch({ + type: 'PAGE_LOAD', + addressHash, + filter, + beyondPageOne: !!blockNumber + }) + connectElements({ store, elements }) + + const addressChannel = socket.channel(`addresses:${addressHash}`, {}) + addressChannel.join() + addressChannel.onError(() => store.dispatch({ + type: 'CHANNEL_DISCONNECTED' + })) + addressChannel.on('balance', (msg) => store.dispatch({ + type: 'RECEIVED_UPDATED_BALANCE', + msg: humps.camelizeKeys(msg) + })) + addressChannel.on('internal_transaction', batchChannel((msgs) => store.dispatch({ + type: 'RECEIVED_NEW_INTERNAL_TRANSACTION_BATCH', + msgs: humps.camelizeKeys(msgs) + }))) + addressChannel.on('pending_transaction', (msg) => store.dispatch({ + type: 'RECEIVED_NEW_PENDING_TRANSACTION', + msg: humps.camelizeKeys(msg) + })) + addressChannel.on('transaction', (msg) => { + store.dispatch({ + type: 'RECEIVED_NEW_TRANSACTION', + msg: humps.camelizeKeys(msg) + }) + setTimeout(() => store.dispatch({ + type: 'REMOVE_PENDING_TRANSACTION', + msg: humps.camelizeKeys(msg) + }), TRANSACTION_VALIDATED_MOVE_DELAY) + }) + + const blocksChannel = socket.channel(`blocks:${addressHash}`, {}) + blocksChannel.join() + blocksChannel.onError(() => store.dispatch({ + type: 'CHANNEL_DISCONNECTED' + })) + blocksChannel.on('new_block', (msg) => store.dispatch({ + type: 'RECEIVED_NEW_BLOCK', + msg: humps.camelizeKeys(msg) + })) + + $('[data-selector="transactions-list"]').length && onScrollBottom(function loadMoreTransactions () { + const { loadingNextPage, nextPageUrl, pagingError } = store.getState() + if (!loadingNextPage && nextPageUrl && !pagingError) { + store.dispatch({ + type: 'LOADING_NEXT_PAGE' + }) + $.get(nextPageUrl) + .done(msg => { + store.dispatch({ + type: 'RECEIVED_NEXT_TRANSACTIONS_PAGE', + msg: humps.camelizeKeys(msg) + }) + }) + .fail(() => { + store.dispatch({ + type: 'PAGING_ERROR' + }) + }) } }) } diff --git a/apps/block_scout_web/assets/js/pages/block.js b/apps/block_scout_web/assets/js/pages/blocks.js similarity index 100% rename from apps/block_scout_web/assets/js/pages/block.js rename to apps/block_scout_web/assets/js/pages/blocks.js diff --git a/apps/block_scout_web/assets/js/pages/pending_transactions.js b/apps/block_scout_web/assets/js/pages/pending_transactions.js new file mode 100644 index 0000000000..75c3ef8d0f --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/pending_transactions.js @@ -0,0 +1,131 @@ +import $ from 'jquery' +import _ from 'lodash' +import URI from 'urijs' +import humps from 'humps' +import numeral from 'numeral' +import socket from '../socket' +import { updateAllAges } from '../lib/from_now' +import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' + +const BATCH_THRESHOLD = 10 + +export const initialState = { + newPendingTransactionHashesBatch: [], + beyondPageOne: null, + channelDisconnected: false, + newPendingTransactions: [], + newTransactionHashes: [], + pendingTransactionCount: null +} + +export function reducer (state = initialState, action) { + switch (action.type) { + case 'PAGE_LOAD': { + return Object.assign({}, state, { + beyondPageOne: action.beyondPageOne, + pendingTransactionCount: numeral(action.pendingTransactionCount).value() + }) + } + case 'CHANNEL_DISCONNECTED': { + return Object.assign({}, state, { + channelDisconnected: true + }) + } + 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 + }) + } + } + default: + return state + } +} + +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('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('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(() => { + if ($transaction.length === 1 && $transaction.siblings().length === 0 && state.pendingTransactionCount > 0) { + window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString() + } else { + slideUpRemove($transaction) + } + }, 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) + slideDownPrepend($pendingTransactionsList, 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 beda3482c4..335a59e974 100644 --- a/apps/block_scout_web/assets/js/pages/transaction.js +++ b/apps/block_scout_web/assets/js/pages/transaction.js @@ -1,42 +1,19 @@ import $ from 'jquery' -import _ from 'lodash' -import URI from 'urijs' import humps from 'humps' import numeral from 'numeral' import socket from '../socket' -import { updateAllAges } from '../lib/from_now' -import { batchChannel, initRedux, slideDownPrepend, slideUpRemove } from '../utils' - -const BATCH_THRESHOLD = 10 +import { initRedux } from '../utils' export const initialState = { - batchCountAccumulator: 0, - newPendingTransactionHashesBatch: [], - beyondPageOne: null, blockNumber: null, - channelDisconnected: false, - confirmations: null, - newPendingTransactions: [], - newTransactions: [], - newTransactionHashes: [], - transactionCount: null, - pendingTransactionCount: null + confirmations: null } export function reducer (state = initialState, action) { switch (action.type) { case 'PAGE_LOAD': { return Object.assign({}, state, { - beyondPageOne: action.beyondPageOne, - blockNumber: parseInt(action.blockNumber, 10), - transactionCount: numeral(action.transactionCount).value(), - pendingTransactionCount: numeral(action.pendingTransactionCount).value() - }) - } - case 'CHANNEL_DISCONNECTED': { - return Object.assign({}, state, { - channelDisconnected: true, - batchCountAccumulator: 0 + blockNumber: parseInt(action.blockNumber, 10) }) } case 'RECEIVED_NEW_BLOCK': { @@ -46,62 +23,6 @@ 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) 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, { - newTransactions: [ - ...state.newTransactions, - ..._.map(action.msgs, 'transactionHtml') - ], - transactionCount - }) - } else { - return Object.assign({}, state, { - batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, - transactionCount - }) - } - } default: return state } @@ -134,104 +55,3 @@ 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('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('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(() => { - if ($transaction.length === 1 && $transaction.siblings().length === 0 && state.pendingTransactionCount > 0) { - window.location.href = URI(window.location).removeQuery('inserted_at').removeQuery('hash').toString() - } else { - slideUpRemove($transaction) - } - }, 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) - slideDownPrepend($pendingTransactionsList, newTransactionsToInsert.reverse().join('')) - - updateAllAges() - } - } - }) -} - -const $transactionListPage = $('[data-page="transaction-list"]') -if ($transactionListPage.length) { - initRedux(reducer, { - main (store) { - store.dispatch({ - type: 'PAGE_LOAD', - transactionCount: $('[data-selector="transaction-count"]').text(), - beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index - }) - const transactionsChannel = socket.channel(`transactions:new_transaction`) - transactionsChannel.join() - transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) - transactionsChannel.on('transaction', batchChannel((msgs) => - store.dispatch({ type: 'RECEIVED_NEW_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 $transactionsList = $('[data-selector="transactions-list"]') - const $transactionCount = $('[data-selector="transaction-count"]') - - if (state.channelDisconnected) $channelDisconnected.show() - if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) - if (state.batchCountAccumulator) { - $channelBatching.show() - $channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() - } else { - $channelBatching.hide() - } - if (oldState.newTransactions !== state.newTransactions) { - const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length) - slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join('')) - - updateAllAges() - } - } - }) -} diff --git a/apps/block_scout_web/assets/js/pages/transactions.js b/apps/block_scout_web/assets/js/pages/transactions.js new file mode 100644 index 0000000000..04544e83b4 --- /dev/null +++ b/apps/block_scout_web/assets/js/pages/transactions.js @@ -0,0 +1,100 @@ +import $ from 'jquery' +import _ from 'lodash' +import URI from 'urijs' +import humps from 'humps' +import numeral from 'numeral' +import socket from '../socket' +import { updateAllAges } from '../lib/from_now' +import { batchChannel, initRedux, slideDownPrepend } from '../utils' + +const BATCH_THRESHOLD = 10 + +export const initialState = { + batchCountAccumulator: 0, + beyondPageOne: null, + channelDisconnected: false, + newTransactions: [], + transactionCount: null +} + +export function reducer (state = initialState, action) { + switch (action.type) { + case 'PAGE_LOAD': { + return Object.assign({}, state, { + beyondPageOne: action.beyondPageOne, + transactionCount: numeral(action.transactionCount).value() + }) + } + case 'CHANNEL_DISCONNECTED': { + return Object.assign({}, state, { + channelDisconnected: true, + batchCountAccumulator: 0 + }) + } + case 'RECEIVED_NEW_TRANSACTION_BATCH': { + 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, { + newTransactions: [ + ...state.newTransactions, + ..._.map(action.msgs, 'transactionHtml') + ], + transactionCount + }) + } else { + return Object.assign({}, state, { + batchCountAccumulator: state.batchCountAccumulator + action.msgs.length, + transactionCount + }) + } + } + default: + return state + } +} + +const $transactionListPage = $('[data-page="transaction-list"]') +if ($transactionListPage.length) { + initRedux(reducer, { + main (store) { + store.dispatch({ + type: 'PAGE_LOAD', + transactionCount: $('[data-selector="transaction-count"]').text(), + beyondPageOne: !!humps.camelizeKeys(URI(window.location).query(true)).index + }) + const transactionsChannel = socket.channel(`transactions:new_transaction`) + transactionsChannel.join() + transactionsChannel.onError(() => store.dispatch({ type: 'CHANNEL_DISCONNECTED' })) + transactionsChannel.on('transaction', batchChannel((msgs) => + store.dispatch({ type: 'RECEIVED_NEW_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 $transactionsList = $('[data-selector="transactions-list"]') + const $transactionCount = $('[data-selector="transaction-count"]') + + if (state.channelDisconnected) $channelDisconnected.show() + if (oldState.transactionCount !== state.transactionCount) $transactionCount.empty().append(numeral(state.transactionCount).format()) + if (state.batchCountAccumulator) { + $channelBatching.show() + $channelBatchingCount[0].innerHTML = numeral(state.batchCountAccumulator).format() + } else { + $channelBatching.hide() + } + if (oldState.newTransactions !== state.newTransactions) { + const newTransactionsToInsert = state.newTransactions.slice(oldState.newTransactions.length) + slideDownPrepend($transactionsList, newTransactionsToInsert.reverse().join('')) + + updateAllAges() + } + } + }) +} diff --git a/apps/block_scout_web/assets/js/utils.js b/apps/block_scout_web/assets/js/utils.js index ed69186d1c..28f36b3cfd 100644 --- a/apps/block_scout_web/assets/js/utils.js +++ b/apps/block_scout_web/assets/js/utils.js @@ -1,6 +1,8 @@ import $ from 'jquery' import _ from 'lodash' -import { createStore } from 'redux' +import { createStore as reduxCreateStore } from 'redux' +import morph from 'nanomorph' +import { updateAllAges } from './lib/from_now' export function batchChannel (func) { let msgs = [] @@ -40,6 +42,37 @@ export function initRedux (reducer, { main, render, debug } = {}) { if (main) main(store) } +export function createStore (reducer) { + return reduxCreateStore(reducer, window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()) +} + +export function connectElements ({ elements, store }) { + function loadElements () { + return _.reduce(elements, (pageLoadParams, { load }, selector) => { + if (!load) return pageLoadParams + const $el = $(selector) + if (!$el.length) return pageLoadParams + const morePageLoadParams = load($el, store) + return _.isObject(morePageLoadParams) ? Object.assign(pageLoadParams, morePageLoadParams) : pageLoadParams + }, {}) + } + function renderElements (state, oldState) { + _.forIn(elements, ({ render }, selector) => { + if (!render) return + const $el = $(selector) + if (!$el.length) return + render($el, state, oldState) + }) + } + store.dispatch(Object.assign(loadElements(), { type: 'ELEMENTS_LOAD' })) + let oldState = store.getState() + store.subscribe(() => { + const state = store.getState() + renderElements(state, oldState) + oldState = state + }) +} + export function skippedBlockListBuilder (skippedBlockNumbers, newestBlock, oldestBlock) { for (let i = newestBlock - 1; i > oldestBlock; i--) skippedBlockNumbers.push(i) return skippedBlockNumbers @@ -52,6 +85,13 @@ export function slideDownPrepend ($container, content) { } }) } +export function slideDownAppend ($container, content) { + smarterSlideDown($(content), { + insert ($el) { + $container.append($el) + } + }) +} export function slideDownBefore ($container, content) { smarterSlideDown($(content), { insert ($el) { @@ -105,3 +145,71 @@ function smarterSlideUp ($el, { complete = _.noop } = {}) { $el.slideUp({ complete, easing: 'linear' }) } } + +// The goal of this function is to DOM diff lists, so upon completion `container.innerHTML` should be +// equivalent to `newElements.join('')`. +// +// We could simply do `container.innerHTML = newElements.join('')` but that would not be efficient and +// it not animate appropriately. We could also simply use `morph` (or a similar library) on the entire +// list, however that doesn't give us the proper amount of control for animations. +// +// This function will walk though, remove items currently in `container` which are not in the new list. +// Then it will swap the contents of the items that are in both lists in case the items were updated or +// the order changed. Finally, it will add elements to `container` which are in the new list and didn't +// already exist in the DOM. +// +// Params: +// container: the DOM element which contents need replaced +// newElements: a list of elements that need to be put into the container +// options: +// key: the path to the unique identifier of each element +// horizontal: our horizontal animations are handled in CSS, so passing in `true` will not play JS +// animations +export function listMorph (container, newElements, { key, horizontal } = {}) { + if (!container) return + const oldElements = $(container).children().get() + let currentList = _.map(oldElements, (el) => ({ id: _.get(el, key), el })) + const newList = _.map(newElements, (el) => ({ id: _.get(el, key), el })) + const overlap = _.intersectionBy(newList, currentList, 'id').map(({ id, el }) => ({ id, el: updateAllAges($(el))[0] })) + + // remove old items + const removals = _.differenceBy(currentList, newList, 'id') + let canAnimate = !horizontal && removals.length <= 1 + removals.forEach(({ el }) => { + if (!canAnimate) return el.remove() + const $el = $(el) + $el.addClass('shrink-out') + setTimeout(() => { slideUpRemove($el) }, 400) + }) + currentList = _.differenceBy(currentList, removals, 'id') + + // update kept items + currentList = currentList.map(({ el }, i) => ({ + id: overlap[i].id, + el: el.outerHTML === overlap[i].el.outerHTML ? el : morph(el, overlap[i].el) + })) + + // add new items + const finalList = newList.map(({ id, el }) => _.get(_.find(currentList, { id }), 'el', el)).reverse() + canAnimate = !horizontal + finalList.forEach((el, i) => { + if (el.parentElement) return + if (!canAnimate) return container.insertBefore(el, _.get(finalList, `[${i - 1}]`)) + canAnimate = false + if (!_.get(finalList, `[${i - 1}]`)) return slideDownAppend($(container), el) + slideDownBefore($(_.get(finalList, `[${i - 1}]`)), el) + }) +} + +export function onScrollBottom (callback) { + const $window = $(window) + function infiniteScrollChecker () { + const scrollHeight = $(document).height() + const scrollPosition = $window.height() + $window.scrollTop() + if ((scrollHeight - scrollPosition) / scrollHeight === 0) { + callback() + } + } + infiniteScrollChecker() + $window.on('scroll', infiniteScrollChecker) +} diff --git a/apps/block_scout_web/assets/package-lock.json b/apps/block_scout_web/assets/package-lock.json index c9c461305c..a9a49fb7c1 100644 --- a/apps/block_scout_web/assets/package-lock.json +++ b/apps/block_scout_web/assets/package-lock.json @@ -3789,8 +3789,7 @@ "version": "2.1.1", "resolved": false, "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true + "dev": true }, "aproba": { "version": "1.2.0", @@ -3814,15 +3813,13 @@ "version": "1.0.0", "resolved": false, "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "resolved": false, "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -3839,22 +3836,19 @@ "version": "1.1.0", "resolved": false, "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "resolved": false, "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "resolved": false, "integrity": "sha1-PXz0Rk22RG6mRL9LOVB/mFEAjo4=", - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -3985,8 +3979,7 @@ "version": "2.0.3", "resolved": false, "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4000,7 +3993,6 @@ "resolved": false, "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4017,7 +4009,6 @@ "resolved": false, "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4026,15 +4017,13 @@ "version": "0.0.8", "resolved": false, "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.2.4", "resolved": false, "integrity": "sha512-hzXIWWet/BzWhYs2b+u7dRHlruXhwdgvlTMDKC6Cb1U7ps6Ac6yQlR39xsbjWJE377YTCtKwIXIpJ5oP+j5y8g==", "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.1", "yallist": "^3.0.0" @@ -4055,7 +4044,6 @@ "resolved": false, "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4144,8 +4132,7 @@ "version": "1.0.1", "resolved": false, "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4159,7 +4146,6 @@ "resolved": false, "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -4255,8 +4241,7 @@ "version": "5.1.1", "resolved": false, "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", - "dev": true, - "optional": true + "dev": true }, "safer-buffer": { "version": "2.1.2", @@ -4298,7 +4283,6 @@ "resolved": false, "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -4320,7 +4304,6 @@ "resolved": false, "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", "dev": true, - "optional": true, "requires": { "ansi-regex": "^2.0.0" } @@ -4369,15 +4352,13 @@ "version": "1.0.2", "resolved": false, "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", - "dev": true, - "optional": true + "dev": true }, "yallist": { "version": "3.0.2", "resolved": false, "integrity": "sha1-hFK0u36Dx8GI2AQcGoN8dz1ti7k=", - "dev": true, - "optional": true + "dev": true } } }, @@ -6428,6 +6409,11 @@ "integrity": "sha512-bAdJv7fBLhWC+/Bls0Oza+mvTaNQtP+1RyhhhvD95pgUJz6XM5IzgmxOkItJ9tkoCiplvAnXI1tNmmUD/eScyA==", "dev": true }, + "nanoassert": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/nanoassert/-/nanoassert-1.1.0.tgz", + "integrity": "sha1-TzFS4JVA/eKMdvRLGbvNHVpCR40=" + }, "nanomatch": { "version": "1.2.13", "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", @@ -6455,6 +6441,14 @@ } } }, + "nanomorph": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/nanomorph/-/nanomorph-5.1.3.tgz", + "integrity": "sha512-VydkKjFWU/DAO0R10awFASRNXQKHrZUMdMIiNcdmWm+IhuifuPOw/dDtpiQ1cNROF8f3ATPrcKRVarEayQJOqA==", + "requires": { + "nanoassert": "^1.1.0" + } + }, "natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", diff --git a/apps/block_scout_web/assets/package.json b/apps/block_scout_web/assets/package.json index a3c05cba5a..be66f75088 100644 --- a/apps/block_scout_web/assets/package.json +++ b/apps/block_scout_web/assets/package.json @@ -28,6 +28,7 @@ "jquery": "^3.3.1", "lodash": "^4.17.10", "moment": "^2.22.1", + "nanomorph": "^5.1.3", "numeral": "^2.0.6", "path-parser": "^4.1.1", "phoenix": "file:../../../deps/phoenix", 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 bb3511a1b3..08e4e5c321 100644 --- a/apps/block_scout_web/lib/block_scout_web/chain.ex +++ b/apps/block_scout_web/lib/block_scout_web/chain.ex @@ -16,7 +16,7 @@ defmodule BlockScoutWeb.Chain do alias Explorer.Chain.{ Address, - Address.TokenBalance, + Address.CurrentTokenBalance, Block, InternalTransaction, Log, @@ -198,7 +198,7 @@ defmodule BlockScoutWeb.Chain do %{"token_name" => name, "token_type" => type, "token_inserted_at" => inserted_at_datetime} end - defp paging_params(%TokenBalance{address_hash: address_hash, value: value}) do + defp paging_params(%CurrentTokenBalance{address_hash: address_hash, value: value}) do %{"address_hash" => to_string(address_hash), "value" => Decimal.to_integer(value)} end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex index 3c29ffa4f3..22825ae63a 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_controller.ex @@ -7,9 +7,10 @@ defmodule BlockScoutWeb.AddressController do def index(conn, _params) do render(conn, "index.html", - addresses: Chain.list_top_addresses(), + address_tx_count_pairs: Chain.list_top_addresses(), address_estimated_count: Chain.address_estimated_count(), - exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null() + exchange_rate: Market.get_exchange_rate(Explorer.coin()) || Token.null(), + total_supply: Chain.total_supply() ) end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex index 3a797d2974..d33d54ccb8 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/address_transaction_controller.ex @@ -8,34 +8,87 @@ defmodule BlockScoutWeb.AddressTransactionController do import BlockScoutWeb.AddressController, only: [transaction_count: 1, validation_count: 1] import BlockScoutWeb.Chain, only: [current_filter: 1, paging_options: 1, next_page_params: 3, split_list_by_page: 1] + alias BlockScoutWeb.TransactionView alias Explorer.{Chain, Market} + alias Explorer.Chain.Hash alias Explorer.ExchangeRates.Token + alias Phoenix.View + + @transaction_necessity_by_association [ + necessity_by_association: %{ + [created_contract_address: :names] => :optional, + [from_address: :names] => :optional, + [to_address: :names] => :optional, + :token_transfers => :optional + } + ] + + def index(conn, %{"address_id" => address_hash_string, "type" => "JSON"} = params) do + with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), + {:ok, address} <- Chain.hash_to_address(address_hash) do + options = + @transaction_necessity_by_association + |> put_in([:necessity_by_association, :block], :required) + |> Keyword.merge(paging_options(params)) + |> Keyword.merge(current_filter(params)) + + {transactions, next_page} = get_transactions_and_next_page(address, options) + + next_page_url = + case next_page_params(next_page, transactions, params) do + nil -> + nil + + next_page_params -> + address_transaction_path( + conn, + :index, + address, + next_page_params + ) + end + + json( + conn, + %{ + transactions: + Enum.map(transactions, fn transaction -> + %{ + transaction_hash: Hash.to_string(transaction.hash), + transaction_html: + View.render_to_string( + TransactionView, + "_tile.html", + current_address: address, + transaction: transaction + ) + } + end), + next_page_url: next_page_url + } + ) + else + :error -> + unprocessable_entity(conn) + + {:error, :not_found} -> + not_found(conn) + end + end def index(conn, %{"address_id" => address_hash_string} = params) do with {:ok, address_hash} <- Chain.string_to_address_hash(address_hash_string), {:ok, address} <- Chain.hash_to_address(address_hash) do pending_options = - [ - necessity_by_association: %{ - [created_contract_address: :names] => :optional, - [from_address: :names] => :optional, - [to_address: :names] => :optional, - :token_transfers => :optional - } - ] - |> Keyword.merge(paging_options(params)) + @transaction_necessity_by_association + |> Keyword.merge(paging_options(%{})) |> Keyword.merge(current_filter(params)) full_options = put_in(pending_options, [:necessity_by_association, :block], :required) - transactions_plus_one = Chain.address_to_transactions(address, full_options) - {transactions, next_page} = split_list_by_page(transactions_plus_one) + {transactions, next_page} = get_transactions_and_next_page(address, full_options) - pending_transactions = - case Map.has_key?(params, "block_number") do - true -> [] - false -> Chain.address_to_pending_transactions(address, pending_options) - end + pending_transactions = Chain.address_to_pending_transactions(address, pending_options) render( conn, @@ -57,4 +110,9 @@ defmodule BlockScoutWeb.AddressTransactionController do not_found(conn) end end + + defp get_transactions_and_next_page(address, options) do + transactions_plus_one = Chain.address_to_transactions(address, options) + split_list_by_page(transactions_plus_one) + end end diff --git a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex index a167d92638..7b5104a458 100644 --- a/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex +++ b/apps/block_scout_web/lib/block_scout_web/controllers/api/rpc/transaction_controller.ex @@ -3,6 +3,26 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do alias Explorer.Chain + def gettxinfo(conn, params) do + with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params), + {:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param), + {:transaction, {:ok, transaction}} <- transaction_from_hash(transaction_hash) do + max_block_number = max_block_number() + + logs = Chain.transaction_to_logs(transaction) + render(conn, :gettxinfo, %{transaction: transaction, max_block_number: max_block_number, logs: logs}) + else + {:transaction, :error} -> + render(conn, :error, error: "Transaction not found") + + {:txhash_param, :error} -> + render(conn, :error, error: "Query parameter txhash is required") + + {:format, :error} -> + render(conn, :error, error: "Invalid txhash format") + end + end + def gettxreceiptstatus(conn, params) do with {:txhash_param, {:ok, txhash_param}} <- fetch_txhash(params), {:format, {:ok, transaction_hash}} <- to_transaction_hash(txhash_param) do @@ -35,6 +55,13 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do {:txhash_param, Map.fetch(params, "txhash")} end + defp transaction_from_hash(transaction_hash) do + case Chain.hash_to_transaction(transaction_hash, necessity_by_association: %{block: :required}) do + {:error, :not_found} -> {:transaction, :error} + {:ok, transaction} -> {:transaction, {:ok, transaction}} + end + end + defp to_transaction_hash(transaction_hash_string) do {:format, Chain.string_to_transaction_hash(transaction_hash_string)} end @@ -54,4 +81,11 @@ defmodule BlockScoutWeb.API.RPC.TransactionController do _ -> "" end end + + defp max_block_number do + case Chain.max_block_number() do + {:ok, number} -> number + {:error, :not_found} -> 0 + end + end end diff --git a/apps/block_scout_web/lib/block_scout_web/etherscan.ex b/apps/block_scout_web/lib/block_scout_web/etherscan.ex index 033be85e52..4bb6a624d2 100644 --- a/apps/block_scout_web/lib/block_scout_web/etherscan.ex +++ b/apps/block_scout_web/lib/block_scout_web/etherscan.ex @@ -321,6 +321,30 @@ defmodule BlockScoutWeb.Etherscan do "result" => nil } + @transaction_gettxinfo_example_value %{ + "status" => "1", + "result" => %{ + "blockNumber" => "3", + "confirmations" => "0", + "from" => "0x000000000000000000000000000000000000000c", + "gasLimit" => "91966", + "gasUsed" => "95123", + "hash" => "0x0000000000000000000000000000000000000000000000000000000000000004", + "input" => "0x04", + "logs" => [ + %{ + "address" => "0x000000000000000000000000000000000000000e", + "data" => "0x00", + "topics" => ["First Topic", "Second Topic", "Third Topic", "Fourth Topic"] + } + ], + "success" => true, + "timeStamp" => "1541018182", + "to" => "0x000000000000000000000000000000000000000d", + "value" => "67612" + } + } + @transaction_gettxreceiptstatus_example_value %{ "status" => "1", "message" => "OK", @@ -428,6 +452,28 @@ defmodule BlockScoutWeb.Etherscan do example: ~s("18") } + @logs_details %{ + name: "Log Detail", + fields: %{ + address: @address_hash_type, + topics: %{ + type: "topics", + definition: "An array including the topics for the log.", + example: ~s(["0xf63780e752c6a54a94fc52715dbc5518a3b4c3c2833d301a204226548a2a8545"]) + }, + data: %{ + type: "data", + definition: "Non-indexed log parameters.", + example: ~s("0x") + }, + blockNumber: %{ + type: "block number", + definition: "A nonnegative number used to identify blocks.", + example: ~s("0x5c958") + } + } + } + @address_balance %{ name: "AddressBalance", fields: %{ @@ -735,6 +781,35 @@ defmodule BlockScoutWeb.Etherscan do } } + @transaction_info_model %{ + name: "TransactionInfo", + fields: %{ + hash: @transaction_hash_type, + timeStamp: %{ + type: "timestamp", + definition: "The transaction's block-timestamp.", + example: ~s("1439232889") + }, + blockNumber: @block_number_type, + confirmations: @confirmation_type, + success: %{ + type: "boolean", + definition: "Flag for success during tx execution", + example: ~s(true) + }, + from: @address_hash_type, + to: @address_hash_type, + value: @wei_type, + input: @input_type, + gasLimit: @wei_type, + gasUsed: @gas_type, + logs: %{ + type: "array", + array_type: @logs_details + } + } + } + @transaction_status_model %{ name: "TransactionStatus", fields: %{ @@ -1569,6 +1644,43 @@ defmodule BlockScoutWeb.Etherscan do ] } + @transaction_gettxinfo_action %{ + name: "gettxinfo", + description: "Get transaction info.", + required_params: [ + %{ + key: "txhash", + placeholder: "transactionHash", + type: "string", + description: "Transaction hash. Hash of contents of the transaction." + } + ], + optional_params: [], + responses: [ + %{ + code: "200", + description: "successful operation", + example_value: Jason.encode!(@transaction_gettxinfo_example_value), + model: %{ + name: "Result", + fields: %{ + status: @status_type, + message: @message_type, + result: %{ + type: "model", + model: @transaction_info_model + } + } + } + }, + %{ + code: "200", + description: "error", + example_value: Jason.encode!(@transaction_gettxreceiptstatus_example_value_error) + } + ] + } + @transaction_gettxreceiptstatus_action %{ name: "gettxreceiptstatus", description: "Get transaction receipt status.", @@ -1692,6 +1804,7 @@ defmodule BlockScoutWeb.Etherscan do @transaction_module %{ name: "transaction", actions: [ + @transaction_gettxinfo_action, @transaction_gettxreceiptstatus_action, @transaction_getstatus_action ] diff --git a/apps/block_scout_web/lib/block_scout_web/resolvers/address.ex b/apps/block_scout_web/lib/block_scout_web/resolvers/address.ex new file mode 100644 index 0000000000..f731f1c7cd --- /dev/null +++ b/apps/block_scout_web/lib/block_scout_web/resolvers/address.ex @@ -0,0 +1,12 @@ +defmodule BlockScoutWeb.Resolvers.Address do + @moduledoc false + + alias Explorer.Chain + + def get_by(_, %{hashes: hashes}, _) do + case Chain.hashes_to_addresses(hashes) do + [] -> {:error, "Addresses not found."} + result -> {:ok, result} + end + end +end 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 b249f03174..dcf9717329 100644 --- a/apps/block_scout_web/lib/block_scout_web/router.ex +++ b/apps/block_scout_web/lib/block_scout_web/router.ex @@ -38,12 +38,18 @@ defmodule BlockScoutWeb.Router do }) end - forward("/graphql", Absinthe.Plug, schema: BlockScoutWeb.Schema) + forward("/graphql", Absinthe.Plug, + schema: BlockScoutWeb.Schema, + analyze_complexity: true, + max_complexity: 50 + ) forward("/graphiql", Absinthe.Plug.GraphiQL, schema: BlockScoutWeb.Schema, interface: :playground, - socket: BlockScoutWeb.UserSocket + socket: BlockScoutWeb.UserSocket, + analyze_complexity: true, + max_complexity: 50 ) scope "/", BlockScoutWeb do diff --git a/apps/block_scout_web/lib/block_scout_web/schema.ex b/apps/block_scout_web/lib/block_scout_web/schema.ex index a50d29f3b9..03b2ee7c8e 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema.ex @@ -3,11 +3,18 @@ defmodule BlockScoutWeb.Schema do use Absinthe.Schema - alias BlockScoutWeb.Resolvers.{Block, Transaction} + alias BlockScoutWeb.Resolvers.{Address, Block, Transaction} import_types(BlockScoutWeb.Schema.Types) query do + @desc "Gets addresses by address hash." + field :addresses, list_of(:address) do + arg(:hashes, non_null(list_of(non_null(:address_hash)))) + resolve(&Address.get_by/3) + complexity(fn %{hashes: hashes}, child_complexity -> length(hashes) * child_complexity end) + end + @desc "Gets a block by number." field :block, :block do arg(:number, non_null(:integer)) diff --git a/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex b/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex index 5f71182cab..de42a24512 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema/scalars.ex @@ -3,7 +3,7 @@ defmodule BlockScoutWeb.Schema.Scalars do use Absinthe.Schema.Notation - alias Explorer.Chain.{Hash, Wei} + alias Explorer.Chain.{Data, Hash, Wei} alias Explorer.Chain.Hash.{Address, Full, Nonce} @desc """ @@ -24,6 +24,23 @@ defmodule BlockScoutWeb.Schema.Scalars do serialize(&to_string/1) end + @desc """ + An unpadded hexadecimal number with 0 or more digits. Each pair of digits + maps directly to a byte in the underlying binary representation. When + interpreted as a number, it should be treated as big-endian. + """ + scalar :data do + parse(fn + %Absinthe.Blueprint.Input.String{value: value} -> + Data.cast(value) + + _ -> + :error + end) + + serialize(&to_string/1) + end + @desc """ A 32-byte [KECCAK-256](https://en.wikipedia.org/wiki/SHA-3) hash. """ diff --git a/apps/block_scout_web/lib/block_scout_web/schema/types.ex b/apps/block_scout_web/lib/block_scout_web/schema/types.ex index 66da7a55d4..b88306585b 100644 --- a/apps/block_scout_web/lib/block_scout_web/schema/types.ex +++ b/apps/block_scout_web/lib/block_scout_web/schema/types.ex @@ -6,12 +6,23 @@ defmodule BlockScoutWeb.Schema.Types do import_types(Absinthe.Type.Custom) import_types(BlockScoutWeb.Schema.Scalars) + @desc """ + A stored representation of a Web3 address. + """ + object :address do + field(:hash, :address_hash) + field(:fetched_coin_balance, :wei) + field(:fetched_coin_balance_block_number, :integer) + field(:contract_code, :data) + end + @desc """ A package of data that contains zero or more transactions, the hash of the previous block ("parent"), and optionally other data. Because each block (except for the initial "genesis block") points to the previous block, the data structure that they form is called a "blockchain". """ object :block do + field(:hash, :full_hash) field(:consensus, :boolean) field(:difficulty, :decimal) field(:gas_limit, :decimal) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex index b1003b4638..0a6ac3375c 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/_tile.html.eex @@ -15,7 +15,7 @@ - <%= transaction_count(@address) %> + <%= @tx_count %> <%= gettext "Transactions sent" %> <% if validator?(@address) do %> @@ -36,7 +36,7 @@ data-usd-exchange-rate="<%= @exchange_rate.usd_value %>"> - (<%= balance_percentage(@address) %>) + (<%= balance_percentage(@address, @total_supply) %>) diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex index 13db6de433..851aa9a0d7 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address/index.html.eex @@ -9,9 +9,10 @@
- <%= for {address, index} <- Enum.with_index(@addresses, 1) do %> + <%= for {{address, tx_count}, index} <- Enum.with_index(@address_tx_count_pairs, 1) do %> <%= render "_tile.html", address: address, index: index, exchange_rate: @exchange_rate, + total_supply: @total_supply, tx_count: tx_count, validation_count: validation_count(address) %> <% end %> diff --git a/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex b/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex index 17e929e972..e866c4ae19 100644 --- a/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex +++ b/apps/block_scout_web/lib/block_scout_web/templates/address_internal_transaction/index.html.eex @@ -8,12 +8,12 @@